JVM垃圾回收

简介: 一文搞定JVM垃圾回收

JVM垃圾回收
1、java的内存是如何划分的?
2、垃圾回收回收的是哪些区域?
3、如何确定哪些对象需要回收?
4、对象生存还是死亡?
5、什么时候回收?
6、如何收回?

1、java运行时数据区

回答第一个问题:java的内存是如何划分的?

java虚拟机在运行java程序的时候,会将内存划分为几个不同的数据区域,它们分别为:程序计数器、java栈、本地方法栈、方法区(有些也称为永久代)、java堆。共5个部分,组成java运行时数据区,如下图。
1

1.1、程序计数器

程序计数器是当前线程所执行字节码的行号指示器,反映当前线程在java虚拟机中的执行状态,java多线程轮流切换时的状态保证依靠程序计数器,因此,为了线程切换后能恢复到正确的执行位置,需要每个线程都有一个独立的程序计数器,也就是说,程序计数器的内存空间为“线程私有”的内存空间,其生命周期与线程的生命周期是一致的。

1.2、java栈

  • 与程序计数器一样,java栈也是线程私有的,生命周期也与线程的生命周期一致。
  • 虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用到执行完成对应着栈帧在虚拟机栈中从入栈到出栈的过程。
  • 局部变量表存储的是编译期可知的数据类型:8大基本类型+引用(指针或者句柄),其中long、double会占用2个局部变量空间,其余的数据类型只占用1个数据空间。
  • 这个区域定义了两种异常:StackOverflowError和OutofMemoryError

1.3、本地方法栈

  • 与java栈类似,所不同的是java栈为执行java方法服务,本地方法栈为执行native方法服务。
  • Sun Hotspot虚拟机直接将二者合二为一

1.4、方法区

  • 线程间共享方法区
  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据
  • 运行时常量池:存放编译期生成的各种字面量和类的符号引用等

1.5、java堆

  • 线程间共享java堆
  • 用于存储类的实例对象,为java垃圾回收管理的主要区域
  • 可以细分为新生代、老年代,或者进一步细分为Eden、From、To等空间
  • 可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

2、垃圾回收的区域

回答第二个问题:垃圾回收回收的那些区域?

  • 程序计数器、java栈、本地方法栈为线程私有的内存空间、生命周期随着线程的结束而释放,故垃圾回收不包括这三个区域。
  • 垃圾回收的主要区域为java对象存储的java堆,其次为存储类信息、常量等的方法区(在大量使用反射,动态代理,ClassLoader的场景下,要考虑永久代的回收)。

3、引用计数算法与根搜索算法

回答第三个问题:如何确定哪些对象需要回收?

3.1、引用计数算法

每个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是可以被回收得了。(在JDK1.2之前,使用的是该算法)

  • 优点:实现简单,判定高效,可以很好解决大部分场景的问题
  • 缺点:

    • 1、无法检测到引用环的存在,当两个对象A、B相互引用的时候,当其他所有的引用都消失之后,A和B还有一个相互引用,此时计数器各为1,而实际上这两个对象都已经没有额外的引用了,已经是垃圾了。但是却不会被回收;
    • 2、开销较大,频繁且大量的引用变化,带来大量的额外运算;主流的JVM都没有选用引用计数算法来管理内存;

3.2、根搜索算法(GCRootsTracing)

2
该算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。如上图(图片来自网络)

  • 优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;
  • 缺点:

    • 实现比较复杂;
    • 需要分析大量数据,消耗大量时间;
    • 可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化;分析过程需要GC停顿,即停顿所有Java执行线程

3.2.1、目前java 中可作为GC Root 的对象

  • 虚拟机栈中引用的对象(本地变量表)
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象(Native Object)

3.2.2、HotSpot虚拟机中对象可达性分析的实现

4、对象生存还是死亡

回答第四个问题:对象生存还是死亡?

经过3中引用计数算法或者根搜索算法已被标记为需要垃圾回收的对象,真的就只能被回收吗,答案是否定的,要真正宣告一个对象死亡,至少要经历两次标记过程。

4

  • 1、第一次标记:在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记;并且进行一次筛选:此对象是否必要执行finalize()方法;

    • (A)、没有必要执行,没有必要执行的情况:

      • (1)、对象没有覆盖finalize()方法;
      • (2)、finalize()方法已经被JVM调用过;
        这两种情况就可以认为对象已死,可以回收;
    • (B)、有必要执行
            对有必要执行finalize()方法的对象,被放入F-Queue队列中;稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;
  • 2、第二次标记:GC将对F-Queue队列中的对象进行第二次小规模标记;
    finalize()方法是对象逃脱死亡的最后一次机会:

    • (A)、如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;
    • (B)、如果对象没有,也可以认为对象已死,可以回收了;一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

