Java与C++之间有一堵墙由内存动态分配和垃圾收集技术所围成的"高墙", 墙外面的人想进去, 墙里面的人却想出来。
一、 概述
GC需要完成的3件事情:
1. 哪些内存需要回收?
2. 什么时候回收?
3. 如何回收?
二、 对象已死吗
1. 引用计数算法
无法处理不用的相互引用的对象组
2. 可达性分析算法
一个对象到GC Roots没有任何引用链时, 此对象不可用
可作为GC Roots的对象:
(1) 虚拟机栈中引用的对象
(2) 方法区中类静态属性引用的对象
(3) 方法区中常量引用的对象
(4) 本地方法栈中JNI引用的对象
3. 再谈引用
4种引用强度:
强引用: 程序代码中普遍存在的, 只要强引用还存在, 永远不会回收;
软引用: 还有用但并非必须的对象, 在系统将要发生内存溢出异常前, 会把这些对象列进回收范围进行第二次回收, 如果这次回收还没有足够的内存才会抛出内存溢出异常;
弱引用: 非必需的对象, 只能生存到下一次垃圾收集发生之前;
虚引用: 在这个对象被收集器回收时收到一个系统通知
4. 生存还是死亡
宣告对象死亡, 至少要经历两次标记过程, 第一次筛选的条件是是否有必要执行finalize()方法, 如有必要, 这个对象将会放置到F-Queue队列中, 由Finalizer线程执行, 但不会保证等待它运行结束, 第二次筛选条件是重新与引用链上的任何一个对象建立关系, 则成功拯救, 否则将被回收, finalize()方法只会被调用一次
5. 回收方法区
永久代的垃圾收集主要回收两部分内容:
废弃常量: 没有任何地方引用常量池中的此常量, 此常量就会被系统清理出常量池
无用的类:
1. 该类所有的实例都已经被回收;
2. 加载该类的ClassLoader已经被回收;
3. 该类对应的java.lang.Class对象没有在任何地方被引用
满足上述3个条件仅仅说是"可以"进行回收, 通过-Xnoclassgc参数进行控制
在频繁自定义ClassLoader的场景中都需要虚拟机具备类卸载的功能, 以保证永久代不会溢出。
三、 垃圾收集算法
1. 标记-清除算法
算法分为"标记"和"清除"两个阶段: 首先标记出所有需要对手的对象, 在标记完成后统一回收所有被标记的对象。
不足点:
(1) 效率问题, 标记和清除两个过程的效率都不高
(2) 空间问题, 标记清除后会产生大量不连续的内存碎片
2. 复制算法
将可用内存按容量划分为大小相等时两块, 每次只使用其中的一块, 当这一块的内存用完了, 就将还活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。
不足点:
代价是将内存缩小为原来的一半
Eden: Survivor1: Survivor2 = 8: 1: 1
分配担保机制
3. 标记-整理算法
标记过程同上, 整理过程是让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存。
4. 分代收集算法
新生代使用复制算法, 老年代使用标记-清除或标记-整理
四、 HotSpot的算法实现
1. 枚举根节点
使用OopMap的数据结构在特定的位置记录引用的位置
2. 安全点
上述的特定位置指的就是安全点, 只有到达安全点才能GC暂停, 安全点的选定太少会让GC等待时间过长, 太多会频繁的暂停增大运行负荷, 所以安全点的选定以"是否具有让程序长时间执行的特征"为标准, 因为每条指令的执行时间都非常短暂, 只有在方法调用、 循环跳转、 异常跳转等指令序列复用的"长时间执行"的功能指令才会产生安全点。
另一点需要考虑的是, 如何在GC发生时让所有线程都"跑"到最近的安全点上停顿下来, 有下面两种方案:
1. 抢先式中断(基本已被pass);
2. 主动式中断: 当GC需要中断线程时, 不直接对线程操作, 设置一个标志, 各个线程执行时在安全点处主动轮询标志, 为真就将自己中断挂起, 另外再加上创建对象需要分配内存的地方
3. 安全区域
安全点的不足点在于只能保证程序执行的时候, 如果程序没有分配CPU时间(Sleep或Blocked状态), 线程无法响应JVM的中断请求, 安全区是引用关系不会发生变化的一端代码片段, 安全区可以看做是被扩展的安全点, 当线程进入安全区时标识自己, 当线程要离开安全区时检查系统是否已经完成了根节点枚举, 如果完成, 继续执行, 否则等待知道收到信号。
五、 垃圾收集器
收集算法是内存回收的方法论, 垃圾收集器则是内存回收的具体实现。
1. Serial收集器
单线程收集器, 采用复制算法, 它在进行工作时必须暂停其他所有的工作线程, 单CPU环境下有着最高的收集效率, 用于Client模式下的新生代。
2. ParNew收集器
Serial收集器的多线程版, Server模式下首选的新生代收集器, 能配合CMS收集器。
-XX:ParallelGCThreads: 限制垃圾收集的线程数
3. Parallel Scavenge收集器
采用复制算法的多线程收集器, 目标是达到一个可控制的吞吐量(运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)), 提供两个参数用于精确控制吞吐量:
-XX:MaxGCPauseMillis: 控制最大垃圾收集停顿时间, 越小GC发生越频繁;
-XX:GCTimeRatio: 设置吞吐量大小, 范围(0, 100), 垃圾收集时间占总时间比率, 19为1 / (1 + 19)的垃圾回收时间比
-XX:+UseAdaptiveSizePolicy: 自适应调节
4. Serial Old收集器
Serial收集器的老年代版本, 采用标记-整理算法
5. Parallel Old收集器
Parallel Scavenge收集器的老年代版本, 采用标记-整理算法
6. CMS收集器
以获取最短回收停顿时间为目标的收集器, 采用标记-清除算法, 过程分为4个步骤:
1. 初始标记: "Stop The World", 仅仅标记GC Roots能直接关联到的对象, 速度很快;
2. 并发标记: 进行GC Roots Tracing;
3. 重新标记: "Stop The World", 修正并发标记时用户线程产生变动的那一部分的标记记录, 停顿时间比初始标记时间稍长, 远比并发标记时间短;
4. 并发清除: 清除
3个明显的缺点:
1. 并发阶段时占用CPU, 数量为(CPU数量 + 3) / 4, 降低总吞吐量;
2. 并发清除过程中用户线程会产生新的浮动垃圾, 无法做到老年代完全被填满时再收集, 需要预留一部分空间, JDK1.6中阈值为92%, 通过调节-XX:CMSInitiatingOccupancyFraction设置阈值;
3. 基于标记清除算法, 会产生大量的空间碎片, -XX:useCMSCompactAtFullCollection开关参数, 顶不住时开启内存碎片的合并整理过程, -XX:CMSFullGCsBeforeCompaction, 调节实行多少次不压缩的GC后跟着压缩一次
7. G1收集器