本文最后更新于 2 分钟前,文中所描述的信息可能已发生改变。
常见问题
JVM 的组成部分有那些?
- 类加载器(ClassLoader):负责将字节码文件加载到运行时数据区。只负责加载字节码文件,不负责验证字节码文件。
- 运行时数据区(Runtime Data Area):包括方法区、堆、栈、本地方法栈、程序计数器等。
- 执行引擎(Execution Engine):负责执行字节码文件。将字节码文件解释为机器码执行。
- 本地方法接口(Native Interface):调用本地接口。
内存结构
JVM 的内存结构,每个区域存储什么内容?
- **方法区(Method Area)/ 元空间(Metaspace,JDK8 之后):**存储类的元信息(字段、方法、构造器、接口)、静态变量、运行时常量池、JIT 编译后的代码等。
- JDK7 之前叫永久代(PermGen)。
- JDK8 之后改为元空间(Metaspace),存放在本地内存中。
- **堆(Heap):**存放对象实例和数组,是垃圾回收(GC)的主要区域。
- 按代分为新生代(Young Gen)、老年代(Old Gen)。
- 线程共享。
- **虚拟机栈(Java Virtual Machine Stack):**每个线程私有,生命周期与线程相同。
- 存放局部变量表、操作数栈、动态链接、方法出口等信息。
- 每次方法调用都会创建一个栈帧,随调用过程入栈、出栈。
- **本地方法栈(Native Method Stack):**每个线程私有。
- 为 JVM 执行 Native 方法(通常是 C/C++ 实现)服务。
- **程序计数器(Program Counter Register):**每个线程私有。
- 一块很小的内存区域,用于记录当前线程正在执行的字节码指令地址。
- 如果执行的是 Native 方法,则 PC 计数器值为空。
- **运行时常量池(Runtime Constant Pool):**方法区的一部分。
- 存放编译期生成的字面量和符号引用,类加载后会放入运行时常量池。
- 在运行时解析为直接引用,例如方法引用、字段引用等。
内存模型 (JMM)
详见 helltractor blog 中 Java Memory Model
垃圾回收 (GC)
详见 helltractor blog 中 Garbage Collect
常见问题
Java 垃圾回收的原理机制?
JVM 使用可达性分析算法来判断一个对象是否存活。它从一组称为 GC Roots 的对象出发,沿着引用链向下搜索,如果一个对象无法通过任何 GC Root 访问到,就被认为是不可达,即“垃圾”。
常见的 GC Roots 包括:
- 虚拟机栈中的引用对象(局部变量表);
- 方法区中类的静态属性;
- 方法区中常量引用;
- 本地方法栈中的 JNI 引用;
除了强引用(Strong Reference)外,Java 还提供了三种“引用类型”来配合垃圾回收机制:
- 软引用:内存不足时会被回收,适合缓存;
- 弱引用:下一次 GC 一定会被回收;
- 虚引用:不会影响生命周期,用于对象被回收时收到通知(配合 ReferenceQueue);
Java 中的垃圾回收器?
- Serial GC:单线程的垃圾回收器,适用于单核 CPU 或堆内存较小的场景。它的优点是简单、高效,但因为只有一个线程执行回收,停顿时间较长,可能影响响应时间;
- Parallel GC:多线程的垃圾回收器,专门设计用于多核 CPU 环境。它的目标是通过并行处理来提高吞吐量,适用于对吞吐量要求较高的场景,如批处理系统。缺点是可能会产生较长的停顿时间;
- CMS(Concurrent Mark-Sweep)GC:旨在最小化停顿时间,采用并发标记和清理阶段,通过“停止世界”阶段最小化影响,适合高并发应用,如 Web 服务。但其缺点是容易导致内存碎片,且回收过程中可能出现长时间的“停止世界”现象;
- G1 GC:作为默认垃圾回收器,G1 结合了 CMS 和 Parallel GC 的优点,支持混合回收(Mix GC),通过将堆划分为多个小的 Region,进行增量式回收,能更灵活地控制停顿时间。G1 特别适用于大内存应用,解决了 CMS 的内存碎片问题,并且可以在较低的停顿时间内提供较高的吞吐量。
类加载机制
类加载流程
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
类加载器
txt
// 类加载器层次结构
Bootstrap ClassLoader(启动类加载器)
↓
Extension ClassLoader(扩展类加载器)
↓
Application ClassLoader(应用程序类加载器)
↓
Custom ClassLoader(自定义类加载器)什么是类加载器?
类加载器(ClassLoader)是 Java 虚拟机(JVM)的一部分,用于将字节码文件加载到 JVM 运行时数据区中。 现有的类加载器基本都是 java.lang.ClassLoader 的子类,该类用于将指定的类找到或生成对应的字节码文件。 同时,类加载器负责加载程序所需的资源文件。
有哪些类加载器?
- 启动类加载器(Bootstrap ClassLoader):不继承 ClassLoader。用于加载 JVM 运行时必备的核心类,如 JAVA_HOME/jre/lib 目录下的类。
- 扩展类加载器(Extension ClassLoader):加载 JVM 扩展目录中的类,如 JAVA_HOME/jre/lib/ext 目录中的类。
- 应用程序类加载器(Application ClassLoader):加载应用程序 classpath 目录中的类。
- 自定义类加载器:继承 ClassLoader 类,实现自定义类加载规则。
双亲委派模型 (Parent Delegation Model)
什么是双亲委派模型?
每个类加载器在尝试加载类或资源时,会首先检查请求加载的类型是否已经被加载过,若没有将这个任务委托给它的父加载器去完成。 只有当父加载器无法找到并加载请求的类时,子加载器才会尝试自己去加载该类。
示例代码:
java
protected Class<?> loadClass(String name, boolean resolve) {
// 1. 检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 2. 委派给父加载器
c = parent.loadClass(name, false);
} else {
// 3. 委派给启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载
}
if (c == null) {
// 4. 自己尝试加载
c = findClass(name);
}
}
return c;
}为什么要使用双亲委派机制?双亲委派模型的优点?
- 避免类的重复加载:当父加载器已经加载了一个类时,子加载器不会再加载一次。
- 保证类加载的安全性: 保证 Java 核心 API 不被篡改,防止恶意代码替换 JDK 中的核心类。
- 提供统一的类加载机制。
如何破坏双亲委派模型?
在某些场景中,父类加载器加载不到子模块中的类,这时需要打破双亲委派模型。
- 重写
loadClass()方法:重写loadClass()方法,让当前 ClassLoader 先尝试加载类。 - 重写
findClass()方法:findClass()只会被loadClass()调用,不会委派给父类。重写 findClass()方法,自定义加载规则。 - 使用线程上下文类加载器:通过
Thread.currentThread().setContextClassLoader()设置线程上下文类加载器。临时替换当前线程的类加载器,使得类加载器不再遵循双亲委派模型。如:Tomcat 的 Web 应用程序类加载器就是通过线程上下文类加载器实现的。
常见问题
自定义 java.lang.String 类,能否使用?
- 可以自定义包名不为 java.lang 的 String 类,并区别包名正常使用
- 自定义包名为 java.lang 的 String 类
- String 类下写 main 方法:由于双亲委派模型,在加载 String 类时,会最终委派给 Bootstrap ClassLoader 去加载,加载的是 rt.jar 包中的那个 java.lang.String,而 rt.jar 包中的 String 类是没有 main 方法的,因此报错误
- 启动类也在 java.lang 包下:这里与是否用到 String 类无关,会报 Prohibited package name: java.lang 错误。由于双亲委派,java.lang 包肯定早于自定义的 java.lang 包的加载,就会冲突.
- 调用方法不在 java.lang 包中:此时由于双亲委派模型的存在,并不会加载到自定义的 String 类
- 来自一线程序员 Seven 的探索与实践,持续学习迭代中~
- 本文已收录于我的个人博客:https://www.seven97.top