GC垃圾回收
GC垃圾回收
-----------------Java 堆内存
Java 堆是被所有线程共享的一块内存区域,所有对象实例和数组都在堆上进行内存分配。为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3 个区域。
新生代
新生代由 Eden 与 Survivor Space(From Space,To Space)构成,大小通过 - Xmn 参数指定,Eden 与 Survivor Space 的内存大小比例默认为 8:1,可以通过 - XX:SurvivorRatio 参数指定,比如新生代为 10M 时,Eden 分配 8M,S0 和 S1 各分配 1M。
对象主要分配在新生代的 Eden Space 和 From Space,少数情况下会直接分配在老年代。如果新生代的 Eden Space 和 From Space 的空间不足,则会发起一次 minor GC,在 GC 的过程中,会将 Eden Space 和 From Space 中的存活对象移动到 To Space,然后将 Eden Space 和 From Space 进行清理。如果在清理的过程中,To Space 无法足够来存储某个对象,就会将该对象移动到老年代中。在进行了 GC 之后,使用的便是 Eden space 和 To Space 了,下次 GC 时会将存活对象复制到 From Space,如此反复循环。当对象在 Survivor 区躲过一次 GC 的话,其对象年龄便会加 1,默认情况下,如果对象年龄达到 15 岁,就会移动到老年代中。
老年代
老年代的空间大小即 - Xmx 与 - Xmn 两个参数之差,用于存放经过几次 Minor GC 之后依旧存活的对象。当老年代的空间不足时,会触发 Major GC/Full GC,速度一般比 Minor GC 慢 10 倍以上。
永久代
在 JDK8 之前的 HotSpot 实现中,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32 位默认永久代的大小为 64M,64 位默认为 85M,可以通过参数 - XX:MaxPermSize 进行设置,一旦类的元数据超过了永久代大小,就会抛出 OOM 异常。
--------------- 如何确定某个对象是 “垃圾”?(引用计数法和可达性分析法。)
在 java 中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。特点是实现简单,效率较高,但是它无法解决循环引用的问题。
public class GCtest {
private Object instance = null;
private static final int _10M = 10 * 1 << 20;
// 一个对象占10M,方便在GC日志中看出是否被回收
private byte[] bigSize = new byte[_10M];
public static void main(String[] args) {
GCtest objA = new GCtest();
GCtest objB = new GCtest();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
为了解决这个问题,在 Java 中采取了 可达性分析法。该方法的基本思想是通过一系列的 “GC Roots” 对象作为起点进行搜索,判断在 “GC Roots” 和一个对象之间有没有可达路径。
以下对象可作为 GC Roots:
- 本地变量表中引用的对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- Native 方法引用的对象
当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。
在可达性分析法中,判定一个对象 objA 是否可回收,至少要经历两次标记过程:
1、如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。
2、如果对象 objA 重写了 finalize()方法,且还未执行过,那么 objA 会被插入到 F-Queue 队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize()方法。finalize()方法是对象逃脱死亡的最后机会,GC 会对队列中的对象进行第二次标记,如果 objA 在 finalize()方法中与引用链上的任何一个对象建立联系,那么在第二次标记时,objA 会被移出 “即将回收” 集合。
public class FinalizerTest {
public static FinalizerTest object;
public void isAlive() {
System.out.println("I'm alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("method finalize is running");
object = this;
}
public static void main(String[] args) throws Exception {
object = new FinalizerTest();
// 第一次执行,finalize方法会自救
object = null;
System.gc();
Thread.sleep(500);
if (object != null) {
object.isAlive();
} else {
System.out.println("I'm dead");
}
// 第二次执行,finalize方法已经执行过
object = null;
System.gc();
Thread.sleep(500);
if (object != null) {
object.isAlive();
} else {
System.out.println("I'm dead");
}
}
}
method finalize is running
I'm alive
I'm dead
从执行结果可以看出:
第一次发生 GC 时,finalize 方法的确执行了,并且在被回收之前成功逃脱;
第二次发生 GC 时,由于 finalize 方法只会被 JVM 调用一次,object 被回收。
当然了,在实际项目中应该尽量避免使用 finalize 方法。
--------------------------------- 收集算法
垃圾收集算法主要有:标记 - 清除、复制、标记 - 整理。
1、标记 - 清除算法 ----Mark-Sweep
最基础的垃圾回收算法,易实现,分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

算法缺点:效率问题,标记和清除过程效率都很低;
空间问题,收集之后会产生大量的内存碎片,不利于大对象的分配。
2、复制算法 -----------Copying
复制算法将可用内存划分成大小相等的两块 A 和 B,每次只使用其中一块,当 A 的内存用完了,就把存活的对象复制到 B,并清空 A 的内存,不仅提高了标记的效率,因为只需要标记存活的对象,同时也避免了内存碎片的问题,代价是可用内存缩小为原来的一半。

3、标记 - 整理算法 ----------Mark-Compact
为了解决 Copying 算法的缺陷,充分利用内存空间,提出了 Mark-Compact 算法。该算法标记阶段和 Mark-Sweep 一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

4. 分代收集算法 ------Generational Collection
分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照 1:1 的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。具体的我上上篇博客有写到。点这里
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是 Mark-Compact 算法。
注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储 class 类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
-------------------------- 垃圾收集器
Java 虚拟机规范并没有规定垃圾收集器应该如何实现,用户可以根据系统特点对各个区域所使用的收集器进行组合使用。
上图展示了 7 种不同分代的收集器,如果两两之间存在连线,说明可以组合使用。
1、Serial 收集器(串行 GC)
Serial 是一个采用单个线程并基于复制算法工作在新生代的收集器,进行垃圾收集时,必须暂停其他所有的工作线程。对于单 CPU 环境来说,Serial 由于没有线程交互的开销,可以很高效的进行垃圾收集动作,是 Client 模式下新生代默认的收集器。
2、ParNew 收集器(并行 GC)
ParNew 其实是 serial 的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与 Serial 一样。
3、Parallel Scavenge 收集器(并行回收 GC)
Parallel Scavenge 是一个采用多线程基于复制算法并工作在新生代的收集器,其关注点在于达到一个可控的吞吐量,经常被称为 “吞吐量优先” 的收集器。
吞吐量 = 用户代码运行时间 /(用户代码运行时间 + 垃圾收集时间)
Parallel Scavenge 提供了两个参数用于精确控制吞吐量:
1、-XX:MaxGCPauseMillis 设置垃圾收集的最大停顿时间
2、-XX:GCTimeRatio 设置吞吐量大小
4、Serial Old 收集器(串行 GC)
Serial Old 是一个采用单线程基于标记 - 整理算法并工作在老年代的收集器,是 Client 模式下老年代默认的收集器。
5、Parallel Old 收集器(并行 GC)
Parallel Old 是一个采用多线程基于标记 - 整理算法并工作在老年代的收集器。在注重吞吐量以及 CPU 资源敏感的场合,可以优先考虑 Parallel Scavenge 和 Parallel Old 的收集器组合。
6、CMS 收集器(并发 GC)
CMS(Concurrent Mark Sweep) 是一种以获取最短回收停顿时间为目标的收集器,工作在老年代,基于 “标记 - 清除” 算法实现,整个过程分为以下 4 步:
1、初始标记:这个过程只是标记以下 GC Roots 能够直接关联的对象,但是仍然会 Stop The World;
2、并发标记:进行 GC Roots Tracing 的过程,可以和用户线程一起工作。
3、重新标记:用于修正并发标记期间由于用户程序继续运行而导致标记产生变动的那部分记录,这个过程会暂停所有线程,但其停顿时间远比并发标记的时间短;
4、并发清理:可以和用户线程一起工作。
CMS 收集器的缺点:
1、对 CPU 资源比较敏感,在并发阶段,虽然不会导致用户线程停顿,但是会占用一部分线程资源,降低系统的总吞吐量。
2、无法处理浮动垃圾,在并发清理阶段,用户线程的运行依然会产生新的垃圾对象,这部分垃圾只能在下一次 GC 时收集。
3、CMS 是基于标记 - 清除算法实现的,意味着收集结束后会造成大量的内存碎片,可能导致出现老年代剩余空间很大,却无法找到足够大的连续空间分配当前对象,不得不提前触发一次 Full GC。
JDK1.5 实现中,当老年代空间使用率达到 68% 时,就会触发 CMS 收集器,如果应用中老年代增长不是太快,可以通过 - XX:CMSInitiatingOccupancyFraction 参数提高触发百分比,从而降低内存回收次数提高系统性能。
JDK1.6 实现中,触发 CMS 收集器的阈值已经提升到 92%,要是 CMS 运行期间预留的内存无法满足用户线程需要,会出现一次 "Concurrent Mode Failure" 失败,这时虚拟机会启动 Serial Old 收集器对老年代进行垃圾收集,当然,这样应用的停顿时间就更长了,所以这个阈值也不能设置的太高,如果导致了 "Concurrent Mode Failure" 失败,反而会降低性能,至于如何设置这个阈值,还得长时间的对老年代空间的使用情况进行监控。
7、G1 收集器
G1(Garbage First)是 JDK1.7 提供的一个工作在新生代和老年代的收集器,基于 “标记 - 整理” 算法实现,在收集结束后可以避免内存碎片问题。
G1 优点:
1、并行与并发:充分利用多 CPU 来缩短 Stop The World 的停顿时间;
2、分代收集:不需要其他收集配合就可以管理整个 Java 堆,采用不同的方式处理新建的对象、已经存活一段时间和经历过多次 GC 的对象获取更好的收集效果;
3、空间整合:与 CMS 的 "标记 - 清除" 算法不同,G1 在运行期间不会产生内存空间碎片,有利于应用的长时间运行,且分配大对象时,不会导致由于无法申请到足够大的连续内存而提前触发一次 Full GC;
4、停顿预测:G1 中可以建立可预测的停顿时间模型,能让使用者明确指定在 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
使用 G1 收集器时,Java 堆的内存布局与其他收集器有很大区别,整个 Java 堆会被划分为多个大小相等的独立区域 Region,新生代和老年代不再是物理隔离了,都是一部分 Region(不需要连续)的集合。G1 会跟踪各个 Region 的垃圾收集情况(回收空间大小和回收消耗的时间),维护一个优先列表,根据允许的收集时间,优先回收价值最大的 Region,避免在整个 Java 堆上进行全区域的垃圾回收,确保了 G1 收集器可以在有限的时间内尽可能收集更多的垃圾。
不过问题来了:使用 G1 收集器,一个对象分配在某个 Region 中,可以和 Java 堆上任意的对象有引用关系,那么如何判定一个对象是否存活,是否需要扫描整个 Java 堆?其实这个问题在之前收集器中也存在,如果回收新生代的对象时,不得不同时扫描老年代的话,会大大降低 Minor GC 的效率。
针对这种情况,虚拟机提供了一个解决方案:G1 收集器中 Region 之间的对象引用关系和其他收集器中新生代与老年代之间的对象引用关系被保存在 Remenbered Set 数据结构中,用来避免全堆扫描。G1 中每个 Region 都有一个对应的 Remenbered Set,当虚拟机发现程序对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于相同的 Region 中,如果不是,则通过 CardTable 把相关引用信息记录到被引用对象所属 Region 的 Remenbered Set 中。

浙公网安备 33010602011771号