JVM内存和垃圾回收

这篇文章将要包括的内容

JVM内存区域

概述

和c++不同,Java把内存的控制权交给虚拟机管理,所以熟悉JVM如何管理内存就显得十分重要了。

运行时数据区

  • 程序计数器

    当前线程所执行的字节码的行号指示器

  • 本地方法栈

    本地方法栈(Native Method Stack)与虚拟机栈的作用相似。二者的区别在于:虚拟机栈为 Java 方法服务;本地方法栈为 Native 方法服务

  • 虚拟机栈

    管理方法的运行

  • Java 堆

    Java 堆(Java Heap) 的作用就是存放对象实例,几乎所有的对象实例都是在这里分配内存

  • 方法区

    方法区(Method Area)也被称为永久代。方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    JDK1.8之后方法区改成了元空间,放在直接内存,不属于运行时数据区,防止方法区过大爆掉运行时数据区的内存,意味着它可以使用机器的全部内存。

hotspot虚拟机对象的剖析

对象的创建过程(区别于类加载)

JVM内存和垃圾回收_第1张图片

Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

Step3:初始化零值(默认初始化)
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头(设置放置在对象头中的信息)
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行init方法(构造初始化)
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的访问定位

句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
JVM内存和垃圾回收_第2张图片

直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
JVM内存和垃圾回收_第3张图片

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

垃圾回收

何为垃圾(那些内存需要回收?)

回收区域:堆和方法区

垃圾定义:引用计数、可达性分析

可达性分析的GC roots:
简洁版:栈(本地方法和虚拟机栈)上的变量、方法区中常量引用的对象和类静态属性引用的对象
详细版:在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。 ·
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 ·
在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
·所有被同步锁(synchronized关键字)持有的对象
·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

完整的GC流程(何时回收?)

新生代:eden:survivor:survivor=8:1:1
老年代
新生代存满了,触发MinorGC,但是之前会检查老年代能不能存下当前新生代的对象
如果开启了老年代空间分配担保规则,那么如果满足老年代中剩余空间大小,大于历次Minor GC之后剩余对象的大小
那么也不会去做fullGC,如果不满足上述条件的话,就fullGC
如果fullGC也不行就会报OOM

垃圾回收算法(如何回收?)

1、标记-清除算法:效率不稳定、内存碎片化
2、标记-复制算法(新生代):分成等大小两块,一次用一块,把一块(A)复制到另外一块(B),然后清除A,大量的复制操作,需要较大空间
3、标记-整理(老年代):标记好了先移动到一边整理,然后清除

分代收集:新生代用标记-复制,老年代用标记-整理
JVM内存和垃圾回收_第4张图片
整理过程:

  1. JAVA 虚拟机提到过的处于方法区的永生代(Permanent Generation)(又称为方法区),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
  2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。
  3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 FromSpace 进行清理。
  4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老年代。
  5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
  6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老年代中。

分区收集:G1垃圾收集器使用

垃圾收集器 (如何回收?)

概览

1、Serial收集器:单线程收集器
2、ParNew收集器:Serial的多线程版本
3、Parallel Scavenge:关注点是吞吐量而非停顿时间
4、Serial Old:Serial的老年代版本,使用标记-整理算法
5、Parallel Old:Parallel Scavenge收集器的老年代版本,使用标记-整理算法
6、CMS收集器
7、Garbage First收集器
8、Shenandoah收集器
9、ZGC
1、2、3、4、5比较好记忆,6、7、8、9见下文JVM内存和垃圾回收_第5张图片

CMS收集器

关注点:吞吐量
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的

过程:其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)

详细过程:
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
而重 新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短;
最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的 对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

缺点:
1、会降低吞吐量
2、无法处理浮动垃圾
3、大量空间碎片

缺点详细版
1、首先,CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏 感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计 算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量 +3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能 力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
2、由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
3、CMS是一款基于“标记-清除”算法实现的收集器,如果 读者对前面这部分介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。

G1收集器

区别点:分区(region)、可以设置期望停顿的时间、按收益动态确定回收集

过程:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)

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

