• 深入理解Java 虚拟机 之垃圾收集器与内存分配策略


    一、概述

    1. 哪些内存需要回收?

    2. 什么时候回收?

    3. 如何回收?

    我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

    二、对象“存活还是死去”

    垃圾回收之前首先得确定对象是“存活”还是“死亡”

    1. 引用计数算法

    给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效时计数器值就减1。任何时候计数器为0的对象就是不可再被使用。

    2. 可达性分析算法

    通过一系列的称为“GC Roots” 的对象作为起始点,从这些点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots 没有任何引用链相连时,则证明对象不可用。

    3. 再谈引用

    上面两张判断对象是否存活都和“引用”有关。

    引用概念:强引用,软引用,弱引用,虚引用。四种强度依次降低

    (1). 强引用:Object object = new Object(); 只要引用还存在就永远不会回收掉被引用的对象

    (2). 软引用:还有用,但非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围进行第二次回收

    (3). 若引用:也是描述非必须对象,但它比若引用更弱一些,它只能生存到下一次垃圾回收之前

    (4).虚引用:最弱的一种引用关系,设置的目的是为了能再这个对象被收集器回收的时候收到一个系统通知。

    4. 生存还是死亡?finalize() 方法

    可达性分析算法中不可达的对象----》“缓刑”阶段----》第一次标记:是否执行finalize() 方法----》有必要执行

    5. 回收方法区

    判断是否为无用的类需要满足三个条件

    三、垃圾收集算法

    1. 标记-清除算法:两个阶段,先标记需求回收的对对象,再清除。缺点:效率不高,产生不连续的内存碎片,不利于创建大内存的对象

    2. 复制算法:内存均分为两个区域,只使用一块区域,当这块内存用完,将存活的对象复制到另一块中,并清理之前那块内存。缺点:可使用内存缩小了一半

    3. 标记-整理算法:过程和标记-清除一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象移向一端,直接清理端边界以外的内存

    4. 分代收集算法:当前商业虚拟机都采用这种算法。将Java 堆分为新生代和老年代,根据各代的特点采用适当的收集算法。

    四、HotSpot 算法实现

    上面都是从理论上介绍对象存活判定算法和垃圾收集算法,而HotSpot 虚拟机上实现这些算法时,必须对算法有严格的考量,才能保证虚拟机的高效运行

    1. 枚举根节点:使用OopMap 的数据结构来得知哪些地方存放着对象引用,在GC 扫描时就能直接得知信息

    缺点:①检查时间长②GC 停顿(当到达安全点时,GC 停顿,所有的Java 执行线程都将停止)

    2. 安全点:程序并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停

    (1). 安全点的选定:“是否具有让程序长时间执行的特征”为标准进行选定

    (2). 如何在GC 时让所有线程都“跑到”最近安全点:①抢先式中断(不采用了)②主动式中断:GC 需要中断线程,不直接对线程进行操作,设置标志,各线程轮询取标志,中断标志位true 就自己中断挂起。

    3. 安全区域(Safe Region)

    解决程序不执行(没有分配cpu 时间,线程处于Sleep 状态或者Blocked 状态)时,线程无法响应JVM 请求,JVM 也不太可能等待线程重新被分配cpu 时间,这就需要安全区域来解决。

    安全区域:指一段代码片段中引用片段不会发生变化。在这个区域中的任意地方开始GC 都是安全的。也可以把它看做是SafePoint 的扩展

    五、垃圾收集器

    收集算法是内存回收的理论,而立即收集器是内存回收的具体实现。Java 虚拟机规范中对垃圾收集器应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别。下面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

    垃圾收集器的上下文语境中的并行和并发:

    并行(Parallel):指多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待。

    并发(Concurrent):指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集器程序运行于另一个CPU之上。

    我们主要讨论Hopspot 中的以下垃圾收集器:

    1. Serial 收集器(串行GC)

    Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,它不仅只会使用一个CPU或者一条收集线程去完成垃圾收集作,而且必须暂停其他所有的工作线程(用户线程),直到它收集完成。下图是Serial/Serial Old收集器运行示意图(表示Serial和Serial Old搭配使用)

    是Jvm client 模式下默认的新生代收集器。对于限定单个CPU 的环境来说,简单高效,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,因此是运行在Client 模式下的虚拟机的不错选择(比如桌面应用场景)。

     2. ParNew (并行GC)收集器

    ParNew收集器其实就是serial收集器的多线程版本,使用复制算法。除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

    它是运行在Server 模式下的虚拟机中首选的新生代收集器,其中一个与性能无关的原因是除了Serial 收集器之外,只有它能与CMS 配合工作。单cpu 情况下Serial 效果要更好。可通过-XX:parallelGCThreads参数来限制收集器线程数。

    3. Parallel Scavenge (并行回收GC)收集器

    Parallel Scavenge 属于新生代收集器,使用复制算法,并行的多线程收集器。parallel Scavenge 收集器的特点是达到一个可控制的吞吐量(其他CMS 等收集器模目标是减少GC 时用户线程的停顿时间)。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。高吞吐量可以高效的利用cpu 时间,尽快的完成计算任务。

    提供两个参数:-XX:MaxGCPauseMillis 最大垃圾收集停顿时间 和  -XX:GCTimeRatio:设置吞吐量大小

    另外还有一个参数:-XX:UseAdaptiveSizePolicy:自适应调节策略(GC Ergonomics),它是一个开关参数,当它打开时就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)今生老年代对象大小(-XX:PretenureSizeThreshold),会根据当前系统的运行情况手机监控信息,动态调整停顿时间和吞吐量大小

    4. Serial Old(串行GC) 收集器

    是Serial 的老年代版本,同样是一个单线程收集器,使用“标记-整理算法”。主要使用在Client模式下的虚拟机。

    如果在Service 模式下使用:1.一种是在JDK1.5以及之前的版本中与Parallel Scavenge 收集器搭配使用,因为那时还没有Parallel  Old 老年代收集器搭配;2.另一种就是作为CMS 收集器的后备预案,在并发收集发生Concurrent Model Failure 时使用。

    5. Parallel Old(并行GC)收集器

    Parallel Old是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法,JDK1.6才提供。

    Parallel Scavenge 新生代收集器无法和老年代收集器与之完美结合,只能采用Serial Old老年代收集器,但是老年代Serial Old 收集器在服务器端应用性能上的“拖累”,其吞吐量反而不一定有PreNew+CMS组合。知道出现Parallel Scavenge + Parallel Old

    6. CMS (并行GC)收集器

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:①初识标记②并发标记③重新标记④并发清楚,其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程(Stop The World)。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。

    由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

    CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:

    (1). CMS收集器对CPU资源非常敏感。在并发(并发标记、并发清除)阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。收集器线程所占用的CPU数量为:(CPU+3)/4=0.25+3/(4*CPU)。因此这时垃圾收集器始终不会占用少于25%的CPU,因此当进行并发阶段时,虽然用户线程可以跑,但是很缓慢,特别是双核CPU的时候,已经占用了5/8的CPU,吞吐量会很低。为了解决这种情况,产生了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用抢占方式来模拟多任务机制,就是在并发(并发标记、并发清除)阶段,让GC线程、用户线程交替执行,尽量减少GC线程独占CPU,这样垃圾收集过程更长,但是对用户程序影响小一些。实际上i-CMS效果很一般,目前已经被声明为“deprecated”。
    (2). CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full  GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以降低内存回收次数提高性能。JDK1.6中,CMS收集器的启动阈值已经提升到92%。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
    (3). 最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full  GC之后增加一个内存碎片的合并整理过程,但是内存整理过程是无法并发的,因此解决了空间碎片问题,却使停顿时间变长。还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full  GC之后,跟着来一次碎片整理过程(默认值是0,表示每次进入Full GC时都进行碎片整理)。

     7. G1收集器(优先回收价值最大的Region,内存“化整为零”的思路)

    G1(Garbage-First)是JDK1.7提供的一个新的面向服务端应用的垃圾收集器,其目标就是替换掉JDK1.5发布的CMS收集器。其优点有:

    (1). 并发与并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短停顿(Stop The World)时间。

    (2). 分代收集:G1不需要与其他收集器配合就能独立管理整个GC堆,但他能够采用不同方式去处理新建对象和已经存活了一段时间、熬过多次GC的老年代对象以获取更好收集效果。

    (3). 空间整合:从整体来看是基于“标记-整理”算法实现,从局部(两个Region之间)来看是基于“复制”算法实现的,但是都意味着G1运行期间不会产生内存碎片空间,更健康,遇到大对象时,不会因为没有连续空间而进行下一次GC,甚至一次Full GC。

    (4). 可预测的停顿:降低停顿是G1和CMS共同关注点,但G1除了追求低停顿,还能建立可预测的停顿模型,可以明确地指定在一个长度为M的时间片内,消耗在垃圾收集的时间不超过N毫秒

    (5). 跨代特性:之前的收集器进行收集的范围都是整个新生代或老年代,而G1扩展到整个Java堆(包括新生代,老年代),将整个Java 堆的内存划分为多个大小相等的独立区域(Region),不再是物理隔离,也不是需要连续的Region 集合。

     G1收集器可以有计划地避免在整个Java 堆中进行全区域的垃圾收集,通过使用Remembered Set 来避免全堆扫描也不会遗漏,如果不计算维护Remembered Set 的操作,G1收集器的运作大致可以划分为以下几个步骤:

    (1). 初识标记:标记GC Root 直接关联的对象

    (2). 并发标记:从GC Root 开始对堆中的对象进行可达性分析,找出存活对象

    (3). 最终标记:修正在并发标记期间因因用户程序继续运作而导致标记产生变动的那一部分标记记录

    (4). 筛选标记:①对Region 的回收价值和成本进行排序②制定回收计划

    8. 理解GC 日志:每一种收集器的日志形式都是由它们自身的实现所决定的,换而言之,每个收集器的日志格式都有可以不一样

    六、内存分配与回收策略

    自动内存管理:给对象分配内存以及回收分配给对象的内存。而给对象分配内存主要是在Eden 区,少数情况下可能会直接分配在老年代中,分配具体规则如下:

    1. 对象优先在Eden 分配

    大多数情况下,对象在新生代Eden 中分配,当Eden 区没有足够的空间时虚拟机将发起一次Minor GC(新生代GC)

    2. 大对象直接进入老年代

    需要大量连续内存空间的对象(例如很长的字符串以及数组),通过设置-XX:PretenureSizeThreshold 参数令大于这个设置值的对象直接进入老年代

    3. 长期存活的对象进入老年代

    每个对象都有一个对象年龄(Age)计数器,对象每经过一次Minor GC,年龄就增加1,增加到一定的程度(默认15,可以通过-XX:MaxTenuringThreshold 设置)就将会被晋升到老年代。

    4. 动态对象年龄判断

    当Survival 空间中相同年龄所有对象大小总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold 中年龄的要求。

    5. 空间分配担保

    七、总结

    认识垃圾收集算法、几款JDK1.7 中提供的垃圾收集器特点以及运作原理,了解Java 虚拟机自动内存分配及回收的主要规则。

    声明:本文版权归作者和博客园共有,欢迎转载,但请在文章页面明显位置给出原文连接。 
  • 相关阅读:
    实现一个最简单的flask应用程序
    python常识
    Flex布局
    ES6的promise的学习
    通过正则获取url参数
    dom0级事件和dom2级事件
    sea.js总结
    跨域的几种方式
    人生苦短,生命也就一次,机会也就一次
    新开的博客先和大家打个招呼吧!
  • 原文地址:https://www.cnblogs.com/hellovoyager1/p/9173010.html
Copyright © 2020-2023  润新知