侧边栏壁纸
  • 累计撰写 57 篇文章
  • 累计创建 10 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

3、圾收集器与内存分配策略

yilee
2023-04-04 / 0 评论 / 0 点赞 / 55 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
本文最后更新于2024-05-31,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

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

3.1 概述

GC 需要完成的3件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

​ 程序计数器、虚拟机枝、本地方法梳3 个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作,每一个钱帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性。

​ Java 堆和方法区中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

3.2 对象已死吗

  1. 引用计数算法

    很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加l ;当引用失效时,计数器值就减l :任何时刻计数器为0 的对象就是不可能再被使用的。

    虚拟机并不是通过引用计数算法来判断对象是否存活的

  2. 可达性分析算法

    在主流的商用程序语言(Java 、C#,甚至包括前面提到的古老的Lisp )的主流实现中,都是称通过可达性分析( Reachability Analysis )来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“ GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots 没有任何引用链相连(用图论的话来说,就是从GC Roots 到这个对象不可达〉时,则证明此对象是不可用的

    image-20220902193432933

    在Java 语言中,可作为GC Roots 的对象包括

    • 虚拟机技(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法战中JNI (即一般说的Native 方法)引用的对象。
  3. 再谈引用

    无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。

    在JDK 1.2 之后, Java 对引用的概念进行了扩充,将引用分为强引用( Strong Reference )、软引用( Soft Reference )、弱引用( Weak Reference )、虚引用( Phantom Reference) 4 种,这4 种引用强度依次逐渐减弱。

    • 强引用就是指在程序代码之中普遍存在的,类似“ Object obj = new Object() ”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
    • 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
    • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。
    • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。
  4. 生存还是死亡

    要真正宣告一个对象死亡,至少要经历两次标记过程:对象不可达时被第一次标记并查询此对象是否有必要执行finalize() 方法。若有必要执行finalize() 方法,那么这个对象将会放置在一个叫做F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer 线程去执行它。==注意,虚拟机并不承诺会等待它运行结束。==稍后GC 将对F-Queue 中的对象进行第二次小规模的标记。

  5. 回收万法区

    ​ 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

    判定一个常量是否是“废弃常量”(加入常量为一个字符串“ abc ”)

    • 没有任何String 对象引用常量池中的“ abc ”常量
    • 没有其他地方引用了这个字面量

    判定一个类是否是“无用的类”

    • 该类所有的实例都已经被回收,也就是Java 堆中不存在该类的任何实例。
    • 加载该类的ClassLoader 已经被回收。
    • 该类对应的java.lang . Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    在大量使用反射、动态代理、CGLib 等ByteCode 框架、动态生成JSP 以及OSGi 这类频繁自定义ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

3.3 垃圾收集算法

  1. 标记一清除算法(Mark Sweep)最基础的收集算法

    ​ 算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实在前一节讲述对象标记判定时已经介绍过了。

    ​ 标记一清除算法是最基础的收集算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

  2. 复制算法(Copying)

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

    • 每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。、
    • 这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

    IBM 公司的专门研究表明,新生代中的对象98% 是“朝生夕死”的,因此将内存分为一块较大的Eden 空间和两块较小的Survivor 空间,每次使用Eden 和其中一块Survivor实现该算法,当Survivor 空间不够用时伞,
    需要依赖其他内存(这里指老年代)进行分配担保( Handle Promotion )。Hotspot 虚拟机默认Eden和Survivor 的大小比例是8 : 1。

  3. 标记一整理算法(Mark Compact)

    ​ 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  4. 分代收集算法(Generational Collection)

    ​ 根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

    • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
    • 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。

3.4 Hotspot 的算法实现

  1. 枚举根节点

    ​ 在可达性分析中从GC Roots 节点找引用链,逐个检查这里面的引用,那么必然会消耗很多时间。

    ​ 可达性分析对执行时间的敏感还体现在GC 停顿上,在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。

    ​ 由于目前的主流Java 虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。

    ​ 在HotSpot 的实现中,是使用一组称为OopMap 的数据结构来达到这个目的的,在类加载完成的时候, HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在JIT 编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

  2. 安全点

    ​ HotSpot 也的确没有为每条指令都生成OopMap ,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点( Safepoint ),即程序执行时并非在所有地方都能停顿下来开始GC ,只有在到达安全点时才能暂停。

    ​ 安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint 。

    如何在GC 发生时让所有线程都“跑”到最近的安全点上再停顿下来。

    • 抢先式中断不需要线程的执行代码主动去配合,在GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。(几乎没有用了)
    • 主动式中断的思想是当GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
  3. 安全区域

    ​ 程序不执行即没有分配CPU 时间时,线程无法响应JVM 的中断请求,“走”到安全的地方去中断挂起,JVM 也显然不太可能等待线程重新被分配CPU 时间。对于这种情况,就需要安全区域( Safe Region )来解决、

    ​ 安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC 都是安全的。我们也可以把Safe Region 看做是被扩展了的Safepoint 。

    ​ 在线程执行到Safe Region 中的代码时,首先标识自己已经进入了Safe Region,当在这段时间里JVM 要发起GC 时,就不用管标识自己为Safe Region 状态的线程了。在线程要离开Safe Region 时,它要检查系统是否已经完成了根节点枚举(或者是整个GC 过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region 的信号为止。

3.5 垃圾收集器

基于JDK 1.7 Update 1 4 之后的HotSpot 虚拟机的收集器,图3 -5 展示了7 种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

image-20220903172948726

  1. Serial 收集器

    ​ Serial 收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.l 之前)是虚拟机新生代收集的唯一选择。这个收集器是一个单线程的收集器,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结收集器的发展:Serial收集器 -> Parallel收集器 -> Concurrent Mark Sweep (CMS)收集器 -> Garbage First ( G1)收集器

    ​ Serial 收集器对于运行在Client 模式下的虚拟机来说是一个很好的选择。

    image-20220903204816989

  2. ParNew 收集器

    ​ Par New 收集器其实就是Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial 收集器可用的所有控制参数、收集算法、Stop The World 、对象分配规则、回收策略等都与Serial 收集器完全一样。

    image-20220903204826949

    ​ Par New 收集器是许多运行在Server 模式下的虚拟机中首选的新生代收集器,因为除了Serial 收集器外,目前只有该收集器能与CMS 收集器配合工作。

    重要概念(并发和并行)

    • 并行( Parallel ):指多条垃圾收集线程并行工作,但此时用户线斗呈仍然处于等待状态。
    • 并发( Concurrent ):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU 上。
  3. Parallel Scavenge 收集器

    ​ Parallel Scavenge 收集器是一个新生代收集器,也经常称为“ 吞吐量优先”收集器。 它也是使用复制算法的收集器,又是并行的多线程收集器。特点是它的目标则是达到一个可控制的吞吐量( Throughput )即用于运行用户代码的时间与CPU 总消耗时间的比值。

    ​ Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数。

    • MaxGCPauseMillis 参数允许的值是一个大于0 的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。(注意:GC 停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。)
    • GCTimeRatio 参数的值应当是一个大于0 且小于100 的整数, 也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。
    • +UseAdaptiveSizePolicy 这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小、Eden 与Survivor 区的比例、晋升老年代对象大小 等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC 自适应的调节策略(GC Ergonomics )。

    自适应调节策略也是Parallel Scavenge 收集器与ParNew 收集器的一个重要区别。

  4. Serial Old 收集器

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

    在Server模式下

    • 在JDK 1.5 以及之前的版本中与ParallelScavenge 收集器搭配使用、
    • 作为CMS 收集器的后备预案,在并发收集发生Concurrent Mode Failure 时使用。

    image-20220903204816989

  5. Parallel Old 收集器

    ​ Parallel Old 是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记一整理”算法。由于老年代Serial Old 收集器在服务端应用性能上的“拖累气使用了Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果。

    ​ 直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU 资源敏感的场合,都可以优先考虑Parallel Scavenge 加Parallel Old 收集器。

    image-20220904155330763

  6. CMS 收集器

    ​ CMS (Concurrent Mark Sweep )收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java 应用集中在互联网站或者B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

    CMS 收集器是基于“标记一清除”算法实现的,过程如下:

    • 初始标记( CMS initial mark):只是标记一下GC Roots 能直接关联到的对象,速度很快
    • 并发标记( CMS concurrent mark): 进行GC Roots Tracing 的过
    • 重新标记( CMS remark): 修正并发标记期间因用户程序继续运作而导致标记产生变动的部分对象
    • 并发清除( CMS concurrent sweep):

    ​ 其中初始标记、重新标记这两个步骤仍然需要“ Stop The World ”,由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此从总体上来说, CMS 收集器的内存回收过程是与用户线程一起并发执行的。

    image-20220904155857956

    CMS 是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,但它有以下3 个明显的缺点:

    • CMS 收集器对CPU 资源非常敏感。CMS 默认启动的回收线程数是( CPU 数量+3) /4,当CPU 在4 个以上时,并发回收时垃圾收集线程不少于25% 的CPU 资源,并且随着CPU 数量的增加而下降。但是当CPU 不足4个时, CMS 对用户程序的影响就可能变得很大。

    • CMS 收集器无法处理浮动垃圾( Floating Garbage ),可能出现“ Concurrent Mode Failure ”失败而导致另一次Full GC 的产生。由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用。要是预留内存无法满足程序需要,就会出现一次“ Concurrent Mode Failure “ 失败,这时虚拟机将临时启用Serial Old收集器来重新进行老年代的垃圾收集。

      伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃坡出现在标记过程之后, CMS 无法在当次收集中处理掉它们,只好留待下一次GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。

      在JDK 1.5 的默认设置下, CMS 收集器当老年代使用了68%的空间后就会被激活

      在JDK 1.6 中, CMS 收集器的老年代启动阔值已经提升至92%

    • CMS 是一款基于“标记一清除”算法实现的收集器,收集结束时会有大量空间碎片产生。导致为新对象分配时无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC

      开关参数 -XX:+UseCMSCompactAtFullCollection 用于在CMS 收集器顶不住要进行FullGC 时开启内存碎片的合并整理过程。

      参数 -XX :CMSFullGCsB eforeCompaction 设置执行多少次不压缩的Full GC 后,跟着来一次带压缩的(默认值为0 ,表示每次进入F ull GC 时都进行碎片整理)。

  7. G1 收集器

    G1 (Garbage-First )收集器是当今收集器技术发展的最前沿成果之一,Gl 是一款面向服务端应用的垃圾收集器。

    • 并行与并发:G1 能充分利用多CPU 、多核环境下的硬件优势,使用多个CPU (CPU或者CPU 核心)来缩短Stop-The-World 停顿的时间
    • 分代收集:G1可以不需要其他收集器配合就能独立管理整个GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC 的旧对象以获取更好的收集效果。
    • 空间整合:G1 从整体来看是基于“标记一整理”算法实现的收集器,从局部(两个Region 之间〉上来看是基于“复制”算法实现的。
    • 可预测的停顿:这是Gl 相对于CMS 的另一大优势,其能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N 毫秒,这几乎已经是实时Java (RTSJ) 的垃圾收集器的特征了。

    ​ 使用Gl 收集器时, Java 堆的内存布局就与其他收集器有很大差别,它将整个Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续) 的集合。

    ​ GI 收集器之所以能建立可预测的停顿时间模型, 是因为它可以有计划地避免在整个Java 堆中进行全区域的垃圾收集。Gl 跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region (这也就是Garbage-First 名称的来由) 。

    ​ 在Gl 收集器中,虚拟机都是使用Remembered Set 来避免全堆扫描的。当进行内存回收时,在GC 根节点的枚举范围中加入Remembered Set 即可保证不对全堆扫描也不会有遗漏。

    Gl 中每个Region 都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference 类型的数据进行写操作时,会产生一个Write Barrier 暂时中断写操作,检查Reference 引用的对象是否处于不同的Region 之中,是则通过CardTable 把相关引用信息记录到被引用对象所属的Region 的Remembered Set 之中。

    收集器的运作大致可划分为以下几个步骤

    • 初始标记( Initial Marking):只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS (Next Top at Mark Start )的值,需要停顿线程,但耗时很短。
    • 并发标记( Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
    • 最终标记( Final Marking):修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,将这段时间对象变化记录在线程Remembered Set Logs 里。这阶段需要停顿线程,但是可并行执行。
    • 筛选回收( Live Data Counting and Evacuation):最后在筛选回收阶段首先对各个Region 的回收价值和成本进行排序,根据周户所期望的GC 停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region ,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

    image-20220904163523430

  8. 理解GC 日志

    33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
    100.667: [Full GC [Tenured : OK - >210K(l0240K ), 0 .0149142 secs] 4603K->210K(1 9456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys =0.00, real=0.02 secs]
    
    • 最前面的数字“ 33.125 :”和“ 100 .667 :”代表了GC 发生的时间,这个数字的含义是从Java 虚拟机启动以来经过的秒数。
    • GC 日志开头的“[ GC ”和“[ Full GC ”说明了这次垃圾收集的停顿类型,有“ Full ”,说明这次GC 是发生了Stop-The-World 的。
    • 接下来的“[ DefNew ”、“ [ Tenured ”、“[ Perm ”表示GC 发生的区域,这里显示的区域名称与使用的GC 收集器是密切相关的,如果采用Parallel Scavenge 收集器,那它配套的新生代称为“ PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。
    • 方括号内部的“ 3324K-> 152K(3712K)”含义是“ GC 前该内存区域已使用容量一> GC 后该内存区域已使用容量(该内存区域总容量)”。
    • 方括号之外的3324K->152K(l 1904网” 表示“ GC 前Java 堆己使用容量-> GC 后Java 堆已使用容量(Java堆总容量) "。
    • 0 .0025925 secs ,,表示该内存区域GC 所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如“[ Times : user=0.01 sys=0.00 , real=0.02 secs ]”,
    • 这里面的user 、sys 和real 与Linux 的time 命令所输出的时间含义一致,分别代表用户态消耗的CPU
      时间、内核态消耗的CPU 事件和操作从开始到结束所经过的墙钟时间( Wall Clock Time ) 。
  9. 垃圾收集器参数总结

    无标题

3.6 内存分配与回收策略

​ 对象的内存分配,就是在堆上分,对象主要分配在新生代的Eden 区上,如果启动了本地线程分配缓冲, 将按线程优先在TLAB 上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

  1. 对象优先在Eden 分配

    ​ 大多数情况下,对象在新生代Eden 区中分配。当Eden 区没有足够空间进行分配时,虚拟机将发起一次MinorGC 。虚拟机提供了-XX:+PrintGCDetails 这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。

    MinorGC 和Full GC

    • 新生代GC( Minor GC ) :指发生在新生代的垃圾收集动作,因为Java 对象大多都具备朝
      生夕叉的特性,所以MinorGC 非常频繁, 一般回收速度也比较快。
    • 老年代GC(Major GC / Full GC ) : 指发生在老年代的GC ,出现了Major GC ,经常会伴
      随至少一次的Minor GC (但非绝对的,在Parallel Scavenge 收集器的收集策略里就有直接
      进行Minor GC 的策略选择过程)。Major GC 的速度一般会比Minor GC 慢10 倍以上。
  2. 大对象直接进入老年代

    ​ 大对象即需要大量连续内存空间的Java 对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息, 经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来 “安置” 它们。

    ​ 虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在E den 区及两个Survivor 区之间发生大量的内存复制(注意:新生代采用复制算法收集内存)。

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

    ​ 为了分代,虚拟机给每个对象定义了一个对象年龄( Age )计数器。如果对象在Eden 出生并经过第一次Minor GC 后仍然存活,并且能被Survivor 容纳的话,将被移动到Survivor 空间中,并且对象年龄设为1 。对象在Survivor 区中每“熬过” 一次Minor GC ,年龄就增加1 岁,当它的年龄增加到一定程度(默认为15 岁),就将会被晋升到老年代中。对象晋升老年代的年龄阔值,可以通过参数-XX:MaxTenuringThreshold 设置。

  4. 动态对象年龄判定

    ​ 虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold 才能晋升老年代,如果在Survivor 空间中相同年龄所有对象大小的总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

  5. 空间分配担保

    在发生Minor GC 之前,

    • 虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间、如果这个条件成立,那么Minor GC 可以确保是安全的。

    • 如果不成立,则虚拟机会查看HandlePromotionFailure 设置值是否允许担保失败。

    • 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,

    • 如果大于,将尝试着进行一次Minor GC ,尽管这次Minor GC 是有风险的;

    • 如果小于,或者HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次Full GC

    ​ 取平均值进行比较其实仍然是一种动态概率的手段, 也就是说,如果某次Minor GC 存活后的对象突增,远远高于平均值的话,依然会导致担保失败( Handle Promotion Failure )。如果出现了HandlePromotionFailure 失败,那就只好在失败后重新发起一次Full GC 。

3.7 小结

​ 内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为。因此, 学习虚拟机内存知识,如果要到实践调优阶段,那么必须了解每个具体收集器的行为、优势和劣势、调节参数。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区