02. 垃圾收集(GC)
垃圾收集(GC)
垃圾收集(Garbage Collection,GC
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
其中第一个问题很好回答,在
判断对象的生死

什么时候回收对象?当然是这个对象再也不会被用到的时候回收。所以要想解决 “什么时候回收
判断对象是否可用的算法
引用计数算法
- 算法描述:
- 给对象添加一个引用计数器;
- 每有一个地方引用它,计数器加
1 ; - 引用失效时,计数器减
1 ; - 计数器值为
0 的对象不再可用。
- 缺点:
- 很难解决循环引用的问题。即
objA.instance = objB; objB.instance = objA;
,objA 和objB 都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为0 ,将永远不能被判为不可用。
- 很难解决循环引用的问题。即
可达性分析算法(主流)
- 算法描述:
- 从 “GC Root” 对象作为起点开始向下搜索,走过的路径称为引用链(Reference Chain
) ; - 从 “GC Root” 开始,不可达的对象被判为不可用。
- 从 “GC Root” 对象作为起点开始向下搜索,走过的路径称为引用链(Reference Chain
Java 中可作为 “GC Root” 的对象:- 栈中(本地变量表中的
reference )- 虚拟机栈中,栈帧中的本地变量表引用的对象;
- 本地方法栈中,
JNI 引用的对象(native 方法) ;
- 方法区中
- 类的静态属性引用的对象;
- 常量引用的对象;
- 栈中(本地变量表中的
即便如此,一个对象也不是一旦被判为不可达,就立即死去的,宣告一个的死亡需要经过两次标记过程。
四种引用类型
- 强引用: 像
Object obj = new Object()
这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用: 用来引用还存在但非必须的对象。对于软引用对象,在
OOM 前,虚拟机会把这些对象列入回收范围中进行第二次回收,如果这次回收后,内存还是不够用,就OOM 。实现类:SoftReference
。 - 弱引用: 被弱引用引用的对象只能生存到下一次垃圾收集前,一旦发生垃圾收集,被弱引用所引用的对象就会被清掉。实现类:
WeakReference
。 - 虚引用: 幽灵引用,对对象没有半毛钱影响,甚至不能用来取得一个对象的实例。它唯一的用途就是:当被一个虚引用引用的对象被回收时,系统会收到这个对象被回收了的通知。实现类:
PhantomReference
。
宣告对象死亡的两次标记过程
- 当发现对象不可达后,该对象被第一次标记,并进行是否有必要执行
finalize()
方法的判断;- 不需要执行:对象没有覆盖
finalize()
方法,或者finalize()
方法已被执行过(finalize()
只被执行一次) ; - 需要执行:将该对象放置在一个队列中,稍后由一个虚拟机自动创建的低优先级线程执行。
- 不需要执行:对象没有覆盖
finalize()
方法是对象逃脱死亡的最后一次机会,不过虚拟机不保证等待finalize()
方法执行结束,也就是说,虚拟机只触发finalize()
方法的执行,如果这个方法要执行超久,那么虚拟机并不等待它执行结束,所以最好不要用这个方法。finalize()
方法能做的,try-finally 都能做,所以忘了这个方法吧!
方法区的回收
永久代的
- 废弃常量:例如一个字符串 “abc”,当没有任何引用指向 “abc” 时,它就是废弃常量了。
- 无用的类:同时满足以下
3 个条件的类。- 该类的所有实例已被回收,
Java 堆中不存在该类的任何实例; - 加载该类的
Classloader 已被回收; - 该类的
Class 对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法。
- 该类的所有实例已被回收,
垃圾收集算法

基础:标记- 清除算法
- 算法描述:
- 先标记出所有需要回收的对象(图中深色区域
) ; - 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……
) 。
- 先标记出所有需要回收的对象(图中深色区域
- 不足:
- 效率问题:标记和清理两个过程的效率都不高。
- 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次
GC 。

解决效率问题:复制算法
-
算法描述:
- 将可用内存分为大小相等的两块,每次只使用其中一块;
- 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
-
不足: 可用内存缩小为原来的一半,适合
GC 过后只有少量对象存活的新生代。 -
节省内存的方法:
- 新生代中的对象
98% 都是朝生夕死的,所以不需要按照1:1 的比例对内存进行划分; - 把内存划分为:
1 块比较大的Eden 区;2 块较小的Survivor 区;
- 每次使用
Eden 区和1 块Survivor 区; - 回收时,将以上
2 部分区域中的存活对象复制到另一块Survivor 区中,然后将以上两部分区域清空; JVM 参数设置:-XX:SurvivorRatio=8
表示Eden 区大小 / 1 块 Survivor 区大小 = 8
。
- 新生代中的对象

解决空间碎片问题:标记- 整理算法
- 算法描述:
- 标记方法与 “标记
- 清除算法” 一样; - 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
- 标记方法与 “标记
- 不足: 存在效率问题,适合老年代。

进化:分代收集算法
- 新生代:
GC 过后只有少量对象存活 —— 复制算法 - 老年代:
GC 过后对象存活率高 —— 标记- 整理算法
HotSpot 中GC 算法的实现
通过前两小节对于判断对象生死和垃圾收集算法的介绍,我们已经对虚拟机是进行

通过之前的分析,
- 找到死掉的对象;
- 把它清了。
想要找到死掉的对象,我们就要进行可达性分析,也就是从
也就是说,进行可达性分析的第一步,就是要枚举
在
因此,
此外,在进行枚举根节点的这个操作时,为了保证准确性,我们需要在一段时间内 “冻结” 整个应用,即
我们让所有线程跑到最近的安全点再停顿下来进行
主要有以下两种方式:
- 抢先式中断:
- 先中断所有线程;
- 发现有线程没中断在安全点,恢复它,让它跑到安全点。
- 主动式中断:
( 主要使用) - 设置一个中断标记;
- 每个线程到达安全点时,检查这个中断标记,选择是否中断自己。
除此安全点之外,还有一个叫做 “安全区域” 的东西,一个一直在执行的线程可以自己 “走” 到安全点去,可是一个处于
安全区域是指在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始
当线程执行到安全区域时,它会把自己标识为
本小节我们主要讲述
7 个垃圾收集器
垃圾收集器就是内存回收操作的具体实现,

Serial / ParNew 搭配Serial Old 收集器

Parallel 搭配Parallel Scavenge 收集器
首先,这俩货肯定是要搭配使用的,不仅仅如此,它俩还贼特别,它们的关注点与其他收集器不同,其他收集器关注于尽可能缩短垃圾收集时用户线程的停顿时间,而
吞吐量
= 运行用户代码时间/ ( 运行用户代码时间+ 垃圾收集时间)
因此,
可调节的虚拟机参数:
-XX:MaxGCPauseMillis
:最大GC 停顿的秒数;-XX:GCTimeRatio
:吞吐量大小,一个0 ~ 100 的数,最大 GC 时间占总时间的比率 = 1 / (GCTimeRatio + 1)
;-XX:+UseAdaptiveSizePolicy
:一个开关参数,打开后就无需手工指定-Xmn
,-XX:SurvivorRatio
等参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,自行调整。
CMS 收集器


参数设置:
-XX:+UseCMSCompactAtFullCollection
:在CMS 要进行Full GC 时进行内存碎片整理(默认开启)-XX:CMSFullGCsBeforeCompaction
:在多少次Full GC 后进行一次空间整理(默认是0 ,即每一次Full GC 后都进行一次空间整理)
关于
CMS 使用 标记- 清除 算法的一点思考:之前对于
CMS 为什么要采用 标记- 清除 算法十分的不理解,既然已经有了看起来更高级的 标记- 整理 算法,那CMS 为什么不用呢?最近想了想,感觉可能是这个原因,不过也不是很确定,只是个人的一种猜测。标记
- 整理 会将所有存活对象向一端移动,然后直接清理掉边界以外的内存。这就意味着需要一个指针来维护这个分隔存活对象和无用空间的点,而我们知道CMS 是并发清理的,虽然我们启动了多个线程进行垃圾回收,不过如果使用 标记- 整理 算法,为了保证线程安全,在整理时要对那个分隔指针加锁,保证同一时刻只有一个线程能修改它,加锁的这一过程相当于将并行的清理过程变成了串行的,也就失去了并行清理的意义了。所以,
CMS 采用了 标记- 清除 算法。
G1 收集器


GC 日志解读
