如何判断对象存活?可达性?引用计数?
在介绍垃圾回收之前,首先需要解决的问题就是:如何判断对象存活?
因为垃圾回收,即内存回收,必然是对不再使用对象的内存区域进行回收,主要有两种存活方式:引用计数与可达性分析。
引用计数
在对象中添加一个引用计数器,一旦出现一个新的reference引用了它,则加一,不引用,则减一。
因此可以将引用计数为0的对象视为不再被使用。但这种方法有个问题,即相互引用。
这种方法很难解决对象之间相互循环引用的问题。比如下面的代码,objA 和 objB 互相引用,这种情况
下,引用计数器的值都是1,不会被垃圾回收。
可达性分析*
通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。
(DFS或BFS?)
那么谁能作为可达性分析的GC Root呢?
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
2. 本地方法栈中JNI(Native方法)引用的对象
3. 方法区中类静态属性引用的对象
4. 方法区中常量引用的对象
5. 所有被同步锁(synchronized关键字)持有的对象。
强引用、弱引用、软引用、虚引用
强引用:垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错
误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管
当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不
一定会很快发现那些只具有弱引用的对象。
虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一
样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。
Minor GC与Full GC
Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速
度一般也会比较快。
Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比
Minor GC 慢很多。
垃圾回收算法
标记清除法:先遍历标记,统一删除;
复制清除法,即半区复制法;
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存
使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。 特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空
标记整理法:标记之后不删除,直接往右边移动,清理掉边界之外的内存;
分类收集法:根据所用内存区(新生代、老年代)来进行方法的选择。
垃圾回收器的优劣
收集器 | 串行、并 行or并发 |
新生 代/老 年代 |
算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速 度优先 |
单CPU环境下的Client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速 度优先 |
多CPU环境时在Server模式 下与CMS配合 |
Parallel Scavenge |
并行 | 新生代 | 复制算法 | 吞吐量 优先 |
在后台运算而不需要太多交 互的任务 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速 度优先 |
单CPU环境下的Client模 式、CMS的后备预案 |
Parallel Old |
并行 | 老年代 | 标记-整理 | 吞吐量 优先 | 在后台运算而不需要太多交 互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速 度优先 |
集中在互联网站或B/S系统 服务端上的Java应用 |
G1 | 并发 | both | 标记-整理 +复制算法 |
响应速 度优先 |
面向服务端应用,将来替换 CMS |
CMS 收集器
Concurrent Mark Sweep 并发标记清除,目的是获取最短应用停顿时间。第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程基本上同时工作。在并发标记和并发清除阶段,虽然用户线程没有被暂停,但是由于垃圾收集器线程占用了一部分系统资源,应用程序的吞吐量会降低。
CMS 垃圾回收基于标记清除算法实现,整个过程分为四个步骤:
初始标记: stw暂停所有的其他线程,记录直接与 gc root 直接相连的对象,速度很快 。
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程。
重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会stw,停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
并发清除:清除死亡对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。