4.1、finalize()方法

  • finalize()是Object类的一个方法,是Java刚诞生时为了使C/C++程序员容易接受它所做出的一个妥协,但不要当作类似C/C++的析构函数;
  • 因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用);
  • 如果需要"释放资源",可以定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用,如File相关类的close()方法;

finalize()方法主要有两种用途:

  • 1、充当"安全网":当显式的终止方法没有调用时,在finalize()方法中发现后发出警告;但要考虑是否值得付出这样的代价;如FileInputStream、FileOutputStream、Timer和Connection类中都有这种应用;
  • 2、与对象的本地对等体有关

      本地对等体:普通对象调用本地方法(JNI)委托的本地对象;

      本地对等体不会被GC回收;

      如果本地对等体不拥有关键资源,finalize()方法里可以回收它(如C/C++中malloc(),需要调用free());

      如果有关键资源,必须显式的终止方法;

5、Minor GC 和 Full GC

回答第五个问题:何时回收

5.1、对象分配原则

  • 1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 5.空间分配担保:每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC;如果小于,检查HandlePromotionFailure设置,如果true则只进行Minor GC,如果false则进行Full GC。

5.2、Minor GC 触发条件

当新生代(Eden区)空间不足时,发起一次 Minor GC

5.3、Full GC 触发条件

  • 调用 System.gc(),系统建议执行 Full GC,但是不必然执行
  • 当老年代空间不足时
  • 方法区空间不足时
  • 历次通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存时
  • 由Eden区、From Survior区向To Survior区复制时,对象大小大于ToSurvior区可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小时

6、java的垃圾回收器

回答第6个问题:如何回收垃圾对象

如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器。

6.1、典型的垃圾收集算法

6.1.1、Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。
3

标记-清除算法分为两个阶段:标记阶段和清除阶段。

  • 标记阶段的任务是标记出所有需要被回收的对象,
  • 清除阶段就是回收被标记的对象所占用的空间

优点:标记-清除算法实现起来比较容易
缺点:标记-清除算法有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

6.1.2、.Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。
4

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

优点:这种算法实现简单,运行高效且不容易产生内存碎片
缺点:

  • 1、对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半
  • 2、Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低

6.1.3、Mark-Compact(标记-整理)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。
5

标记阶段:该算法标记阶段和Mark-Sweep一样,

整理阶段:在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存

6.1.4、Generational Collection(分代收集)算法

4
分代收集算法理论来源于统计学,分代收集算法是目前大部分JVM的垃圾收集器采用的算法。

它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

比如新生代的对象在每次垃圾时都会有大量的对象死去,只有很少一部分存活,那就可以选择标记-复制算法。另外,在新生代中每次死亡对象约占98%,那么在标记-复制算法中就不需要按照1:1的比例来划分内存区域,而是将新生代细分为了一块较大的Eden和两块较小的Survivor区域,HotSpot中默认这两块区域的大小比例为8:2。每次新生代可用区域为Eden加上其中一块Survivor区域,共90%的内存空间,这样就只有10%的内存空间处在被闲置状态。在进行垃圾回收时,存活的对象被转移到原本处在“空闲的”Eden区域。如果某次垃圾回收后,存活对象所占空间远大于这10%的内存空间时,也就是Survivor空间不够用时,需要额外的空间来担保,通常是将这些对象转移到老年代。

对于老年代来说,大部分对象都处在存活状态。同时,如果一个大对象要在该区域进行分配,而内存空间又不足,那么在没有外部内存空间担保的情况下,就必须选用标记-清除或者标记-整理算法来进行垃圾回收了。

堆区之外还有一个代就是永久代(Permanet Generation),也称方法区,它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

6.1.5、分区算法

其主要就是将整个内存分为N个多小的独立空间,每个小空间都可以独立使用,这样细粒度的控制一次回收都少个小空间和那些个小空间,而不是对整个空间进行GC,从而提升性能,并减少GC的停顿时间。

6.2、GC算法优劣标准:评价一个垃圾收集GC算法的两个标准

  • 1、吞吐量(throughput)越高算法越好
    JVM在专门的线程[GC Threads]中执行GC 只要GC线程是活动的 就会和应用程序线程[Application Threads]争用当前可用CPU的时钟周期而吞吐量就是指应用程序线程占程序总用时的比例
  • 2、暂停时间(pause times)越短算法越好

    • 一个时间段内应用程序线程让GC线程执行而完全暂停。
    • 垃圾回收器的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程,只有这样系统才不会有新的垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性,也有益于更好地标记垃圾对象,因此垃圾回收时,都会产生应用程序的停顿