与CMS对比
1、优点
简洁版:
(1)可以指定最大停顿时间、
(2)分Region的内存布局、
(3)按收益动态确定回收集这些创新性设计带来的红利
(4)不存在内存碎片
详细版:暂且不论可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集这些创新性设计带来的红利,
单从最传统的算法理论上看,G1也更有发展潜力。与CMS 的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存 空间碎片,垃圾收集完成之后能提供规整的可用内存。

Shenandoah收集器

与G1的不同之处
1、支持并发的整理算法
2、默认不使用分代收集的
3、摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系。

过程:
初始标记(Initial Marking)
并发标记(Concurrent Marking
最终标记(Final Marking)
并发清理(Concurrent Cleanup)
并发回收(Concurrent Evacuation) 和之前收集器的不同之处
并发引用更新(Concurrent Update Reference)
最终引用更新(Final Update Reference)
并发清理(Concurrent Cleanup)
读者只要抓住其中三个最重要的并发阶段(并发标记、并发回收、并发引用更新)

过程详细版:
Shenandoah收集器的工作过程大致可以划分为以下九个阶段
1、初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍 是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。
2、并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段 是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
3、最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值 最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停 顿。
4、并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到 的Region(这类Region被称为Immediate Garbage Region)。
5、并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah与之前HotSpot中其他收集器的 核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之 中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进 行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象 进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对 象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通 过读屏障和被称为“Brooks Pointers”的转发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要 再回头介绍它)。并发回收阶段运行的时间长短取决于回收集的大小。
6、初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指 向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未 做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收 集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的 停顿。
7、并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户 线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它 不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为 新值即可。
8、最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots 中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
9、并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已 再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收 这些Region的内存空间,供以后新对象分配使用。

Shenandoah用以支持并行整理的核心概念 ——Brooks Pointer
学习了Shenandoah收集器的工作过程,我们再来聊一下Shenandoah用以支持并行整理的核心概念 ——Brooks Pointer。“Brooks”是一个人的名字。1984年,Rodney A.Brooks在论文《Trading Data Space for Reduced Time and Code Space in Real-Time Garbage Collection on Stock Hardware》中提出了使用转发 指针(Forwarding Pointer,也常被称为Indirection Pointer)来实现对象移动与用户程序并发的一种解决 方案。此前,要做类似的并发操作,通常是在被移动对象原有的内存上设置保护陷阱(Memory Protection Trap),一旦用户程序访问到归属于旧对象的内存空间就会产生自陷中段,进入预设好的异 常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上。虽然确实能够实现对象移动与用 户线程并发,但是如果没有操作系统层面的直接支持,这种方案将导致用户态频繁切换到核心态, 代价是非常大的,不能频繁使用。

转发指针工作原理
不过,毕竟对象定位会被频繁使用到,这仍是一笔不可忽视的执行成本,只是它比起内存保护陷 阱的方案已经好了很多。转发指针加入后带来的收益自然是当对象拥有了一份新的副本时,只需要修 改一处指针的值,即旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转 发到新的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址 访问的代码便仍然可用,都会被自动转发到新对象上继续工作

转发指针缺点
转发指针另一点必须注意的是执行频率的问题,尽管通过对象头上的Brooks Pointer来保证并发时 原对象与复制对象的访问一致性,这件事情只从原理上看是不复杂的,但是“对象访问”这四个字的分 量是非常重的,对于一门面向对象的编程语言来说,对象的读取、写入,对象的比较,为对象哈希值 计算,用对象加锁等,这些操作都属于对象访问的范畴,它们在代码中比比皆是,要覆盖全部对象访 问操作,Shenandoah不得不同时设置读、写屏障去拦截。 之前介绍其他收集器时,或者是用于维护卡表,或者是用于实现并发标记,写屏障已被使用多 次,累积了不少的处理任务了,这些写屏障有相当一部分在Shenandoah收集器中依然要被使用到。除 此以外,为了实现Brooks Pointer,Shenandoah在读、写屏障中都加入了额外的转发处理,尤其是使用 读屏障的代价,这是比写屏障更大的。代码里对象读取的出现频率要比对象写入的频率高出很多,读 屏障数量自然也要比写屏障多得多,所以读屏障的使用必须更加谨慎,不允许任何的重量级操作。 Shenandoah是本书中第一款使用到读屏障的收集器,它的开发者也意识到数量庞大的读屏障带来的性 能开销会是Shenandoah被诟病的关键点之一[9],所以计划在JDK 13中将Shenandoah的内存屏障模型改 进为基于引用访问屏障(Load Reference Barrier)[10]的实现,所谓“引用访问屏障”是指内存屏障只拦 截对象中数据类型为引用类型的读写操作,而不去管原生数据类型等其他非引用字段的读写,这能够 省去大量对原生类型、对象比较、对象加锁等场景中设置内存屏障所带来的消耗。

