一篇搞懂JVM与经典GC(CMS/G1)

一.jvm内存区域,先上图

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。
在 JVM 中,JVM 内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈等。

同时按照与线程的关系也可以划分为线程私有和线程共有:
线程私有区域:一个线程拥有单独的一份内存区域,例如本地方法栈,虚拟机栈,程序计数器。
线程共享区域:被所有线程共享,且只有一份,例如堆,方法区。

接下来咱们一一解释jvm内存区域。

1.首先是方法区(别名元空间),java不同版本叫法不同,后期版本优化为元空间。方法区与堆空间类似,也是一个共享内存区,所以方法区是线程共享的。它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法等(早期方法区包含运行时常量池,后期运行时常量池移到堆内了,但是逻辑上还是属于方法区)。

需要注意的是:方法区也是会触发full gc的,方法区的空间默认21m,如果项目很大,代码很多很可能这些类的元信息和字节码信息加载到内存时会大于方法区默认空间大小,就会触发1-n次full gc来动态扩大元空间,所以启动jvm时设置好元空间大小,绝大部分项目256-512m的空间就够用了。

2.虚拟机栈

栈其实是一种先进后出,后进先出的数据结构,jvm中的栈也是类似。

jvm栈的功能就是通过栈帧存储程序调用的过程,包括调用程序中的基础数据类型以及引用数据类型数据,这些变量在脱离栈的作用域后会自动释放

虚拟机栈是线程私有的,每个虚拟机栈都包括操作数栈,局部变量表,动态链接和返回地址。

局部变量表:
顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,我们只需要存放它的一个引用地址即可。
操作数据栈
存放 java 方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。操作数栈本质上是 JVM  执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。
动态连接:
涉及Java 语言特性多态,会根据多态的类型去调用相应的方法。
返回地址:
正常返回(调用程序计数器中的地址作为返回)、异常等。

虚拟机栈结构如下图

线程在运行时,在执行每个方法的时候都会创建一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口(返回地址)等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。栈的大小缺省为 1M,可配置。

3.本地方法栈

本地方法栈与java虚拟机栈类似,只不过本地方法栈管理的是本地方法的调用,它服务的对象是 native 方法,也就是jvm封装好的方法,将本地方法栈与虚拟机栈合二为一去理解就好,唯一的区别就是两种栈的栈帧中调用的方法不同,前者是jvm级别,后者是app级别。

4.程序计数器

较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。

通俗的理解就是记录当前程序执行到哪里了。程序运行过程,线程上下文切换,stop world时的安全区域都会涉及到程序计数器。它也是jvm中唯一一个不会出现内存溢出的的内存区域。

5.堆

堆是jvm中最大的一块内存区域,程序中创建的几乎所有对象都在这里存储,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。垃圾回收也主要是回收这一块内存区域。

随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。在 Java 中,就叫作 GC。

堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区, Survivor 又由 From Survivor 和 To Survivor 组成,正常情况下Eden 与Survivor的空间比例是8:1:1。

 6.直接内存(也称堆外内存)

JVM 运行时,会从操作系统申请大块的堆内存,进行数据的存储;包括虚拟机栈、本地方法栈和程序计数器,堆等等区域。在这之外操作系统剩余的内存就是堆外内存。java中unsafe类可以操作堆外内存。

二.JVM中的垃圾回收(GC)

1.垃圾回收算法

根据新生代与老年代的不同,来使用不同的垃圾回收算法。

新生代一般使用复制算法

老年代一般使用标记-清除算法或者标记-整理算法

复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

Appel  式回收
一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),使用效率从50%提升到90%。

标记-清除算法:

算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。

标记-整理算法:

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然 没有内存碎片,但是 效率偏低。我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新( 直接指针需要调整)。所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。

2.JVM中常见的垃圾回收器

见下图

早期的单线程垃圾回收器,Serial/Serial Old。只适合回收几十到一两百兆的空间。

jdk1.8默认垃圾回收器Parallel Scavenge (ParallerGC )/Parallel Old,适合回收几百兆到几个G的空间。

ParNew与CMS配合使用,虽然ParNew是多线程的但是基本和serial没什么区别,唯一的区
别:多线程,多 CPU 的,停顿时间比 Serial 少。ParNew基本已被淘汰。CMS适合回收几个G到20G的空间

G1将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(region)。同时并没有在空间上严格的将内存划分为新老代,新生代和老年代互相之间会动态变化。G1适合回收上百G内存空间

CMS是第一代支持多并发垃圾回收器,G1是跟以往垃圾回收器有较大区别的回收器,重点比较下这两个比较比较又代表意义的垃圾回收器

Concurrent Mark Sweep(CMS )

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。随着互联网的发展,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。CMS 收集器是基于“标记—清除”算法实现的。
整个过程分为 4 个步骤,包括:
初始标记-短暂,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
并发标记-和用户的应用程序同时进行,进行 GC Roots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)
重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除,由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

虽然CMS是第一代并发垃圾回收器,但是它也有一些缺点,比如CPU敏感,当处理核心数不足 4 个时,CMS 对用户的影响较大(已经2023年了,一般都多核服务器了,不会有大的影响)。浮动垃圾,并发清楚过程中,用户线程还在运行,这一时间段产生的垃圾就是浮动垃圾,所以jvm一般会预留出一小部分空间来处理浮动垃圾。空间碎片,由于CMS采用标记-清除算法进行老年代垃圾回收,必然会产生不连续的内存空间,当这种不连续的内存空间随着垃圾回收增多时,会严重影响大对象的分配,此时CMS可能会退化成Serial Old单线程回收器

G1


设计思想
随着 JVM 中内存的增大,STW 的时间成为 JVM 急迫解决的问题,G1 将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region
Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂,例如堆空间总大小又4096M,region个数设置成最大2048,那每个region的空间就是2M。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的进行回收大多数情况下都把 HumongousRegion 作为老年代的一部分来进行看待。具体从上图中,能看得出来G1内存分配比以往的垃圾回收器有很大的不同。

垃圾回收过程

G1 的运作过程大致可划分为以下四个步骤:
初始标记
仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
并发标记
从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记
对用户线程做另一个短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的漏标记录。
筛选回收
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。

总结对比: CMS就是最经典的分代回收的并发垃圾回收器,老年代采用标记-清除,缺点是空间碎片较多时会退化成serial old单线程回收,此时serial old采用标记-整理老年代,效率大幅度变慢。G1为了匹配更大的空间回收,采用化整为零的方式,将堆内存平均划分为多个等大的空间region,并没有严格意义上区分新老代,在逻辑上保持原有的分代回收机制(年轻代复制算法,老年代标记-整理)的同时,新老代之间动态变化,极大的提高了垃圾回收的效率(region原地从年轻代变成老年代,省去了新老代变化数据移动的过程),而且G1的region会合并多个region空间为超大空间,在大对象分配上更加便利,效率更高。G1与CMS的垃圾回收空间的平衡点是6-8G,空间越大越能体现G1垃圾回收器的优势。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值