16jvm

本文最后更新于 2021-07-10 11:56:11

JVM

对象生死判定

引用计数算法

Java 中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1

难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。

可达性分析算法

img

可达性分析算法也叫根搜索算法,通过一系列的称为 GC Roots 的对象作为起点,然后向下搜索。搜索所走过的路径称为引用链 (Reference Chain), 当一个对象GC Roots 没有任何引用链相连时, 即该对象不可达,也就说明此对象是 不可用的

Java中, 可作为GC Roots的对象包括以下四种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈JNINative方法)引用的变量
  • 方法区类静态属性引用的变量
  • 方法区常量引用的变量

垃圾回收算法

标记-清除算法

标记-清除算法对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。

实现简单,不需要进行对象进行移动。

标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

复制算法

这种收集算法解决了标记清除算法存在的效率问题。它将内存区域划分成相同的两个内存块。每次仅使用一半的空间,JVM生成的新对象放在一半空间中。当一半空间用完时进行GC,把可到达对象复制到另一半空间,然后把使用过的内存空间一次清理掉。

按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

标记-整理算法

标记-整理算法 采用和 标记-清除算法 一样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将所有的存活对象往一端空闲空间移动,然后清理掉端边界以外的内存空间。

解决了标记-清理算法存在的内存碎片问题。

仍需要进行局部对象移动,一定程度上降低了效率。

垃圾回收器

Serial-复制算法

img

Serial收集器是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。

Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于运行在Client模式下的虚拟机来说是一个很好的选择。

ParNew收集器-复制算法

img

ParNew收集器是新生代并行收集器,其实就是Serial收集器的多线程版本。

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

Parallel Scavenge(并行回收)收集器-复制算法

Parallel Scavenge收集器是新生代并行收集器,追求高吞吐量,高效利用 CPU

该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。

Serial Old 收集器-标记整理算法

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

在server模式下

在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用

作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

img

Parallel Old 收集器-标记整理算法

img

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。

Parallel 收集器在新生代和老年代也都有对应的版本,除了收集算法不同,两个版本并没有其他差异

CMS收集器-标记清除算法

img

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

  • 初始标记

    • 单线程执行
    • 需要stop-the-world
    • 仅仅把GC Roots的直接关联可达的对象给标记一下,由于直接关联对象比较小,所以这里的速度非常快
  • 并发标记

    • 对于初始标记过程所标记的初始标记对象,进行并发追踪标记
    • 此时其他线程仍可以继续工作
    • 此处时间较长,但不停顿
    • 并不能保证可以标记出所有的存活对象
  • 重新标记

    • 在并发标记的过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾
    • 此处执行并行标记,与用户线程不并发,所以依然是“Stop The World”
    • 且停顿时间比初始标记稍长,但远比并发标记短
  • 并发清除

    • 并发清除之前所标记的垃圾
    • 其他用户线程仍可以工作,不需要停顿
  • 对cpu资源敏感

    • 在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
  • 浮动垃圾

    • CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾
  • 产生大量内存碎片

    • 这个问题并不是CMS的问题,而是算法的问题。由于CMS基于”标记-清除”算法,清除后不进行压缩操作,所以会产生碎片
    • “-XX:+UseCMSCompactAtFullCollection”
      • 使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;
      • 但合并整理过程无法并发,停顿时间会变长;
      • 默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
    • “-XX:+CMSFullGCsBeforeCompaction”
      • 设置执行多少次不压缩的Full GC后,来一次压缩整理;
      • 为减少合并整理过程的停顿时间;
      • 默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
      • 由于空间不再连续,CMS需要使用可用”空闲列表”内存分配方式,这比简单实用”碰撞指针”分配内存消耗大;

G1回收器-Region

img

img

G1(Garbage - First)名称的由来是G1跟踪各个Region里面的垃圾堆的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region

G1与前面的垃圾收集器有很大不同,它把新生代、老年代的划分取消了

取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了

在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

可预测的停顿:低停顿的同时实现高吞吐量。G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这样就保证了在有限的时间内尽可能提高效率。(这也就是Garbage-First名称的来由)

ZGC

染色指针示意图

在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。

ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。

ZGC主要新增了两项技术,一个是着色指针Colored Pointer,另一个是读屏障Load Barrier。

TB 级别的堆内存管理,GC Pause 不会随着 堆大小的增加 而增大

ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked1、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。

由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。

与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。

JVM内存模型

img

栈示意图1

  • 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。

    • 堆的作用是存放对象实例和数组。从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的
  • 方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。

  • 方法栈(JVM Stack):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。

  • 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native 方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。

  • 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。

大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。在1.7之前在(JDK1.2 ~ JDK6)的实现中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收

对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它可永久代有什么不同的?存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的而元空间属于本地内存;存储内容不同,元空间存储类的元信息静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。


16jvm
https://jiajun.xyz/2020/11/26/java/java基础/16jvm/
作者
Lambda
发布于
2020年11月26日
更新于
2021年7月10日
许可协议