6.3、典型的垃圾收集器

4
7种常用的垃圾收集器,分为两块,上面为新生代收集器,下面是老年代收集器。
如果两个收集器之间存在连线,就说明它们可以搭配使用

6.3.1、Serial收集器

4
Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;
JDK1.3.1前是HotSpot新生代收集的唯一选择;

  • 特点

    • 针对新生代;
    • 采用复制算法;
    • 单线程收集;
    • 进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";
  • 应用场景

    • 依然是HotSpot在Client模式下默认的新生代收集器;
    • 也有优于其他收集器的地方:
    • 简单高效(与其他收集器的单线程相比);
    • 对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
    • 在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的
  • 设置参数:"-XX:+UseSerialGC":添加该参数来显式的使用串行垃圾收集器;

6.3.2、Serial Old收集器

4
1、Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
2、主要意义也是在于给Client模式下的虚拟机使用。
3、如果在Server模式下,那么它主要还有两大用途:
一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],
另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

  • 特点

    • 针对老年代;
    • 采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
    • 单线程收集;
  • 应用场景

    • 主要用于Client模式;
    • 而在Server模式有两大用途:(A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);(B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);

6.3.3、ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。
4

  • 特点:除了多线程外,其余的行为、特点(如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等;)和Serial收集器一样;
    +应用场景

    • 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;
    • 但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
  • 设置参数

    • "-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;
    • "-XX:+UseParNewGC":强制指定使用ParNew;    
    • "-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
  • 为什么只有ParNew能与CMS收集器配合
    CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作; CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作; 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

6.3.4、Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

  • 特点

    • 新生代收集器,复制算法,并行的多线程收集器
    • 吞吐量优先”收集器
    • 目标是达到一个可控制的吞吐量(Throughput)
  • 吞吐量:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
  • 应用场景

    • 高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;
    • 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;
  • 设置参数:Parallel Scavenge收集器提供两个参数用于精确控制吞吐量

    • (A)、"-XX:MaxGCPauseMillis"控制最大垃圾收集停顿时间,大于0的毫秒数;MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁;
    • (B)、"-XX:GCTimeRatio"设置垃圾收集时间占总时间的比率,0
    • (C)、"-XX:+UseAdptiveSizePolicy"开启这个参数后,就不用手工指定一些细节参数,如: 新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);  这是一种值得推荐的方式:(1)、只需设置好内存数据大小(如"-Xmx"设置最大堆); (2)、然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;(3)、那些具体细节参数的调节就由JVM自适应完成; 这也是Parallel Scavenge收集器与ParNew收集器一个重要区别;   

6.3.5、Parallel Old收集器

4
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;JDK1.6中才开始提供;

  • 特点

    • 针对老年代;
    • 采用"标记-整理"算法;
    • 多线程收集;
  • 应用场景

    • JDK1.6及之后用来代替老年代的Serial Old收集器;
    • 特别是在Server模式,多CPU的情况下;
    • 这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;
  • 设置参数:"-XX:+UseParallelOldGC":指定使用Parallel Old收集器;

6.3.6、CMS收集器

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;

是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

4

4_2_

  • 特点

    • 针对老年代;
    • 基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
    • 以获取最短回收停顿时间为目标;
    • 并发收集、低停顿;
    • 需要更多的内存(看后面的缺点);
  • 应用场景

    • 与用户交互较多的场景;
    • 希望系统停顿时间最短,注重服务的响应速度;
    • 以给用户带来较好的体验;
    • 如常见WEB、B/S系统的服务器上的应用;
  • 设置参数:"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;
  • CMS收集器运作过程:可以分为4个步骤

    • (A)、初始标记(CMS initial mark):仅标记一下GC Roots能直接关联到的对象;速度很快;但需要"Stop The World";
    • (B)、并发标记(CMS concurrent mark):进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;应用程序也在运行;并不能保证可以标记出所有的存活对象;
    • (C)、重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
      需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率;
    • (D)、并发清除(CMS concurrent sweep):回收所有的垃圾对象;整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
  • CMS收集器3个明显的缺点

    • (A)、对CPU资源非常敏感:并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更
    • (B)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败

      • (1)、浮动垃圾(Floating Garbage):在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;也要可以认为CMS所需要的空间比其他垃圾收集器大;"-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;JDK1.5默认值为68%;JDK1.6变为大约92%;  
    • (2)、"Concurrent Mode Failure"失败:如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。
  • (C)、产生大量内存碎片:由于CMS基于"标记-清除"算法,清除后不进行压缩操作;产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。解决方法:

    • (1)、"-XX:+UseCMSCompactAtFullCollection"使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;但合并整理过程无法并发,停顿时间会变长; 默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
    • (2)、"-XX:+CMSFullGCsBeforeCompaction"设置执行多少次不压缩的Full GC后,来一次压缩整理;为减少合并整理过程的停顿时间; 默认为0,也就是说每次都执行Full GC,不会进行压缩整理;

