jvm初探-内存分配gc原理与垃圾收集器

菜鸟-翡青 菜鸟-翡青     2022-08-20     579

关键词:

JVM初探- 内存分配、GC原理与垃圾收集器

标签 : JVM


JVM内存的分配与回收大致可分为如下4个步骤: 何时分配 -> 怎样分配 -> 何时回收 -> 怎样回收.
除了在概念上可简单认为new时分配外, 我们着重介绍后面的3个步骤:


I. 怎样分配- JVM内存分配策略

对象内存主要分配在新生代Eden区, 如果启用了本地线程分配缓冲, 则优先在TLAB上分配, 少数情况能会直接分配在老年代, 或被拆分成标量类型在栈上分配(JIT优化). 分配的规则并不是百分百固定, 细节主要取决于垃圾收集器组合, 以及VM内存相关的参数.


对象分配

  • 优先在Eden区分配
    JVM内存模型一文中, 我们大致了解了VM年轻代堆内存可以划分为一块Eden区和两块Survivor区. 在大多数情况下, 对象在新生代Eden区中分配, 当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域, 如果在Minor GC期间发现新生代存活对象无法放入空闲的Survivor区, 则会通过空间分配担保机制使对象提前进入老年代(空间分配担保见下).
  • 大对象直接进入老年代
    Serial和ParNew两款收集器提供了-XX:PretenureSizeThreshold的参数, 令大于该值的大对象直接在老年代分配, 这样做的目的是避免在Eden区和Survivor区之间产生大量的内存复制(大对象一般指 需要大量连续内存的Java对象, 如很长的字符串和数组), 因此大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间.

对象晋升

  • 年龄阈值
    VM为每个对象定义了一个对象年龄(Age)计数器, 对象在Eden出生如果经第一次Minor GC后仍然存活, 且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代.

  • 提前晋升: 动态年龄判定
    然而VM并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代: 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无须等到晋升年龄.


II. 何时回收-对象生死判定

(哪些内存需要回收/何时回收)

在堆里面存放着Java世界中几乎所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些对象已死(可回收).


可达性分析算法

在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象:

  • 在Java, 可作为GC Roots的对象包括:
    1. 方法区: 类静态属性引用的对象;
    2. 方法区: 常量引用的对象;
    3. 虚拟机栈(本地变量表)中引用的对象.
    4. 本地方法栈JNI(Native方法)中引用的对象。

注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).


III. GC原理- 垃圾收集算法

分代收集算法 VS 分区收集算法

  • 分代收集
    当前主流VM垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的 新生代老年代永久代. 这样就可以根据各年代特点分别采用最适当的GC算法:
    • 在新生代: 每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集.
    • 在老年代: 因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”“标记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.
  • 分区收集
    上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分, 而分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间.
    在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿.

分代收集

新生代-复制算法

该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉.

(图片来源: jvm垃圾收集算法)

这使得每次只对其中一块内存进行回收, 分配也就不用考虑内存碎片等复杂情况, 实现简单且运行高效.

现代商用VM的新生代均采用复制算法, 但由于新生代中的98%的对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代.


老年代-标记清除算法

该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.

该算法会有以下两个问题:
1. 效率问题: 标记和清除过程的效率都不高;
2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集.


老年代-标记整理算法

标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存.


永久代-方法区回收

  • 在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量无用的类. 回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:
    1. 该类所有的实例都已经被回收, Java堆中不存在该类的任何实例;
    2. 该类对应的Class对象没有在任何地方被引用(也就是在任何地方都无法通过反射访问该类的方法);
    3. 加载该类的ClassLoader已经被回收.

但即使满足以上条件也未必一定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 因此在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项, 开启VM的类卸载功能, 以保证方法区不会溢出.


补充: 空间分配担保

在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).
然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).


IX. GC实现- 垃圾收集器

GC实现目标: 准确、高效、低停顿、空闲内存规整.


新生代

1. Serial收集器

Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它的特点是 只用一个CPU/一条收集线程去完成GC工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW).

虽然是单线程收集, 但它却简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内.


2. ParNew收集器

ParNew收集器其实是前面Serial的多线程版本, 除使用多条线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一样(也是VM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器).

由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).


3. Parallel Scavenge收集器

与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量:

=(+)

jvm初探-使用堆外内存减少fullgc

...签:JVM问题:大部分主流互联网企业线上ServerJVM选用了CMS收集器(如Taobao、LinkedIn、Vdian),虽然CMS可与用户线程并发GC以降低STW时间,但它也并非十分完美,尤其是当出现ConcurrentModeFailure由并行GC转入串行时,将导致非常长时间的StopTheWorld... 查看详情

jvm之gc日志分析与对象内存分配回收策略(代码片段)

...🐬GC日志分析的重要性:阅读分析虚拟机和垃圾收集器的日志是处理Java虚拟机内存问题必备的基础技能。🐟垃圾收集日志面临的问题:垃圾收集器日志是一系列人 查看详情

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

