JAVA GC 原理详解

大多数情况下我们对GC的了解都只是浅层含义上的,下面我们来详细讲解下内部的一些实现原理。
讲解GC之前,我们得先了解下JVM的内存结构,才能让我们理解GC导致是干嘛的。

一.JVM 内存结构

JVM内存结构由6个部分组成,分别如下

1. 程序计数器(Program Conuter Register)

一块较小的内存空间,它是当前线程执行字节码的行号指示器,字节码解释工作器就是通过改变这个计数器的值来选取下一条需要执行的指令。它是线程私有的内存,也是唯一一个没有OOM异常的区域。

2. Java虚拟机栈区(Java Virtual Machine Stacks)

也就是通常所说的栈区,它描述的是Java方法执行的内存模型,每个方法被执行的时候都创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法被调用到完成,相当于一个栈帧在虚拟机栈中从入栈到出栈的过程。此区域也是线程私有的内存,可能抛出两种异常:如果线程请求的栈深度大于虚拟机允许的深度将抛出StackOverflowError;如果虚拟机栈可以动态的扩展,扩展到无法动态的申请到足够的内存时会抛出OOM异常。

3. 本地方法栈(Native Method Stacks)

本地方法栈与虚拟机栈发挥的作用非常相似,区别就是虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。

4. 堆区(Heap)

所有对象实例和数组都在堆区上分配,堆区是GC主要管理的区域。堆区还可以细分为新生代、老年代,新生代还分为一个Eden区和两个Survivor区。此块内存为所有线程共享区域,当堆中没有足够内存完成实例分配时会抛出OOM异常。

5. 方法区(Method Area)

方法区也是所有线程共享区,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。GC在这个区域很少出现,这个区域内存回收的目标主要是对常量池的回收和类型的卸载,回收的内存比较少,所以也有称这个区域为永久代(Permanent Generation)的。当方法区无法满足内存分配时抛出OOM异常。


二.GC 原理

针对GC的原理机制,主要搞清楚下面三个问题。

  1. 什么时候回收?
  2. 哪些需要回收?
  3. 怎么回收?

1. 什么时候回收?

1.对象优先分配到新生代的Eden区,当不够空间的时候进行一次Minor GC,清理频率很高。
2.Full GC发生在老年代,当不够空间的时候进行一次Full GC,伴随着也会进行一次Minor GC。
3.进行Minor GC时,会判断每次变成晋升到老年代的对象平均值是否大于老年代剩余空间,如果大于,则进行一次Full GC,如果小于就会去判断HandlePromotionFailure设置是否允许担保失败,如果允许,则进行Minor GC,不允许则改为Full GC。

上面提到GC主要管理的是堆区,堆区主要分为`新生代`和`老年代`
* 【新生代】分为一个Eden和两个Survivor区。新new的对象都放在这里,很快消亡。
* 【老年代】新new的大对象直接丢到这里(为了避免在Eden区和两个Survivor区发生大量的内存拷贝),其余就是在新生代多次回收没被干掉过来变成老家伙的对象了。

2. 哪些需要回收?

当调用了finalize()方法后,则需要进行清理,具体场景如上。

* 什么是finalize()方法?

每次进行GC之前系统都会调用一次finalize()方法,用以清理所有活动并且释放资源。

* 什么时候调用finalize()方法?

1.GC调用之前,例如运行System.gc();(调用System.gc()只是建议JVM去执行,是否执行还得JVM去判断)
2.程序退出时,每个对象都会调用finalzie
3.显式调用finalize

3. 怎么回收?

JVM会根据不同的收集器使用不同的算法组合来达到回收的效果

### 垃圾收集算法

* mark-sweep(标记-清除)- 标记所有需要回收的对象,在标记完成后统一回收这些对象。

缺点:1.标记和清除两个过程的效率都不高。2.标记清除会产生大量不连续的内存碎片。

* copying(复制)- 主要用来回收新生代

新生代分为一个Eden区、两个Survivor区(Survivor0、Survivor1),Eden和Survivor默认8:1。回收时先把Eden存活对象复制到Survivor0区,清空Eden区,当Survivor0区满了以后,把Eden和Survivor0区的存活对象复制到Survivor1区,清空Eden区和Survivor0区,之后交换Survivor0和Survivor1区,保持Survivor1区是空的,如此往复

* mark-compact(标记-整理)

主要用来回收老年代。标记需要回收的对象,将其他存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

* generational collection(分代)

目前常用的收集算法,区分新生代和老年代做不同的算法收集。针对新生代,只有少量存活,选用复制算法。针对老年代,存活率高,没有额外的空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

--------

### 垃圾收集器

* Serial 、Serial Old 收集器 [-XX:+UseSerialGC,使用 Serial + Serial Old 组合回收]

适合单处理器系统,并且在进行垃圾回收的时候会暂停其他所有的工作线程(stop the world),对于多处理器的系统来说是灾难

* ParNew 收集器 [-XX:UseParallelGC,使用 Parallel Scavenge + Serial Old 组合回收]

serial 收集器的多线程版本

* Parallel Scavenge 、Parallel Old 收集器 [-XX:GCTimeRatio,-XX:MaxGCPauseMillis]

通过两个参数GCTimeRatio和MaxGCPauseMillis,尽可能缩短垃圾收集器用户线程的停顿时间,从而达到一个可控制的吞吐量。

* CMS (Concurrent Mark Sweep)收集器 [-XX:UseConcMarkSweepGC,使用 ParNew + CMS + Serial Old 组合回收]

以获取最短回收停顿时间为目标的收集器。

步骤

1.初始标记(CMS initial mark),标记GC Roots能直接关联到的对象,速度很快
2.并发标记(CMS concurrent mark),进行GC Roots Tracing
3.重新标记(CMS remark),重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致变动的标记记录,比并发标记时间短
4.并发清除(CMS concurrent sweep)并行删除

缺点
1. 比较消耗CPU资源,默认启动回收线程数是(CPU数量+3)/4。
2. CMS收集器无法处理浮动垃圾(CMS清理阶段用户线程还运行着,伴随生成的新垃圾只能在下次GC再清理掉),可能出现“Concurrent Mode Failure”而导致另一次Full GC的产生。可以通过-XX:CMSInitiatingOccupancyFraction来调节。
3. 标记-清除会导致内存碎片而导致触发Full GC(切换到Serial Old收集器收集老年代)。可以通过-XX:UseCMSCompactAtFullCollection、-XX:CMSFullGCsBeforeCompaction来调节。

* G1(Garbage-First) 收集器 [-XX:+UseG1GC]

Java堆的内存分布和其他收集器有很大不同,它将整个Java堆划分为多个大小相等的独立区域`Region`,老年代和新生代不再物理隔离,而是一部分`Region`的集合。G1会跟踪各个Region的垃圾堆积价值大小,后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的`Region`。

特点
1.并发和并行
2.分代收集
3.空间整合
4.可预测的停顿

步骤
1.初始标记(Initial Marking)
2.并发标记(Concurrent Marking)
3.最终标记(Final Marking)
4.筛选回收(Live Data Counting and Evacuation)
posted @ 2017-05-14 16:10  进击的程序猿-L  阅读(319)  评论(0编辑  收藏  举报