JVM 组成

JNI:Java Native Interface(本地方法) 用于将 Java 与本地已编译语言交互,可能会丧失平台可移植性,但有其使用场景:沿用旧库、提高性能等。 声明一个 JNI 的方法如下:
public class JNI {// native 关键字修饰public native void callNative();//载入本地库static {System.loadLibrary("libName");}public static void main(String[] args) {// 直接调用 JNI 提供的方法new JNI().callNative();}}
线程私有
- 程序计数器:存储线程执行的字节码行号,不存在 OOM;
- 栈:在栈帧中存储局部变量表、操作数栈、动态链接、方法出口,为 Java 方法服务;
-
线程共享
堆:创建的对象、产生的数据都在堆中,是垃圾回收器主要的内存区域;
方法区:存储常量、静态变量、类信息、JIT 产生的机器码、运行时常量池。
JVM 类加载
类加载阶段
- 符号引用:以一组符号来描述所引用的目标,引用的目标不一定被加载到内存,因此与 JVM 内存布局无关,在 class 文件中以 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等类型的常量出现(代码层面的引用);
直接引用:指向目标的指针,是被加载到内存的引用,因此与 JVM 内存布局有关(内存层面的引用)。
client 方法:编译器根据类中被
static修饰的赋值和语句块生成的方法,若不存在static则不生成。
加载 Loading:JVM 读取 class 文件到方法区内,并在堆中创建 java.lang.Class 对象;
- 验证 Verification:确保 class 文件符合 JVM 要求;
- 准备 Preparation:在方法区中分配内存空间与被
final修饰的变量初始值; - 解析 Resolution:将常量池中的符号引用替换为直接引用;
- 初始化 Initialization:运行 client 方法(先执行父类的 client 方法成功后才会执行子类的 client 方法),以下几种情况不执行该流程:
- 被
final修饰的直接进入常量池,不会触发该类的初始化; - 子类只使用父类被
static修饰的字段时不触发子类初始化; - 定义对象数组不触发初始化;
- 使用类名获取 Class 对象时不触发初始化;
- 使用 Class.
加载特定类时,使用 initialize 参数设置是否需要对该类初始化; - 使用 ClassLoader 默认的 loadClass 方法加载类时,不触发初始化。
- 被
- 使用 Using
- 卸载 Unloading:类不再被需要时从 JVM 中卸载。
类加载的双亲委派机制
在类加载阶段使用双亲委派机制,保障类的唯一性和安全性:将类加载请求向上委派给父加载器完成。
需要注意的是,加载器的父与子关系不是类的“继承”关系,而是父加载器作为子加载器类内变量的方式组合而成。
双亲委派机制如何破坏?
自定义一个类加载器,重写它的loadClass方法,在其中不涉及parent即可。除了
loadClass之外还有其他几个方法:findClass:根据名称/位置加载 .class 字节码。defineClass:字节码转化为 Class。
为什么需要双亲委派机制?
类加载器之间有层次关系(优先级),使用双亲委派机制有以下好处:
- 避免类的重复加载:父加载器加载过一个类之后,子加载器不需要重新加载这个类。
- 保证安全性:Bootstrap ClassLoader 只加载 JAVA_HOME 中的类,不会被随意替换,保证 JDK 中核心 API 的安全。
JVM 内存
JVM 内存分区
GC:Garbage Collection,垃圾回收。
- 新生代:JVM 新创建的对象(大对象除外,直接进入老年代),GC 算法为 MinorGC;
- Eden 区:新创建的对象;
- SurvivorFrom 区:上一次 GC 幸存的对象。
- SurvivorTo 区:保留 GC 幸存的对象。
- 老年代:存放长生命周期对象和大对象,Survivor 中存活轮次(年龄)达到阈值后进入该区,GC 算法为 MajorGC;
永久代:存放 Class 和元数据 Meta 信息,无 GC 流程,JDK 8 后被元数据区取代(元数据区使用 OS 的内存,不受 JVM 限制)
垃圾回收相关
垃圾确定算法
引用计数法
存在对象的引用,引用计数 +1;删除引用,引用计数 -1;引用计数为 0 时判断为垃圾。
存在循环引用的问题:A 中有 B,B 中有 A 的情况。可达性分析
定义一些 GC Roots,通过它到对象是否存在可达路径,不可达则判断为垃圾。
常用 GC Root:栈中的引用;
- JNI 的引用;
- 方法区的静态变量;
- 常量对象;
-
垃圾回收算法
标记清除算法
流程:标记阶段标记需要被回收的对象;清除阶段进行回收与内存释放。
-
复制算法
流程:划分两个区域,新生成的对象在区域 A;A 中对象满了则进行标记并全部复制到区域 B,再将 A 清空。
-
标记整理算法
流程:标记阶段标记需要被回收的对象;整理阶段将存活对象移到内存的一端后清除另一端对象并释放内存。
缺点:针对不同类型对象(生命周期长短、对象大小)无法统一高效。
分代收集算法
流程:
将内存分为新生代、老年代:新生代存新生成的对象,生命周期短且回收数量多;老年代存大对象,生命周期长数量少;
新生代采用复制算法,将内存分为了 Eden 区、SurvivorFrom 区和 SurvivorTo 区,回收过程将 Eden 和 SurvivorFrom 中存活的对象复制到 SurvivorTo 后清空这两个区;
老年代采用标记清除算法。
垃圾收集器
| 垃圾收集器名称 | 适用分代 | 使用垃圾回收算法 | 线程 | 描述 |
|---|---|---|---|---|
| Serial | 新生代 | 复制算法 | 单线程 | 必须暂停全部工作线程,无线程交互开销,是 Client 模式下新生代默认 GC |
| ParNew | 多线程 | Serial 的多线程实现,是 Server 模式下新生代默认 GC | ||
| Parallel Scavenge | 多线程 | 可以控制停顿时间、吞吐量、自适应调节的多线程 GC,效率更高 | ||
| Serial Old | 老年代 | 标记整理算法 | 单线程 | Serial 的老年代实现,是 Client 模式下老年代默认 GC |
| Parallel Old | 多线程 | 优先考虑吞吐量,其次考虑停顿时间的 GC | ||
| CMS (Concurrent Mark Sweep) |
标记清除算法 | 优先考虑停顿时间,由于标记与清除的阶段采用兵并发,因此无需暂停用户线程,只需要在标记 GC Roots 直接关联对象的时候暂停 | ||
| G1 (Garbage First) |
不区分 | 标记整理算法 | 避免 Full GC 引起的停顿,将堆分为大小固定的几个独立区域进行 GC,优先回收垃圾最多的区域,可以控制停顿时间 |
引用类型
强引用
将对象赋给一个引用变量时即为强引用,有引用存在就不会被回收,是内存泄漏的主要原因。
软引用
SoftReference 类实现,在系统内存不足时被回收。
弱引用
WeakReference 类实现,在 GC 过程中一定被回收。
虚引用
PhantomReference 类实现,用于跟踪对象的垃圾回收状态。
引用类型引发的问题
ThreadLocal 内存泄漏问题:ThreadLocalMap的 key 为弱引用(发生 GC 会被回收),value 为强引用(GC 过程不会被回收),有可能造成 key 被 GC,value 没被 GC,ThreadLocalMap 中出现 key 为 null 的 Entry,产生内存泄漏。
解决方式:调用 set、get 和 remove 方法时,会清理掉 key 为 null 的记录,使用 ThreadLocal 的方法后手动使用 remove 方法。
JVM 调优参数
| 参数 | 含义 |
|---|---|
| -Xss | 栈大小 |
| -Xmx | 最大堆大小 |
| -Xms | 初始堆大小 |
| -Xmn | 新生代大小 |
| -XX:SurvivorRatio | Eden 区中两个 survivor 区大小比例 |
| -XX:NewRatio | 老年代 : 新生代大小比例 |
| -XX:OldSize | 老年代大小 |
| -XX:NewSize | 新生代大小 |