6.3.6、G1(Garbage-First)收集器

6.3.6.1、什么是G1
  • G1的第一篇paper(附录1)发表于2004年,在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS
  • G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
  • 在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
  • 在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。

screenshot

G1(Garbage-First)是JDK7-u4才推出商用的收集器;
4

  • 特点

    • (A)、并行与并发:能充分利用多CPU、多核环境下的硬件优势;
      可以并行来缩短"Stop The World"停顿时间;

也可以并发让垃圾收集与用户程序同时进行;

  • (B)、分代收集,收集范围包括新生代和老年代 :能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象;虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region);新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
  • (C)、结合多种垃圾收集算法,空间整合,不产生碎片,从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法;这是一种类似火车算法的实现;都不会产生内存碎片,有利于长时间运行;
  • (D)、可预测的停顿:低停顿的同时实现高吞吐量,G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
  • 应用场景:面向服务端应用,针对具有大内存、多处理器的机器;最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:

    • (1)、超过50%的Java堆被活动数据占用;
    • (2)、对象分配频率或年代提升频率变化很大;
    • (3)、GC停顿时间过长(长于0.5至1秒)。
  • 设置参数

    • "-XX:+UseG1GC":指定使用G1收集器;
    • "-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
    • "-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;
    • "-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
  • G1收集器运作过程:不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)

    • (A)、初始标记(Initial Marking):

      • a.仅标记一下GC Roots能直接关联到的对象;
      • b.且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
      • c.需要"Stop The World",但速度很快;
    • (B)、并发标记(Concurrent Marking):

      • a.进行GC Roots Tracing的过程;
      • b.刚才产生的集合中标记出存活对象;
      • c.耗时较长,但应用程序也在运行;
      • d.并不能保证可以标记出所有的存活对象;
    • (C)、最终标记(Final Marking):

      • a.为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
      • b.上一阶段对象的变化记录在线程的Remembered Set Log;
      • c.这里把Remembered Set Log合并到Remembered Set中;
      • d.需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
      • e.采用多线程并行执行来提升效率;
    • (D)、筛选回收(Live Data Counting and Evacuation):

      • a.首先排序各个Region的回收价值和成本;
      • b.然后根据用户期望的GC停顿时间来制定回收计划;
      • c.最后按计划回收一些价值高的Region中垃圾对象;
      • d.回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
      • e.可以并发进行,降低停顿时间,并增加吞吐量;

G1垃圾回收-Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

Young GC 阶段:

  • 阶段1:根扫描

    静态和本地对象被扫描
  • 阶段2:更新RS

    处理dirty card队列更新RS
  • 阶段3:处理RS

    检测从年轻代指向年老代的对象
  • 阶段4:对象拷贝

    拷贝存活的对象到survivor/old区域
  • 阶段5:处理引用队列

    软引用,弱引用,虚引用处理
    

screenshot

G1之Rset

GC时我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
screenshot

GC pause (young)日志

-XX:+PrintGCDetails日志示例

