天天看点

JVM垃圾收集器与内存分配策略

Java会对内存进行自动分配与回收管理,使上层业务更加安全,方便地使用内存实现程序逻辑。

垃圾回收的目的是清除不再使用的对象,自动释放内存。

垃圾收集器与内存分配策略

  栈的内存随着方法的结束和线程结束自动回收,因此Java堆和方法区是垃圾收集器所关注的内存区域

  判断对象是否可以回收

    1、 引用计数法:给对象中添加一个引用计数器,当有一个地方被引用时加1,引用失效减1,计数器为0的就是可以回收的,但是会有互相引用的情况

    2、可达性分析法:从每个GC Roots出发, 判断对象到一系列称为GC Roots的对象有没有引用链相连

  GCROOTS:

    1.虚拟机栈(局部变量表)中引用的对象

    2.方法区类静态属性引用的对象

    3.方法区类中常量引用的对象

    4.本地方法栈中JNI引用的对象(native方法)

  即使在可达性分析法中不可达的对象,也至少要经历两次标记过程:

    第一次标记:可达性分析后无与GC Roots相连的引用链

    第二次标记:第一次标记后筛选(finalize()方法没有被JVM调用过)后放置在F-Queue队列中,仍无引用链和GC Roots相连则进行第二次标记

  方法区的收集:废弃常量和无用的类

    废弃的常量:如常量池中的字符串常量“abc”,没有String对象引用常量池的这个“abc”常量,那么abc就是废弃常量可以移除常量池

    无用的类:

      1、该类的实例都被回收  

      2、加载该类的ClassLoader已被回收

      3、该类的Class对象没有在任何地方被引用,也就是无法通过反射访问该类的方法

一、垃圾收集

 垃圾收集算法

  1、复制

  将内存划分为大小相等的两块,每次只使用其中一块,当其中的一块用完了将其上面存活的对象复制到另一块上面,然后把使用过的内存空间一次清理掉。

  新生代中的对象98%都是朝生夕死的,因此新生代按照8:1:1的比例分为了eden,survivor from 和survivor to空间,每次回收将eden和survivor from中存活的对象复制到survivor to中,不够的话再放到old中,然后将eden,survivor from一次清除掉。

  缺点:将可用内存缩小为了原来的一半,对象存活率较高时不适合使用。

  2、标记-清除

  首先标记需要回收的对象,在标记完成后统一回收  

  从每个GC Roots出发,依次标记有引用关系的对象,最后将没有标记的对象清除。

    问题 1、效率问题:标记和清除效率都不高,需要扫描两次

    问题 2、空间问题:清除后会产生大量内存碎片,过多的话会导致以后分配大对象如数组找不到一块连续的内存而提前触发一次GC 

  3、标记-整理

  为了解决标记-清除算法产生垃圾碎片的问题,又出现了标记-整理算法,该算法类似于计算机的磁盘整理。

  首先会从GC Roots出发标记存活的对象,然后将存活对象整理到内存空间的一端,形成连续的已使用空间,最后把已使用空间之外的部分全部清理掉,这样就不会产生空间碎片的问题。适用于老年代GC。

 垃圾收集策略

  分代收集

      新生代每次垃圾回收都有大量的对象死去少量存活,只需付出少量对象的复制成本即可完成收集。采用复制算法

    老年代对象存活率高,没有额外的空间做担保, 只能采用标记-清除或者标记-整理算法

 垃圾收集器

  新生代垃圾收集器:Serial、ParNew、ParallelScavenge、G1

  老年代垃圾收集器:CMS、Serial Old(MSC)、Parallel Old、G1

  垃圾收集器的发展,使用户线程的停顿时间在不断缩短,但是仍没办法完全消除,因此寻找更优秀的垃圾收集器仍在继续!Java 9之后,默认都采用G1进行垃圾回收。

  Serial收集器:单线程,采用复制算法,而且进行垃圾收集时,必须暂停JVM其他所有的工作进程(Stop The World),直到它收集结束。仍是Client模式下虚拟机新生代默认收集器

  ParNew收集器:Serial的多线程版本,采用复制算法,其他基本相同。是运行在server模式下的虚拟机首选的新生代收集器

  Parallel Scavenge收集器:与其他收集器关注点在缩短用户线程停顿时间不同,它关注点是达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行代码时间+垃圾收集时间)如:JVM总运行100分钟,

    垃圾收集1分钟,那吞吐量=99%,如果新生代采用了此收集器,那老年代只能使用Serial Old收集器

  Serial Old收集器:Serial收集器的老年代版本,同样单线程,采用标记-整理算法,存在意义是给Client客户端JVM使用

  Parallel Old收集器:Parallel Scavenge收集器的老年代版本,采用多线程和标记-整理算法

  CMS收集器(Concurrent Mark Sweep):

  一种以获取最短回收停顿时间为目标的收集器,基于标记- 清除算法实现,并且是并发执行的。

