标记算法
对象被判断为垃圾的标准
当一个对象没有任何引用指向它时,则判定为垃圾。
引用计数算法
- 通过判断对象被引用的数量判定对象是否可以回收
- 每个对象实例持有一个引用计数器,每当有新的引用指向它则计数器加1,当引用失效则计数器减1。
- 任何引用计数器值为0的对象都可以被标记为垃圾。
优点: 执行效率高,不影响程序正常逻辑执行。
缺点: 无法解决循环引用的问题。
可达性分析算法
通过一系列被称为"GC Root"的对象作为起始点,向下搜索,搜索过程中经过的路径称为"引用链"。
而当某个对象到"GC Root"之间没有任何引用链时,称此对象为不可达。
主流语言的主流实现中,均使用可达性分析算法作为GC过程中标记垃圾的算法。
JAVA中可作为GC Root的几种对象
- 虚拟机栈中引用的对象 (局部变量表)
- 本地方法栈中引用的对象
- 方法区中静态属性引用的对象
回收算法
标记-清除算法(Mark-Sweep)
- 标记: 通过可达性分析算法标记处内存中不可达对象
- 清除: 标记完成后,线性遍历内存回收被标记对象,同时清除标记以便下次执行垃圾回收。
缺点
- 效率: 标记和清除两个阶段效率都不高。
- 空间: 清除后会产生大量大连续的内存碎片,大量内存碎片可能导致当需要分配占用较大内存的对象时,无法找到可用连续内存区域而提前触发下一次垃圾回收。
复制算法(Copying)
通过将内存划分为同样大小的两块区域(对象面和空闲面),每次使用其中一块作为对象内存分配区域(对象面)。
当执行垃圾回收时,将对象面的所有存活对象复制到空闲面,同时直接清除对象面并转化为空闲面。
复制算法适用于对象存活率低的内存区域,当前主流的虚拟机实现都使用复制算法来回收年轻代区域的垃圾。
实现中会将堆内存划分为年轻代和老年代两块(2:1),同时年轻代又划分为一块Eden和两块Survivor(8:1:1)。
当回收时,将Eden和一块Survivor中存活对象复制到另一块Survivor,最后清理掉Eden和Survivor。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。这样,当Survivor没有足够内存空间存放上一次年轻代中存活对象时,这些对象将通过分配担保机制直接进入老年代。
标记-整理算法(Mark-Compact)
- 标记: 和标记清除算法的标记过程一样
- 整理: 移动所有存活对象,按内存地址依次排列,最后将末端内存地址之后的空间全部回收。
- 解决了标记清除算法产生的内存不连续问题
- 适用于对象存活率较高的场景
分代收集算法(Generational Collection)
- 垃圾回收算法的组合拳
- 按对象生命周期的不同划分出不同的内存区域,分别采用不同的内存回收策略
- 提高了JVM的垃圾回收效率
JDK7及之前版本堆内存划分
JDK8之后的堆内存划分
Minor GC
作用于年轻代,目的是快速回收掉生命周期较短的对象
对象如何晋升到老年代
- 在年轻代中经历过一定次数的垃圾回收依然存活的对象 (默认15次)
- Survivor区中没有足够内存存放MinorGC后依然存活的对象,直接存入老年代
- 新生成的大对象 (-XX:+PretenuerSizeThreshold)
垃圾回收相关的调优参数
- -XX:SurvivorRatio Eden和Survivor的比例 默认8:1:1
- -XX:NewRatio 新生代和老年代和比例 默认1:2
- -XX:MaxTenuringThreshold 对象从年轻代晋升到老年代需要经过GC的阈值
Major GC(Full GC)
标记-清除
标记-整理
执行Full GC会触发Minor GC,执行效率慢,但频率低。
触发Major GC的条件
- 老年代空间不足
- 永久代空间不足(JDK1.7及之前)
- 年轻代晋升到老年代对象平均大小大于老年代剩余内存空间
- System.gc()
新生代垃圾回收器
常见的垃圾回收器联系
Serial收集器(-XX:useSerialGC, 复制算法)
单线程收集,使用时必须暂停其他用户线程。
简单高效,client模式下默认的年轻代收集器
ParNew收集器(-XX:useParNewGC, 复制算法)
多线程收集,其他特性和Serial收集器一样。
server模式下默认的年轻代收集器
单核模式下效率不如Serial,适用于多核处理器环境。
调优参数 -XX:UseParallelGCThreads
Parallel Scavenge收集器(-XX:useParallelGC, 复制算法)
比起前面两种关注用户线程停顿时间的收集器,Parallel收集器更关注系统吞吐量。
是server模式下默认的年轻代收集器
调优参数 -XX:UseAdaptiveSizePolicy
老年代垃圾回收器
Serial Old收集器(-XX:UseSerialOldGC, 标记整理)
单线程,垃圾收集时需要暂停用户线程
简单高效,client模式下默认的老年代收集器
Parallel Old收集器(-XX:UseParallelOldGC, 标记-整理)
多线程,吞吐量优先。
JDK1.6之后提供,配合年轻代的Parallel Scavenge收集器,适合cpu敏感 计算密集的场景。
CMS收集器(-XX:UseConMarkSweep, 标记清除)
- 初始标记: 标记GC Root直接关联的对象。stop-the-world
- 并发标记: 并发追溯标记,程序不会停顿。
- 并发预处理: 查找执行并发标记阶段,从年轻代新晋升到老年代的对象。
- 重新标记: 标记在执行并发标记过程中,由于用户线程继续执行而产生的变化的对象。stop-the-world
- 并发清理: 清理垃圾对象,程序不会停顿。
G1收集器(-XX:UseG1GC, 复制+标记-整理算法)
并发和并行:可以利用多核cpu的特性,并行处理垃圾收集过程。且和用户线程并发执行,不会阻塞用户线程
分代收集:年轻代使用复制收集算法,老年代使用标记整理算法
空间整合:基于标记整理收集算法解决内存空间碎片化的问题
可预测的停顿:可建立期待的停顿点。
使用基于G1的垃圾收集器时,会将整个java堆内存划分为多个大小相同的region
年轻代和老年代在物理空间上不再是隔离的。
常见面试题
Q:Object中的finalize()方法和C++中的析构函数是否相同
- 析构函数的调用时间是确定的,而finalize是不确定的
- 当触发垃圾回收时,首先根据可达性分析算法确定对象是否可达,如不可达则第一次标记。之后判断是否覆盖finalize方法,如覆盖则将对象放置在F-Queue队列中。最后由虚拟机创建一个低优先级的finalize线程去执行finalize方法。
- 由于finalize线程的低优先级,方法执行随时可能会被终止。
- finalize方法给即将被垃圾收集器回收的对象最后一次逃脱清理的机会。
Q:强引用、弱引用、软引用、虚引用
强引用
- 最普遍的引用,例如Object obj = new Object();
- 当内存空间不足时,jvm宁可抛出OOM终止程序也不会回收强引用指向的对象
- 通过将引用置为null来弱化引用或者超出作用域范围,使其指向对象被回收
软引用
- 用来描述一些有用非必须的对象
- 当内存空间不足时可以回收软引用指向的对象
- JDK1.2提供了SoftReference类实现软引用
弱引用
- 描述有用非必须对象,强度低于软引用
- 一旦发生GC直接回收
- JDK1.2提供了WeakReference类实现弱引用
虚引用
- 不会决定对象生命周期,任何时间都可能被回收
- 主要起到哨兵的作用,用来跟踪对象被垃圾回收器回收的活动。
- 只能和引用队列ReferenceQueue一起使用