2018-03-14T17:25:20.920+0800: 37343.682: [GC pause (young), 0.13128020 secs]
   [Parallel Time:  85.1 ms]
      [GC Worker Start (ms):  37343683.5  37343683.6  37343683.6  37343683.7  37343683.7  37343683.7  37343683.7  37343683.8  37343683.8  37343683.8
       Avg: 37343683.7, Min: 37343683.5, Max: 37343683.8, Diff:   0.3]
      [Ext Root Scanning (ms):  31.4  31.1  26.3  28.5  32.1  34.6  30.8  31.5  30.1  32.1
       Avg:  30.9, Min:  26.3, Max:  34.6, Diff:   8.3]
      [Update RS (ms):  10.7  10.7  16.0  11.9  8.9  8.9  10.7  10.3  12.2  11.9
       Avg:  11.2, Min:   8.9, Max:  16.0, Diff:   7.1]
         [Processed Buffers : 2 19 14 16 5 6 2 24 14 3
          Sum: 105, Avg: 10, Min: 2, Max: 24, Diff: 22]
      [Scan RS (ms):  4.1  4.3  4.3  4.3  4.3  3.9  4.4  4.4  4.1  2.2
       Avg:   4.0, Min:   2.2, Max:   4.4, Diff:   2.1]
      [Object Copy (ms):  27.6  27.6  27.0  28.9  28.1  26.1  27.4  27.1  27.2  27.4
       Avg:  27.4, Min:  26.1, Max:  28.9, Diff:   2.7]
      [Termination (ms):  0.0  0.0  0.1  0.1  0.0  0.0  0.0  0.0  0.1  0.0
       Avg:   0.0, Min:   0.0, Max:   0.1, Diff:   0.0]
         [Termination Attempts : 8 3 5 6 6 5 11 1 4 7
          Sum: 56, Avg: 5, Min: 1, Max: 11, Diff: 10]
      [GC Worker End (ms):  37343757.8  37343757.9  37343758.1  37343758.1  37343757.6  37343758.1  37343757.7  37343757.7  37343757.9  37343757.5
       Avg: 37343757.8, Min: 37343757.5, Max: 37343758.1, Diff:   0.6]
      [GC Worker (ms):  74.3  74.4  74.4  74.5  73.9  74.4  74.0  73.9  74.1  73.7
       Avg:  74.1, Min:  73.7, Max:  74.5, Diff:   0.8]
      [GC Worker Other (ms):  11.4  11.5  11.5  11.6  11.8  11.5  11.8  11.9  11.5  11.5
       Avg:  11.6, Min:  11.4, Max:  11.9, Diff:   0.5]
   [Clear CT:   3.7 ms]
   [Other:  42.5 ms]
      [Choose CSet:   1.3 ms]
      [Ref Proc:   2.5 ms]
      [Ref Enq:   0.0 ms]
      [Free CSet:  37.2 ms]
   [Eden: 4573M(4573M)->0B(4578M) Survivors: 22M->16M Heap: 7190M(8192M)->2617M(8192M)]
 [Times: user=0.76 sys=0.00, real=0.13 secs]   

young GC 日志解析

  • 37343.682: [GC pause (young), 0.13128020 secs]
    这是最顶层的信息,它告诉我们这是一个从进程启动后37343.682秒开始的一个疏散暂停,在这时年轻代所有的区域被疏散,如Eden和Survivor。这次收集用了0.13128020秒完成的。
相关文章
|
16天前
|
算法 Java
JVM垃圾回收机制
JVM垃圾回收机制
15 0
|
1月前
|
Java 程序员
探讨JVM垃圾回收机制与内存泄漏
探讨JVM垃圾回收机制与内存泄漏
|
2月前
|
算法 Java 关系型数据库
掌握这3个技巧,你也可以秒懂JAVA性能调优和jvm垃圾回收
JVM 是一个虚拟化的操作系统,类似于 Linux 和 Window,只是他被架构在了操作系统上进行接收 class 文件并把 class 翻译成系统识别的机器码进行执行,即 JVM 为我们屏蔽了不同操作系统在底层硬件和操作指令的不同。
21 0
|
2月前
|
存储 缓存 算法
JVM的垃圾回收机制
JVM的垃圾回收机制
|
3月前
|
算法 Java
JVM GC和常见垃圾回收算法
JVM GC和常见垃圾回收算法
48 0
|
3月前
|
存储 算法 Java
理解JVM的内存模型和垃圾回收算法
理解JVM的内存模型和垃圾回收算法
43 2
|
3月前
|
算法 Java
jvm性能调优 - 15JVM的老年代垃圾回收器CMS的缺点
jvm性能调优 - 15JVM的老年代垃圾回收器CMS的缺点
52 0
|
3月前
|
消息中间件 算法 Java
jvm性能调优 - 14JVM的老年代垃圾回收器CMS原理
jvm性能调优 - 14JVM的老年代垃圾回收器CMS原理
45 0
|
3月前
|
存储 监控 算法
垃圾回收器、垃圾回收算法、空间分配担保、JVM调优、GC回收对象的过程
垃圾回收器、垃圾回收算法、空间分配担保、JVM调优、GC回收对象的过程
|
6天前
|
存储 前端开发 安全
JVM内部世界(内存划分,类加载,垃圾回收)(上)
JVM内部世界(内存划分,类加载,垃圾回收)
32 0