ZGC收集器

定义:ZGC收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
关键词:
1、Region
2、染色指针
3、内存多重映射

Region(大、中、小)
先从ZGC的内存布局说起。与Shenandoah和G1一样,ZGC也采用基于Region的堆内存布局,但 与它们不同的是,ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称 为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的 Region可以具有如图3-19所示的大、中、小三类容量:
小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对 象。
大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置 4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实 现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到) 的,因为复制一个大对象的代价非常高昂。

染色指针
染色指针是一种直接将少量额外的信息存储在指针上的技术,可是为什么指针本身也可以存储额 外信息呢?在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节[3]。实际上,基于需求 (用不到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多)和成本(消耗更多晶 体管)的考虑,在AMD64架构[4]中只支持到52位(4PB)的地址总线和48位(256TB)的虚拟地址空 间,所以目前64位的硬件实际能够支持的最大内存只有256TB。此外,操作系统一侧也还会施加自己 的约束,64位的Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空 间,64位的Windows系统甚至只支持44位(16TB)的物理地址空间。 尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍 然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46位指针宽 度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对 象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问 到,如图3-20所示。当然,由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致 ZGC能够管理的内存不可以超过4TB(2的42次幂)

染色指针优点
染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的 目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些 专门的记录操作。实际上,到目前为止ZGC都并未使用任何写屏障,只使用了读屏障(一部分是染色 指针的功劳,一部分是ZGC现在还不支持分代收集,天然就没有跨代引用的问题)。内存屏障对程序 运行时性能的损耗在前面章节中已经讲解过,能够省去一部分的内存屏障,显然对程序运行效率是大 有裨益的,所以ZGC对吞吐量的影响也相对较低。

过程
并发标记(Concurrent Mark)
并发预备重分配(Concurrent Prepare for Relocate)
并发重分配(Concurrent Relocate)
并发重映射(Concurrent Remap)

过程详细版
1、并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的 阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的 短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志 位。
2、并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出 本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器 的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增 量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的 维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面 的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对 全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。
3、并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分 配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明 确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次 访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象 上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(SelfHealing)能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比 Shenandoah的Brooks转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢, 因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染 色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于 新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也 没有关系,这些旧指针一旦被使用,它们都是可以自愈的。
4、并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所 有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不 是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第 一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束 后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射 阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所 有对象的,这样合并就节省了一次遍历对象图[9]的开销。一旦所有指针都被修正之后,原来记录新旧 对象关系的转发表就可以释放掉了。

垃圾收集器的选择

关注点
如果是数据分析、科学计算类的任务,目标是能尽快算出结果, 那吞吐量就是主要关注点;如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务 超时,这样延迟就是主要关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是 不可忽视的。

JVM回收方式和内存分配策略

回收方式

Minor GC

MinorGC 采用复制算法。

  1. eden 、 From 复制到 To,年龄+1
    首先,把 Eden和 From 区域中存活的对象复制到 To区域(如果有对象的年龄以及达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1(如果 To不够空间就放到老年区);
  2. 清空 eden 、 From
    然后,清空 Eden 和 From 中的对象;
  3. To和 From 互换
    最后,To和 From 互换,原 To成为下一次 GC 时的 From 区。

Full GC

触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
1、 调用 System.gc()
2、老年代空间不足
3、空间分配担保失败
4、JDK 1.7 及以前的永久代空间不足
5、Concurrent Mode Failure

内存分配策略

1、对象优先在 Eden 分配
2、大对象直接进入老年代
3、长期存活的对象进入老年代

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

5、空间分配担保
老年代会对servivor区进行空间担保,也就是存活的对象太多或者太大的时候,可以临时存放在老年代,这个特殊的对象通过一个引用来与servivor建立关联,所以因为有这样一种空间分配当宝机制,在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,说明满足担保条件,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

你可能感兴趣的:(2022我的春招实习笔记,java)