...理的。因此内存的分配和回收也是jvm三大功能之一。垃圾收集器(GC)需要完成三件事情:哪些内存需要回收?什么时候进行回收?如何回收?本篇博客将解答jvm是如何处理以上三个问题的。值得注意的是,java运行时数据区中的... 查看详情

jvm基础--gc垃圾回收机制

...com/nantang/p/5674793.htmlhttps://www.cnblogs.com/fenghun/p/5029535.htmlJVM初探:内存分配、GC原理与 查看详情

jvm--垃圾回收

...有一定了解的基础上,接下来进行JVM垃圾收集的学习垃圾收集器与内存分配策略1.概述  内存的动态分配与内存回收技术已经很成熟了,了解GC和内存分配:一方面为了当出现内存溢出,内存泄漏的时候排查问题,另一方面垃... 查看详情

垃圾收集器与内存分配策略(深入理解jvm二)

1.概述垃圾收集(GarbageCollection,GC).当需要排查各种内存溢出、内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。Java内存运行时,程序计数器、... 查看详情

理解jvm之垃圾收集器概述

前言很多人将垃圾收集(GarbageCollection)视为Java的伴生产物,实际1960年诞生的Lisp是第一门真正使用内存动态分配与垃圾手机技术的语言。在目前看来,内存的动态分配与内存回收已经相当成熟,但了解GC与内存分配还是非常有必要... 查看详情

jvm03------垃圾收集(下)(代码片段)

...己的JVM自动分配和回收内存空间。垃圾回收机制是由垃圾收集器GarbageCollection来实现的,GC是后台一个低优先级的守护进程。在内存中低到一定限度时才会自动运行,因此垃圾回收的时间是不确定的。为何要这样设计:因为GC也要... 查看详情

jvm虚拟机-03jvm内存分配机制与垃圾回收算法(代码片段)

JVM虚拟机-03、JVM内存分配机制与垃圾回收算法1JVM内存分配与回收1.1对象优先在Eden区分配大多数情况下,对象在新生代中?Eden?区分配。当?Eden?区没有足够空间进行分配时,虚拟机将发起一次Minor?GC。我们来进行实际测试一下。在... 查看详情

jvm内存分配策略

...会伴随至少一次的MinorGC(并非绝对,比如在ParallelScavenge收集器的收集策略里就有直接进行MajorGC的策 查看详情

垃圾收集器与内存分配策略(代码片段)

1.引用计数GC算法每个对象都会有对应的计数器来计算对象引用,但JVM不会采用该策略,因为不能解决对象相互引用的回收。publicclassReferenceCountingGCpublicObjectinstance=null;privatestaticfinalint_1M=1024*1024;privatebyte[]bigSize=newbyte[2*_1M];publicstat... 查看详情

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

1.    垃圾收集器与内存分配策略垃圾回收机制(GarbageCollection,GC),GC的历史要比java悠久。1960年诞生于MIT的Lisp是第一个真正使用内存动态分配和垃圾收集技术的语言。当时人们考虑GC需要解决三件事:哪些内存需要回收... 查看详情

《深入理解jvm——gc算法与内存分配策略》

 JVM深入理解JVM(2)——GC算法与内存分配策略 PostedbyCrowonAugust10,2017说起垃圾收集(GarbageCollection,GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行... 查看详情

《深入理解jvm——gc算法与内存分配策略》(代码片段)

 JVM深入理解JVM(2)——GC算法与内存分配策略 PostedbyCrowonAugust10,2017说起垃圾收集(GarbageCollection,GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行... 查看详情

jvm架构和工作原理及gc工作机制

...要讲四个部分:JVM结构、内存分配、垃圾回收算法、垃圾收集器一、JVM结构JVM主要包括四个部分:1.类加载器(ClassLoader):在JVM启动时或者在类运行时将需要的class加载到JVM中。2.执行引擎:负责执行class文件中包含的字节码指令... 查看详情

深入jvm系列之gc机制收集器与gc调优

一、回想JVM内存分配须要了解很多其它内存模式与内存分配的,请看 深入JVM系列(一)之内存模型与内存分配1.1、内存分配:1、对象优先在EDEN分配2、大对象直接进入老年代 3、长期存活的对象将进入老年代 4、适龄... 查看详情

深入jvm系列之gc机制收集器与gc调优(转)

一、回顾JVM内存分配 需要了解更多内存模式与内存分配的,请看 深入JVM系列(一)之内存模型与内存分配  1.1、内存分配:1、对象优先在EDEN分配2、大对象直接进入老年代 3、长期存活的对象将进入老年代 ... 查看详情

jvm系列---垃圾收集器与内存分配策略

回顾上文介绍了jvm的内存区域以及介绍了内存的溢出情况。jvm区域分为5个,线程独有:虚拟机栈,本地方法栈,程序计数器。线程共享:方法区,堆两种溢出:栈溢出(StackOverflowError),OutOfMemoryError(OOM)为什么学习垃圾收集... 查看详情