(只会收集老年代和永久代,1.8后改为元空间(需要设置CMSClassUnloadingEnabled)),不会收集年轻代

   回收步骤:

    1、初始标记  标记GCRoots直接关联的对象(Stop The World)

    2、并发标记  往下跟踪标记所有与GCRoots有引用链可达的对象( 可与用户线程同时工作,不会STW),就是进行GCROOTS Tracing的过程

    3、重新标记  修正并发标记期间因用户线程运行而导致的标记变动的一部分对象 (Stop The World)

    4、并发清除  清除未标记的对象  (可与用户线程同时工作,不会STW)

   缺点:

    1、虽然在并发阶段可与用户线程同时工作,但是会占用CPU资源,导致应用程序变慢,总吞吐量会降低

    2、无法处理浮动垃圾,即在并发清除阶段新产生的垃圾,只有留待下一次GC时再清理掉

    3、使用标记-清除算法,会有大量内存碎片产生。

      为了解决这个问题, CMS 可以通过配置 -XX:+UseCMSCompactAtFullCollection 参数,强制 JVM 在 FGC 完成后对老年代进行压缩 , 执行一次空间碎片整理 , 但是空间碎片整理阶段也会引发 STW。为了减少 STW 次数, CMS 还可以通过配置一 XX:+CMSFullGCsBeforeCompaction=n参数,在执行了n次FGC后, JVM再在老年 代执行 空间碎片整理。

    从整个过程来看,并发标记和并发清除的耗时最长,但是不需要停止用户线程,初始标记和重新标记耗时较短,但是需要停止用户线程。总体而言,整个过程造成的停顿较短,大部分的时间可以和用户线程一起工作。

  G1收集器(Garbage-First):

  JDK9默认的垃圾收集器,而且不再区分年轻代和老年代进行回收。

  G1逻辑上将整个Java堆划分为多个大小相等的独立区域(region),包括Eden、Survivor、Old、Humongous四种类型。其中Humongous是特殊的old类型,专门用于放置大型对象。这样的划分意味着不需要一个连续的内存空间管理对象。

  G1在进行回收的时候会在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先回收收益最大的Region。

  G1采用的是复制算法,有非常好的空间整合能力,不会产生大量的空间碎片。

  G1的一大优势在于可预测的停顿时间,能够尽可能快地在指定时间内完成垃圾回收任务。

  如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

    1、初始标记:标记GC ROOT直接关联的对象,需要STW

    2、并发标记:从GC Roots的直接关联对象开始遍历扫描所有有引用链相连的对象,扫描完成后还会重新处理并发标记过程中产生变动的对象。

    3、最终标记:短暂暂停用户线程,修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分标记记录,需要STW

    4、筛选回收:更新region的统计数据,对每个Region的回收价值和成本排序,根据用户设置的停顿时间制定回收计划。再把需要回收的Region中存活的对象复制到空的Region,同时清理旧的Region,需要STW

   特点:

    1、并行与并发:充分利用多CPU,多核环境的硬件优势,来缩短停顿时间,在GC期间可通过并发的方式让Java程序继续执行

    2、分代收集:采用不同的算法去收集刚创建的对象,存活了一段时间的对象和熬过多次GC的对象,以获取更好的收集效果

    3、空间整合:整体基于标记-整理算法,内部region之间采用复制算法,都不会产生内存空间碎片

    4、可预测的停顿时间:除了追求短时间停顿外,还建立了可预测停顿模型,使在M毫秒内,在垃圾收集上的时间不超过N毫秒

    

二、内存分配与回收策略

  Minor GC 新生代GC:一般比较频繁,回收速度也比较快

  Major GC/Full GC 老年代GC:调用System.gc() 强制执行GC为Full GC

  (Full GC停顿时间比Minor GC高几个量级,一般为50倍以上)

  内存分代模型:

  是一种内存管理模型。它将Java中的内存分为不同的区域,在GC时,不同的区域采取不同的算法,可以提高回收效率。

  内存分代模型将内存中的区域分成两部分:新生代、老年代

  两块区域的比例默认是 1:2 

  对象存活的时间较短,则属于新生代。存活时间较长,则属于老年代。每经过一次YGC,没被回收掉的对象年龄+1,年龄达到15岁之后,新生代的对象到达老年代

  对象优先在Eden区上分配,Eden区上没有足够的空间分配时,触发一次Minor GC(新生代GC),将存活的对象复制进Survivor to区,若Survivor to区没有足够的空间存放,则通过分配担保机制将对象转移到老年代中。

同时,经过一次Minor GC进入到survivor to区的对象,年龄计数器设为1,在Survivor from区的对象每经过一次Minor GC,年龄加1,当年龄增加到 -XX:MaxTenuringThreshold 设定的阀值(默认15)或者在Survivor区中有相

同年龄的所有对象大小总和大于Survivor区大小的一半,那么大于这个年龄的对象,将会被移动到老年代中。

  每进行Minor GC之前,在允许担保失败的情况下,JVM将查看老年代中最大可用连续空间是否大于历次minor GC晋升到老年代的对象的平均大小,如果大于,将进行一次Minor GC;如果minor GC后老年代空间不足,

则紧接着触发Full GC,如果小于,则直接触发Full GC。(新生代和老年代的比例默认是1:2)

  (HandlePromotionFailure设置是否允许担保失败(默认允许),如果不允许担保失败,那么每次Minor GC前JVM查看老年代中最大的连续空间是否大于新生代所有对象的大小总和,如果小于,则直接触发Full GC)

  大的对象(成员变量很多)可能直接进入老年代,避免在Eden区和两个Survivor区之间发生大量的内存复制。典型的大对象是那种很长的字符串对象或者数组。超过 -XX:PretenureSizeThreshold参数配置的大小的对象直接在老年代分配内存。

  

JVM垃圾收集器与内存分配策略
JVM垃圾收集器与内存分配策略

END.