1.内存模型
堆(线程共享):堆是对象或数组分配内存的区域,也是垃圾回收的主要区域。现代VM通常采用分代收集算法,因此堆中还分为Eden区、Survivor From和Survivor To区
方法区(线程共享):方法区主要存放加载的类信息、静态变量、常量(运行时常量池和字符串常量池)、即时编译器编译的代码等。Hot Spot VM将分代收集算法应用到方法区中,使用Java堆中的永久代实现方法区,用于回收常量和类型的卸载,但收益较低,JDK1.7之后方法区从永久代变为元空间
虚拟机栈(线程私有):虚拟机栈存放Java方法栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等。方法的执行过程就是栈帧入栈出栈的过程
本地方法栈(线程私有):与虚拟机栈基本一致,区别是存放本地方法
程序计数器(线程私有):记录当前线程执行的字节码指令的地址,如果是Native方法则为空
2. 垃圾回收
垃圾定义:只要对象不再被使用了,就认为该对象是垃圾,对象所占的空间就可以被回收
常见垃圾:废弃常量(没有任何对象引用该常量)、无用类(该类的所有实例对象都被回收、该类的类加载器已被回收、该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类)
2.1 垃圾确认算法
- 引用计数法:通过对象间的引用计数判断对象是否可以回收。如果对象的引用计数为0,则说明没有其他对象与之关联,不太可能再被用到,面临垃圾回收。缺点是可能出现循环引用问题
- 可达性分析:通过寻找对象与GC Roots对象之间是否存在可达路径,判断对象是否可达。如果不存在可达路径,则说明对象不可达,经过两次不可达判断后,面临垃圾回收,解决循环引用问题
GC Roots:
- 虚拟机栈(栈帧的局部变量表)中引用的对象:若此栈帧为栈顶,证明活跃,对象也正被引用,即为GC Roots
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
2.2 垃圾回收算法
- 标记清除:先通过垃圾确认算法标记垃圾,然后直接清除。最简单,效率最高,但可能造成内存碎片,即使空间足够但也无法存储需要连续存储空间的大对象或者数组
- 标记复制:将内存区域一分为二,每次只使用一半。先标记,然后将存活的对象复制到另一半空间,清除垃圾。缺点是只能使用一半的内存,在存活对象很多时,复制的效率较低
- 标记整理:先标记垃圾,然后将存活的对象整理到内存区域的一端,清除端边界外的内存
- 分代收集:大部分JVM使用分代收集算法,核心是针对不同的内存区域采用不同的垃圾回收算法。在年轻代采用标记复制,老年代采用标记整理算法
2.3 垃圾回收器
- Serial GC:单线程回收,工作时暂停其他线程。避免线程切换,效率高。年轻代和老年代都有使用,老年代是Serial Old GC。通常用于客户端垃圾回收
- Parallel GC:多线程回收,工作时暂停其他线程。注重吞吐量,适用于后台不需要处理高频交互的场景
- Concurrent Mark Sweep(CMS) GC:多线程回收,注重低延迟。通过多个阶段标记,主要是为了减少STW时间,使用标记清除算法
特点
- 多线程并发:垃圾收集线程和用户线程(基本上)同时工作
- 低停顿,提高用户体验
步骤:标记-清除算法
- 初始标记:暂停所有其他工作线程,CMS线程记录下直接与GC Roots相连的对象。速度很快
- 并发标记:同时开启垃圾收集线程和用户线程,用闭包结构标记可达对象(由于用户线程可能会不断更新引用域,所以无法保证可达性分析的实时性。因此也会跟踪记录这些发生引用更新的地方)
- 重新标记:暂停所有其他工作线程,CMS线程修正并发标记期间引用更新对象的标记。停顿时间稍大于初始标记,远小于并发标记
- 并发清除:同时开启垃圾收集线程和用户线程,垃圾回收线程对未标记的区域做清扫
缺点
- 对CPU资源敏感
- 无法处理浮动垃圾
- 使用标记-清除算法,导致收集结束时会有大量空间碎片产生
- G1:将堆分为多个大小相等的区域,可以独立的进行垃圾回收,目的是提供可预测的停顿时间的同时保持高吞吐量
- ZGC:Oracle开发,JDK11启用,目的是停顿时间小于10毫秒,同时支持超大内存分配