Java 虚拟机在进行垃圾回收之前,需要判断识别哪些对象为垃圾对象,再针对不同年代的对象,运用不同的回收算法对垃圾对象进行回收;
判断垃圾对象
- 引用计数算法(Reference Counting)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。Java 虚拟机里面没有选用引用计数器算法来管理内存,其中最主要的原因是它很难解决对象之间循环引用的问题。
- 可达性分析算法(Reachability Analysis)
通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
在 Java 中,可作为 GC Roots 的对象包括下面几种:- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象;
对象引用
以上两种算法,判断对象是否为可回收的垃圾对象,都和对象引用有关;
在 JDK1.2 之后,将引用分为以下 4 类:
- 强引用(Strong Reference)
Object object = new Object 诸如此类为强引用;
垃圾回收器永远不会回收强引用的对象; - 软引用(Soft Reference)
软引用关联的是一些有用但非必须的对象;
在内存不足时,垃圾回收器会对这些对象进行二次回收; - 弱引用(Weak Reference)
弱引用是描述非必须对象;
无论内存是否足够,垃圾回收器都会回收该类对象; - 虚引用(Phantom Reference)
虚引用是最弱的一种引用;无法通过虚引用获得对象实例;
虚引用的唯一目的是能在这个对象被回收时收到一个系统通知;
垃圾收集算法
- 标记-清除算法(Mark-Sweep)
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的垃圾对象,在标记完成后统一回收所有被标记的对象;
标记-清除算法的不足是:在标记清除之后会产生大量不连续的内存碎片,内存碎片太多会导致程序需要分配大对象时无法找到足够的内存空间而提前触发一次垃圾回收; - 复制算法(Copying)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当使用的一块内存不足时,就将存活的对象复制到另外一块内存上,然后再把已使用多的内存空间一次清理掉。
优点是对整个半区的内存进行回收,不用考虑内存碎片问题,运行高效;
缺点是可用内存仅为一半,空间浪费; - 标记-压缩算法(Mark-Compact)
将存活对象都向一端移动,然后直接清理掉边界以外的内存。
当前商业虚拟机都采用分带收集算法(Generational Collection):根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的垃圾收集算法;
在新生代中,每次垃圾收集时都有大批垃圾对象,只有很少的存活对象,使用复制算法,只需要少量对象的复制成本即可完成收集。因为新生代中,90% 的对象都是短命的,所以并不需要按 1:1 的比例来划分内存,而是将内存分配为一块较大的 Eden 空间和两块较小的 Survivor 空间。当回收时,将 Eden 和 Survivor 中存活的对象复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,即每次新生代可用的内存空间为整个新生代内存容量的 90%,只有 10% 的空间会被浪费;
在老年代中因为对象存活率高,复制成本高,因此就必须使用标记-清理或者标记-压缩算法;
垃圾收集器
- Serial 收集器
Serial GC 是一个新生代的单线程收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束;因此可能会产生较长时间的停顿; - -XX:+UseSerialGC* 使用 Serial GC
- 新生代采用复制算法
- 老年代采用整理-压缩算法
- ParNew 收集器
ParNew GC 实际是 Serial GC 的多线程版本,它使用多线程进行垃圾收集,其余行为都与 Serial GC 完全一致,也是一个新生代收集器; - -XX:+UseParNewGC* 使用 ParNew GC
- -XX:+UseParallelGCThreads* 指定垃圾回收的线程数
- 新生代采用复制算法
- 老年代采用整理-压缩算法
- Parallel Scavenge 收集器
Parallel Scavenge GC 也是一个并行的多线程新生代收集器,但它的关注点是达到一个可控制的吞吐量(Throughput):cpu运行代码的时间与cpu总耗时的比值,即代码运行耗时/(代码运行耗时+垃圾回收耗时);
高吞吐量意味着可以高效率的利用 CPU 时间,尽快完成计算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge GC 提供两个参数用于精确控制吞吐量:- MaxGCPauseMillis:控制最大垃圾收集停顿时间,值是一个大于 0 的毫秒数,收集器尽可能保证内存收集耗时不超过该设定值;
- GCTimeRatio:控制垃圾收集时间占总时间的比率,相当于是吞吐量的倒数;值是 0~100 的整数,默认 99,即 1%(1/(1+99)) 的垃圾收集时间;
- Serial Old 收集器
Serial Old GC 是 Serial GC 的老年代版本,同样是单线程收集;- 新生代采用复制算法
- 老年代采用整理-压缩算法
它主要有两种用途: - 与新生代收集器搭配使用,如:搭配 Parallel Scavenge GC;
- 作为 CMS GC 的后备方案,在并发收集发生 Concurrent Mode Failure 时使用;
- Paralled Old 收集器
Paralled Old GC 是 Parallel Scavenge GC 的老年代版本;
由于 Serial Old GC 单线程方面的性能原因,ParallelScavengeGC+SerialOldGC 的组合,未必在整体应用中获得吞吐量最大化的效果;
在 JDK1.6 之后,ParallelScavengeGC+ParalledOldGC 组合,才是真正可以符合吞吐量及 CPU 资源敏感的场景; - CMS 收集器
CMS GC(Concurrent Mark Sweep)是采用“标记-清除”算法的老年代收集器,目标是获取最短停顿时间。
它的整体收集过程分为四个步骤;
初始标记(CMS initial mark)
初始标记需要停顿所有的工作线程(Stop The World),但仅仅只是标记一下 GC Roots 能直接关联的对象,速度很快;并发标记(CMS concurrent mark);
并发标记是对 GC Roots 进行跟踪(Tracing)的过程,可以和工作线程一同进行;重新标记(CMS remark);
重新标记是对并发标记因工作线程继续运行而产生变动的对象进行修正标记,所以也会 Stop The World,这个阶段的停顿时间会比初始标记耗时要长;并发清除;
一次性清楚标记的垃圾对象,可以和工作线程同时进行;由于耗时最长的并发标记和并发清除过程都可以和工作线程同时工作,所以整体来说,CMS GC 的内存回收过程是与工作线程并发执行的;
- G1 收集器
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,
其他收集器都是对整个新生代或者老年代进行回收,但 G1 收集器将 Java 堆内存划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离的了,它们都是一部分不连续的 Region 集合;
G1 跟踪各个 Region 里面的垃圾堆积的价值(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region,这种有计划的回收方式避免了在整个 Java 堆中进行全区域的垃圾收集,保证了 G1 在有限时间内可以获得尽可能高的回收效率;
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!