jvm 相关

310 阅读42分钟

相关概念:

空间划分

image.png 程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而消亡;栈帧随着方法的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

JIN:Java Native Interface

服务启动中接口超时相关文章,即时编译【挺好的文章】

juejin.cn/post/710188…

(一)内存空间

cpu高,或者占用内存高,不一定是java 进程导致的,hhhh~比如容器里既有java 进程又有非java进程,非java进程也可能占用cpu或者内存非常高。说jvm 是说java进程的。

www.jianshu.com/p/3bab26d25…

(二)虚拟内存

github.com/CyC2018/CS-…

从操作系统的角度来说, 当一个程序申请内存的时候,操作系统并不会立刻把实际的物理内存给他,而是给他虚拟内存。 当程序真正执行用到内存的时候,才发送页面的调度,分配实际内存。

也即Java 配置 -xms,-xmx=4个g的时候,操作系统并不会立刻给他实际内存,当随着老年代增长的,操作系统将会发送页面调度,分配给他实际的内存,也即Rss 随着老年代的增加呈现上涨趋势;

从jvm 层面,当发生垃圾回收,比如full gc 老年代占用内存减少,但是jvm并不会把这些回收后的内存空间立刻返给操作系统。而是方便产生垃圾后再次使用。所以full gc 后,rss 使用率不会减少。

还有可能内存碎片导致,建议使用jemalloc

(三)启动时频繁gc

即使 xms,xmx 配置的一至 ,以及 MetaspaceSize ,MaxMetaspaceSize配置的一致,当机器启动的时候也会出现频繁的ygc ,因为,程序刚启动,必然需要加载很多对象到内存里,光是年轻代肯定装不下,需要频繁的ygc ,把一些对象驱赶到老年代里。

(四) 线程独立性

OOM相关知识,某个OOM后jvm并不会退出。

image.png OutOfMemoryError 也只是一个java中的异常而已。在线程执行中,如果发生的异常,都由线程进行独立的处理,而不会抛出到其它的线程,更不会抛出给启动线程的外部线程。这就是保证了这种线程的独立性。但是当持有大对象的内存异常被catch 住不释放的时候,full gc后仍然空间仍然不被释放,就会导致一直full gc,即时cpu不高,java进程down掉了,也会阻塞java其他线程,包括jsatck命令,因为full gc会一直STW。

java虚拟机退出的条件是:JVM 不存在非守护线程(前台线程),JVM就会退出。 线程OOM,JVM不一定退出。线程池OOM,JVM不一定退出

常见的OOM场景 blog.csdn.net/weixin_4214…

(五)java进程down

  • 一))))系统OOM Killer操作系统日志是否有oom kill(dmesg 有oom kill 信息),这种情况原因有两种:

  • (1)JVM内存导致

  • JVM参数配置的不合理,导致JVM内存超过了容器内存限制被操作系统杀掉。JVM内存 = Heap + Metaspace + Xss + Direct Memory + Code Cache

  • (2)JNI 内存溢出(JAVA NITIVE INTERFACE)

  • (Java程序调用了C类库,这部分内存不归JVM管理,JVM想GC也GC不到),被操作系统杀掉。

  • 二))))jvm 运行崩溃,大概率使用了java agent,会生成hs_err_PID.log,也可以通过JVM参数-XX:ErrorFile来进行指定目录. java agent 为什么会导致jvm 崩溃;类加载隔离问题【双亲委派模型破坏】,字节码合法性问题,内存分配失败

(五)机器cpu比较高的原因

  • 机器cpu过高可能的原因[临时解决方案:机器重启和扩容]
  • (1)代码中有死循环;
  • (2)快速创建大量临时变量,导致频繁触发gc回收;
  • (3) 请求过高,导致创建大量临时变量,导致频繁ygc;
  • (4) 线程上下文切换,锁竞争严重
  • (5)JIT 编译

Code Cache:

blog.csdn.net/renfufei/ar…

"CodeCache is full. Compiler has been disabled." 这个错误信息通常出现在 Java 应用程序中,特别是在使用 HotSpot JVM 的情况下。它表示 Java 虚拟机(JVM)中的代码缓存已经满了,导致编译器被禁用。

  1. CodeCache 是什么? ****CodeCache 是 JVM 中一块独立的 非堆内存区域 ****,专门用于存储 JIT 编译器生成的本地机器码 ****(Native Code)和 JNI 本地方法代码 ****。其核心作用是避免重复编译热点代码,提升执行效率。
  2. 存储内容
    • JIT 编译后的代码 ****:如频繁调用的方法(触发阈值后编译)或循环体(OSR 编译)。
    • JNI 本地方法 ****:通过 native关键字声明的本地方法生成的机器码。
    • 元数据 ****:如编译后的方法签名、栈映射表等辅助信息

native libraries:

而JINI Native Code 是JVM内存调用了Native方法,即C、C++方法,所以需要使用C、C++的思路去解决,暂不考虑。

元空间

Metaspace的大小和加载类的数目有很大关系,加载的类越多,Metaspace占用内存也就越大。 在jvm启动的时候,并不会分配MaxMetaspaceSize这么大的一块内存出来,对与64位JVM来说,元空间的默认初始大小是20.75MB,之后不断地进行扩容。

Metaspace被分配于堆外空间,默认最大空间只受限于系统物理内存。跟它相关的比较重要的两个JVM参数:-XX:MetaspaceSize -XX:MaxMetaspaceSize ,MetaspaceSize:并不是最小的空间,它与内存分配没有任何关系,主要控制matesaceGC发生的初始阈值,也就是最小阈值。也就是说当使用的matespace空间到达了MetaspaceSize的时候,就会触发Metaspace的GC【无论哪个垃圾 回收器,都是full gc】。为了避免元空间超过MetaspaceSize 进行gc然后扩容,我们一般设置MetaspaceSize 与MaxMetaspaceSize一样的值。

MaxMetaspaceSize表示的是保证committed的内存不会超过这个值,一旦超过这个值就会触发Gc,gc后仍超过这个阈值将会抛出异常java.lang.OutOfMemoryError: Metaspace。

图片.png gc设置太小图像现象:

图片 (1).png

当配置上 XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Logs时,检查到OutOfMemoryError错误时将生成dump文件,但是他是堆空间文件,看不了元空间信息。但可以通过工具看到

元空间溢出日志打印:
java.lang.OutOfMemoryError: Metaspace

Dumping heap to /export/Logs/java_pid276.hprof ...

Heap dump file created [1202851396 bytes in 516.014 secs]


garbage-first heap total 4427776K, used 753247K [0x00000006d2400000, 0x00000006d2604390, 0x00000007e0800000)

region size 2048K, 1 young (2048K), 0 survivors (0K)

Metaspace used 450404K, capacity 514157K, committed 524288K, reserved 1513472K

class space used 49054K, capacity 59392K, committed 61184K, reserved 1048576K

gc也就是进行类的卸载。full gc 回收三个区域(新生代,老年代,原空间)

满足三个条件的类才会被卸载:

(1)该类所有的实例都已经被回收;

(2)加载该类的ClassLoader已经被回收;

(3)该类对应的java.lang.Class对象没有任何地方被引用。

类元数据一旦满足卸载条件即可被回收,是否遍历整个元空间,不会

  • GC 不会直接遍历元空间的所有内存块,而是通过 类加载器(ClassLoader) 的引用链间接管理。
  • 关键机制
    • 类加载器作为根对象 :类加载器是类元数据的持有者,若加载器不可达,其加载的类元数据会被标记为垃圾。
    • 元空间分块管理 :元空间以 Chunk为单位分配内存,GC 通过 SpaceManager维护空闲块列表,仅扫描已分配块中的存活元数据

回收算法

  • 标记-清除(Mark-Sweep) :标记所有存活的类元数据(通过类加载器可达性分析),清除不可达的元数据。
  • 分块回收 ****:元空间按 Chunk分配,回收时直接释放空闲 Chunk,无需整理碎片

我们可以在jvm启动的时候配置-XX:TraceClassLoading -XX:TraceClassUnloading

打印类的加载和卸载信息,如果一个类频繁被加载那就是有问题的类(通常是动态类)。

参考:从一起GC血案谈到反射原理原创 heapdump.cn/article/547…

元空间溢出也有可能空间碎片

导致相关问题介绍: bugs.openjdk.org/browse/JDK-… 建议升级到jdk11.用的元空间很少,就溢出了,如下图用的很少,但是提交的很多,很多碎片在里面

image.png

直接内存

为什么直接内存操作效率要高? java操作对象一定要操作堆里的吗

Java NIO,引入了三个要素,selector、channel、buffer。(DirectByteBuffer 是byteBuffer的一种实现方式)。直接内存一般操作效率比较高,主要原因是它调用native本地方法直接分配 Java 虚拟机之外的内存,然后通过一个存储在堆中的DirectByteBuffer对象直接操作该内存,而无须先将外部内存中的数据复制到堆中再进行操作。

由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。所以我们一般通过-xx:MaxDirectMemorySize设置元空间的最大值,如果不指定,默认与堆的最大值-Xmx参数值一 致。

当我们设置了xx:MaxDirectMemorySize设,如果直接内存超过MaxDirectMemorySize将触发full gc。原因如下:

如果通过java原生API ByteBuffer.allocateDirect 申请内存,当达到阈值的时候会强制调用system.gc。full gc并不能直接释放直接内存,但是full gc能回收堆中的DirectByteBuffer对象,当 DirectByteBuffer 被 GC 之前 cleaner 对象会被放入一个引用队列(ReferenceQueue ,JVM 会启动一个低优先级线程扫描这个队列,并且执行 Cleaner 的 clean 方法来做清理工作。释放对应的直接内存。

如果XX:+DisableExplicitGC参数配置,将会导致system.gc 失效,进而直接内存溢出

其实我们也可以通过unsafe来申请内存,然后unsafe释放内存,不用通过full gc 在释放内存。其实allocateDirect底层也是调用unsafe,但是allocateDirect给我们很好的封装了unsafe。

Heap空间

它是java线程共享的用于存储对象实例的空间。-Xms设置堆的最小空间大小。-Xmx设置堆的最大空间大小。一般Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍,我们经常把-Xms和-Xmx 设置的一致,如果 -Xms和-Xmx设置的不一致,在初始化时只会初始-Xms大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 gc 。如果新生代不足是是ygc,老年代不足的话可能会是fullgc 。

1、对于分代垃圾回收算法:

按照分代垃圾回收算法可以把整个堆分为新生代和老年代,新生代又可以分为eden,from ,to。其中java1.8 默认老年代与新生代大小为2:1,eden,from ,to为8:1:1。

image.png

(非分区垃圾回收器示意图)

基本的分代垃圾回收算法

1、 对象首先分配在Eden

2、 新生代Eden空间不足时,会触发minor gc(yong,gc),Eden和survivor from存活对象使用copy到survivor to,存活对象年龄加1,并交换survivor from  和survivor to

3 、当对象寿命超过阈值是,会晋升至老年代。最大寿命为15

4、当老年代空间不足会触发major gc(old gc),目前只有cms会单独对老年代进行gc。

默认新生代

相关参数:

( Old )与 ( Young ) 的比例的值为 1:2

–XX:NewRatio=2

Edem : from : to = 8 : 1 : 1

–XX:SurvivorRatio=8

-XX:NewSize设置新生代最小空间大小。

-XX:MaxNewSize设置新生代最大空间大小。

2、对于分区域垃圾回收算法。

image (1).png

垃圾回收相关概念

(一)判断对象是否是垃圾

1、可达性分析。没有被GCROOT直接或者间接引用的对象会被回收。

2、引用计数法,存在循环引用不可回收的问题

(二)可作为GCroot的四种对象

1类静态属性引用的对象

2常量引用的对象。

3栈帧中局部变量表中引用的对象{java虚拟机栈,本地方法栈}

4NATIVE方法中引用的对象

(三)四种引用

1、强引用,只要对象被强引用引用,jvm宁愿发出内存溢出异常也不会回收该对象

2、软引用,如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,再次垃圾回收就会回收这些对象的内存。

3、弱引用,仅有弱引用引用该对象时,在垃圾回收无论内存是否充足,都会回收弱引用对象。

4、虚引用,仅有虚引用时,它就和没有任何引用一样,在任何时候都可能被垃圾回收,它的主要目的是用来跟踪对象被垃圾回收的活动。

(四)垃圾回收算法

1、标记清除,特点:快,但会产生空间内存碎片。

2、标记整理,特点:慢,不会产生内存空间碎片。

3、复制,特点:比较快,不会有内存碎片,但需要占用双倍内存空间。

一般新生代用复制算法,老年代用另外两种算法,这与空间对象回收的特点有关。

新生代对象存活时间短,每次回收时存活的对象较少,这样复制用到空间也就较少,复制所用的时间也较少。

老年代每次回收时存活的对象较多,不适合用复制算法。

(五)跨代引用:

所谓的跨代既新生代引用老年代对象,或者老年代引用新生代。

1、老年代引用新生代。

如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生对象在收集时同样得以存活,进而年龄增长后晋升到老年代中,这时跨代引用也随机被消除了。ygc的时候没必要全扫老年代。

Remembered Set(Rset) 我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构Remembered Set。G1当中每个region都有一个Rset。一般要和根分区扫描结合。

card table CMA

卡表用于老年代到新生代的引用,将老年代内存空间,或者某一块Region按照512字节分成不同的“卡页(Card Page)”,一个Card Page内存中通常有多个对象,只有有一个对象存在跨代、区域的引用,那么这个卡页就是Dirty的。卡表中维护着一个字节数组,每一位字节按顺序对应一个Card Page。如果对应的Card Page是Dirty的,那么这一位就是1,否则是0。

Write Barrier

HotSpot虚拟机通过Write Barrier维护卡表状态。写屏障可以看做是虚拟机层面对“引用类型变量的赋值”这个动作的AOP切面,在引用赋值时以提供程序执行额外动作。赋值前叫Pre-Write Barrier,赋值后叫Post-Write Barrier。应用写屏障后会被织入赋值操作指令流来更新卡表。

同理读屏障类似,是在从堆中“读取对象引用”时进行类AOP操作。zgc

2、对于老年代被新生代引用

新生代空间小,且对象存活时间短,在扫描老年代的同时可以直接扫描新生代。

(七)几种gc的分类

yong gc

old gc(CMS)

mixed gc: 年轻代和部分老年代(G1)

full gc :年轻代,老年代,元空间

一般情况下,Full GC 之前会先进行一次 Young GC,以尽量减少 Full GC 的负担,提高垃圾收集的整体效率。这种策略有助于减少 Full GC 的频率和暂停时间,从而改善应用程序的性能。

image (2).png

(八) OopMap、Safe Point、Safe Region 年轻代回收以及初始标记阶段

OopMap:GC Roots 枚举的过程中,需要对栈进行扫描,找到哪些地方存储了对象的引用。但是栈存储的数据不止是对象的引用,因此对整个栈进行全量扫描会非常浪费时间。所以引入OopMap来存储栈中的对象引用的信息,以空间换时间。

Safe Point:通过oopMap 可以快速进行GCROOT枚举,但在方法执行的过程中,可能会导致引用关系发生变化,那么OopMap就要随着变化。如果每次引用关系发生了变化都要去修改OopMap成本很高。所以引入安全点的概念。只有在 Safe Point 才会更新(或生成)对应的 OopMap。发生gc时一定要却确保所有的线程都到达安全点,此时OopMap 里的引用关系才是准确。

当所有线程都到达Safe Point,有两种方法中断线程:

◉ 抢占式中断(Preemptive Suspension) JVM会中断所有线程,然后依次检查每个线程中断的位置是否为Safe Point,如果不是则恢复用户线程,让它执行至 Safe Point 再阻塞。 ◉ 主动式中断(Voluntary Suspension) 大部分 JVM 实现都是采用主动式中断,需要阻塞用户线程的时候,首先做一个标志,用户线程会主动轮询这个标志位,如果标志位处于就绪状态,就自行中断

安全点不是任意的选择,既不能太少以至于让收集器等待时间过长,也不能过多以至于过分增大运行时的内存负荷。通常选择一些执行时间较长的指令作为安全点,如方法调用、循环跳转和异常跳转等

safe region

并不是所有线程都有能力到达安全点的,比如线程正处于Sleep或者Blocked状态。这些线程他不会自己走到安全点,所以引入安全区域 (Safe Region)。

安全区域指的是,在某段代码中,引用关系不会发生变化,线程执行到这个区域是可以安全停下进行 GC 的。因此,我们也可以把 安全区域看做是扩展的安全点。当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域。那样当这段时间里虚拟机要发起 GC 时,就不必去管这些在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否处于 STW 状态,如果是,则需要等待直到恢复。

垃圾回收算法

两个指标:

一,吞吐量: CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间)。 比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

二,低延迟(Latency): 也可以理解为最大停顿时间,即垃圾收集过程中一次 STW 的最长时间,越短越好,一定程度上可以接受频次的增大,GC 技术的主要发展方向。

注:STW 造成时间长的主要是问题是在老年代的回收。

一、串行垃圾回收算法。

串行serial(复制算法) +serial old(标记整理)

image (3).png

特点:单线程回收新生代和老年代 ,吞吐量低,延迟高。没有单独针对老年代的垃圾回收,只有yong gc 和full gc。

二、吞吐量优先

paralle scavenge(复制算法) + paralle scavenge old(标记整理)【java1.8默认垃圾回收器】

image (4).png

特点:多线程回收老年代和新生代,吞吐量高,延迟高。没有单独针对老年代的垃圾回收,只有yong gc 和fullgc。【scavenge 捡破烂】

Paralle scavenge 它是一个自适应调节策略垃圾回收器,可以动态调节新生代大小,edgen,survivo比例,晋升老年代参数。最大垃圾收集停顿时间,以及最大gc停顿时间占比,达到可控吞吐量。

  • -XX: MaxGCPauseMillis 控制最大停顿时间,大于0的毫秒数。但是停顿时间变小可能会牺牲吞吐量和新生代空间。
  • -XX: GCTimeRatio 设置吞吐量大小N,大于0小于100的整数,1/(1+N)。默认情况下,-XX:GCTimeRatio 的值是 99,这意味着垃圾收集时间最多占总时间的 1%
  • -XX: +UseAdaptiveSizePolicy,根据系统运行情况动态调整新生代大小、Eden与Survivor比例。

注意:

UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数SurvivorRatio失效

由于AdaptiveSizePolicy会动态调整 Eden、Survivor 的大小,有些情况存在Survivor 被自动调为很小,还存活的对象进入Survivor 装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发FULL GC

三、响应时间优先

Paranew(并行清理新生代(new)) gc(复制算法) +cms(标记清除)

image (5).png

特点:针对新生代采用多线程,老年代采用CMS,有单独的old gc。具有低延迟的特点。

由于并发清理垃圾时候仍然会产生新的老年代垃圾,所以CMS 并不是当老年代空间不足时才进行垃圾回收。

-XX: CMSInitiatingOccupancyFraction =70 控制打到指定阈值的时候进行cms垃圾回收。

(cms算法处理过程)

  1. 初始标记(CMS-initial-mark) ,会导致stw,但速度很快,标记一下GC Roots能直接关联到的对象;
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;GC Roots的直接关联对象开始遍历整个对象图的过程。
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;

会查找前一阶段执行过程中,从新生代晋升或新分配或被更新的对象。通过并发地重新扫描这些对象,预清理阶段可以减少下一个stop-the-world 重新标记阶段的工作量

  1. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;

这个阶段其实跟上一个阶段做的东西一样,也是为了减少下一个STW重新标记阶段的工作量。增加这一阶段是为了让我们可以控制这个阶段的结束时机,比如扫描多长时间(默认5秒)或者Eden区使用占比达到期望比例(默认50%)就结束本阶段。

  1. 重新标记(CMS-remark) ,会导致swt;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
  2. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  3. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset)

缺点:

1、并发阶段虽然不会停止用户线程,但会占用cpu资源造成用户程序运行变慢

2、cms无法回收浮动垃圾 。

3、会产生大量空间内存碎片。

-XX:+UseCMSCompactAtFullCollection可以在 Full GC 期间对老年代进行压缩,减少内存碎片,从而提高内存的利用效率。

-XX:CMSFullGCsBeforeCompaction=:设置在进行 Full GC 之前需要执行多少次 Full GC 才进行一次压缩。默认值通常是 0,即每次 Full GC 都进行压缩。

-XX:+CMSScavengeBeforeRemark 是一个 JVM 参数,用于配置 Concurrent Mark-Sweep (CMS) 垃圾收集器在 Remark 阶段之前是否执行一次 Minor GC(即 Young GC)。【CMS 垃圾收集器的 Remark 阶段是 Stop-the-World(STW)的,这意味着应用程序线程会暂停。通过在 Remark 阶段之前执行一次 Minor GC,可以减少新生代中的存活对象数量,从而减少需要在 Remark 阶段处理的引用数量。这有助于加快 Remark 阶段的执行速度,减少 STW 时间】

并发下的可达性分析

三色标记法

image (6).png

三色标记,在JVM和GO等很多语言的垃圾收集中都有使用。三色标记指三种颜色:白色表示对象未被扫描过,或者在标记阶段结束后没人引用他,仍为白色的对象将被回收。;黑色表示该对象所有引用都被扫描过;灰色表示该对象上至少存在一个引用没被扫描过。

image (7).png

在使用三色标记的遍历算法下。当用户线程产生了从黑色对象到白色对象的引用并且删除了所有从灰色对象到该白色对象的引用时,会影响扫描结果而对用户程序造成影响。因此同时引入了两个解决方案,一个是增量更新,一个是原始快照。

增量更新:当发生这种操作时,就将新插入的引用记录下来,当扫描结束之后再以记录的黑色节点为根重新扫描一次。避免了用户线程新建从黑色对象到白色对象的引用。(CMS)

原始快照:当发生这种操作时,就将要删除的引用记录下来,当扫描结束之后再以记录的灰色节点为根重新扫描一次。避免了所有灰色对象到该白色对象的引用都被删除。(G1)STAB

G1 垃圾回收算法

CMS的最大问题在于老年代会产生空间碎片,导致full gc 会比较频繁。

G1虽然保留新生代和老年代概念,但是这两个空间不再固定,而是一系列Region的动态集合。根据每个Region回收获得的空间大小及所需时间,维护一个回收优先级列表,优先处理回收性价比最高的Region。这也是“Garbage First”名字的由来。

image (8).png

大对象

如果一个对象的大小超过Region容量的50%以上,G1 就认为这是个巨型对象。在其他垃圾收集器中,这些巨型对象默认会被分配在老年代。因为他是直接进入老年代,所以可能是个短期对象。并不符合老年代的定义,可能会增加老年代gc频繁的次数。为了解决这个问题,G1划分了一个H区专门存放巨型对象,这个巨型对象占用的空间会被记入老年代,但是在年轻代回收的时候就进行巨型对象的回收。

起始快照算法Snapshot at the beginning (SATB)【并发标记】

SATB会创建一个对象图,相当于堆的逻辑快照,从而确保并发标记阶段所有的垃圾对象都能通过快照被鉴别出来。当赋值语句发生时,应用将会改变了它的对象图,那么JVM需要记录被覆盖的对象。因此写前栅栏会在引用变更前,将值记录在SATB日志或缓冲区中。每个线程都会独占一个SATB缓冲区,并发标记线程(Concurrent Marking Threads)在标记的同时,还会定期检查和处理全局缓冲区列表的记录,然后根据标记位图分片的标记位,扫描引用字段来更新RSet。此过程又称为并发标记/SATB写前栅栏。

并发优化线程【赋值语句】

当赋值语句发生后,写后栅栏会先通过G1的过滤技术判断是否是跨分区的引用更新,并将跨分区更新对象的卡片加入缓冲区序列,即更新日志缓冲区或脏卡片队列。与SATB类似,一旦日志缓冲区用尽,则分配一个新的日志缓冲区,并将原来的缓冲区加入全局列表中。

并发优化线程(Concurrence Refinement Threads)

只专注扫描日志缓冲区记录的卡片来维护更新RSet,线程最大数目可通过-XX:G1ConcRefinementThreads(默认等于-XX:ParellelGCThreads)设置。并发优化线程永远是活跃的,一旦发现全局列表有记录存在,就开始并发处理。如果记录增长很快或者来不及处理,那么通过阈值-X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-XX:G1ConcRefinementRedZone,G1会用分层的方式调度,使更多的线程处理全局列表。如果并发优化线程也不能跟上缓冲区数量,则Mutator线程(Java应用线程)会挂起应用并被加进来帮助处理,直到全部处理完。因此,必须避免此类场景出现。

无论老年代还是新生代都采用复制算法。

-XX:InitiatingHeapOccupancyPercent=45 默认老年代占用45 触发mixGc

Mix gc阶段

初始标记 Initial Mark

会标记出所有 GC Roots 节点以及直接可达的对象,这一阶段需stop the world,但是耗时很短。但是并不是堆空间达到IHOP阈值之后立刻就进行初始标记STW,而是会等待下一次年轻代收集,利用年轻代收集的STW完成初始标记阶段。

根分区扫描 Root Region Scanning

在初始标记暂停结束后,年轻代收集也完成的对象复制到Survivor的工作,应用线程开始活跃起来。此时为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为根分区扫描(Root Region Scanning) 同时扫描的Suvivor分区也被称为根分区(Root Region)。根分区扫描必须在下一次年轻代垃圾收集启动前完成(并发标记的过程中,可能会被若干次年轻代垃圾收集打断),因为每次GC会产生新的存活对象集合。

并发标记Concurrent Marking

扫描GC Roots的直接关联对象开始遍历整个对象图的过程,该阶段与应用程序并发运行。

采用原始快照的方式,将值记录在SATB日志或缓冲区中。每个线程都会独占一个SATB缓冲区,初始有256条记录空间。当空间用尽时,线程会分配新的SATB缓冲区继续使用,而原有的缓冲去则加入全局列表中。最终在并发标记阶段,并发标记线程(Concurrent Marking Threads)在标记的同时,还会定期检查和处理全局缓冲区列表的记录,然后根据标记位图分片的标记位,扫描引用字段来更新RSet

重新标记( Remark ,STW)

重新标记阶段是为了修正在并发标记期间,因应用程序继续运作而导致标记产生变动的那一部分标记记录,就是去处理剩下的 SATB日志缓冲区和所有更新,找出所有未被访问的存活对象。

清理阶段Cleanup

该阶段主要是排序各个 Region 的回收价值和成本,并根据用户所期望的GC停顿时间来制定回收计划。(这个阶段并不会实际去做垃圾的回收,也不会执行存活对象的拷贝)

清除阶段执行的详细操作有一下几点:

① RSet梳理:启发式算法会根据活跃度和RSet尺寸对分区定义不同等级,同时RSet数理也有助于发现无用的引用。

② 整理堆分区:为混合收集识别回收收益高(基于释放空间和暂停目标)的老年代分区集合;

③ 识别所有空闲分区:即发现无存活对象的分区,该分区可在清除阶段直接回收,无需等待下次收集周期。

image (9).png

当G1发起全局并发标记之后,并不会马上开始混合收集,G1会先等待下一次年轻代收集,然后在该 young gc 收集阶段中,确定下次混合收集的CSet。

8次,是分成8个批次回收,并不是8次年轻代回收,会产生stw的。

(1)并发标记结束以后,老年代中100%为垃圾的 region 就直接被回收了,仅部分为垃圾的region会被分成8次回收(可以通过 -XX:G1MixedGCCountTarget 设置,默认阈值8)。

(2)由于老年代的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且由一个阈值决定内存分段是否被回收 -XX:G1MixedGCLiveThresholdPercent,默认为 65%,意思是垃圾占内存分段比例要达到 65% 才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

(3)混合回收并不一定要进行8次,有一个阈值 -XX:G1HeapWastePercent,默认值 10%,意思是允许整个堆内存有 10%的空间浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收,因为 GC 会花费很多的时间,但是回收到的内存却很少。

ygc 阶段:

swt 并发回收年轻代

Option and Default ValueDescription
-XX:+UseG1GCUse the Garbage First (G1) Collector
-XX:MaxGCPauseMillis=n【也是个神棍】-XX:MaxGCPauseMillis通过动态调整 ​​年轻代大小​​、​​混合回收策略​​、​​堆容量​​ 和 ​​标记触发阈值​​ 等参数,平衡吞吐量与延迟。其核心是通过历史数据和实时监控,自适应选择最优的 GC 策略,但需结合业务场景合理设置目标值,避免过度优化。
G1MaxNewSizePercent60%年轻代最大占比
G1NewSizePercent5%年轻代最小占比
-XX:InitiatingHeapOccupancyPercent=n设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。就是说当使用内存占到堆总大小的45%的时候,G1将开始并发标记阶段。为混合GC做准备,经过观察发现如果这个数值设定过大会导致JVM无法启动并发标记,直接进行FullGC处理。G1的FullGC是单线程,一个22G的对GC完成需要8S的时间,所以这个值在调优的时候写的45%
-XX:NewRatio=nRatio of new/old generation sizes. The default value is 2.
-XX:SurvivorRatio=nRatio of eden/survivor space size. The default value is 8.
-XX:MaxTenuringThreshold=n该参数定义了对象在新生代中经历 ​​Minor GC 的最大次数​​(即年龄阈值),超过该阈值的对象会被晋升到老年代。
-XX:ParallelGCThreads=nSets the number of threads used during parallel phases of the garbage collectors. The default value varies with the platform on which the JVM is running. **并行阶段 STW **
-XX:ConcGCThreads=nNumber of threads concurrent garbage collectors will use. The default value varies with the platform on which the JVM is running. **并行阶段 和应用线程一起运行 **
-XX:G1ReservePercent=nSets the amount of heap that is reserved as a false ceiling to reduce the possibility of promotion failure. The default value is 10.G1ReservePercent 是一个用于调整 G1 垃圾回收器行为的重要参数,指定了堆内存中预留的百分比,以确保在垃圾回收过程中有足够的空间来处理浮动垃圾。通过合理设置这个参数,可以有效减少垃圾回收导致的停顿时间和提升回收效率。
-XX:G1HeapRegionSize=n定义 G1 垃圾回收器中每个 ​​Region 的内存大小​​ The minimum value is 1Mb and the maximum value is 32Mb.
-XX:SoftRefLRUPolicyMSPerMB**:每1M空闲空间可保持的SoftReference软饮用的存活时间对象生存的时长(单位ms)。这个参数就是一个常量,默认值1000,可以通过参数:-XX:SoftRefLRUPolicyMSPerMB进行设置。

G1日志 https://juejin.cn/post/6844903893906751501

监控截图

image (10).png

Zgc(分区垃圾回收器)

G1 有四个STW过程中,1、初始标记因为只标记GC Roots,耗时较短。2、再标记因为对象数少,耗时也较短。3、清理阶段因为内存分区数量少,耗时也较短。4、转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW(虽然是paralle,但耐不住老年代对象多。STW时间较长)。由于G1未能解决转移过程中准确定位对象地址的问题,所以对象转移阶段,无法并行执行。

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

分region布局

小型 Region (Small Region ):容量固定为 2M, 存放小于 256K 的对象。

中型 Region (Medium Region):容量固定为 32M,放置大于等于256K但小于4M的对象。

大型 Region (Large Region): 容量不固定,可以动态变化,但必须为2MB 的整数倍,用于放置 4MB或以上的大对象。

染色指针(Colored Pointer)

所有对象的地址视图默认处于 Remapped状态​​。这是 ZGC 设计的核心机制之一

以前的垃圾收集器的 GC 信息都保存在对象头中,而 ZGC 的 GC 信息保存在指针中(直接把标记信息记录在对象的引用指针上)

image (11).png

8位:预留给以后使用;

1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;

1位:Remapped标识,初始标记时默认值,标记结束时代表要清理的对象

1位:Marked1标识;表示活跃的对象

1位:Marked0标识;表示活跃的对象

42位:对象的地址(所以它可以支持2^42=4T内存)

ZGC 包含的阶段

初始标记:

从 GC Roots 出发,找出 GC Roots 直接引用的对象,对应指针marked0标记为1,这个过程需要 STW,不过STW 的时间跟 GC Roots 数量成正比与堆大小无关,耗时比较短。

并发标记:

  • GC 标记线程访问对象时,如果对象地址视图是 Remapped,就把对象地址视图切换到 Marked0,如果对象地址视图已经是 Marked0,说明已经被其他标记线程访问过了,跳过不处理。

  • 标记过程中Java 应用线程新创建的对象会直接进入 Marked0 视图。

  • 标记过程中Java 应用线程访问对象时,如果对象的地址视图是 Remapped,就把对象地址视图切换到 Marked0(读屏障实现)

  • 标记结束后,如果对象地址视图是 Marked0,那就是活跃的,如果对象地址视图是 Remapped,那就是不活跃的。

再标记和非强根并行标记

重新标记那些在并发标记阶段发生变化的对象。该阶段是 STW的,最多1ms,超过1ms则再次进入并发标记阶段。即ZGC几乎所有暂停都只依赖于GC Roots集合大小。但是因为GC工作线程和应用程序线程是并发运行,所以可能存在GC工作线程执行结束标记时,应用程序线程又有新的引用关系变化导致漏标记。在该步中,还会对非强根(软引用,虚引用等)进行并行标记。

并发预备重分配(Concurrent Prepare for Relocate ):

这个阶段需要根据特定的查询条件统计得出本次收集过程要清理那些 Region,将这些 Region组成重分配集(Relocation Set)。ZGC 每次回收都会扫描所有的 Region,用范围更大的扫描成本换取G1中记忆集和维护成本。

初始转移:

转移根对象引用的对象,该步需要STW。

并发重分配:

读屏障实现:重分配是 ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region上,并为重分配集中的每个 Region维护了一个转发表(Forward Table),记录从旧对象到新对象的转换关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据 Region上的转发表记录将访问转到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。

并发重映射(Concurrent Remap):

重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正他们都是要遍历所有对象,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。

image (1) (1).png

GC时,初始标记:日志中 Pause Mark Start。

GC时,再标记:日志中 Pause Mark End。

GC时,初始转移:日志中Pause Relocate Start。

内存分配阻塞:当内存不足时线程会阻塞等待 GC完成,关键字是 “Allocation Stall”。

ZGC只有三个STW阶段:初始标记,再标记,初始转移。ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在 GC完成之前,新产生的对象不会将堆占满,是 ZGC参数调优的第一大目标,因为在 ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。

zgc 的触发时机

  • 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
  • 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
  • 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
  • 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是“Proactive”。
  • 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
  • 外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
  • 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

full gc 触发时机

一.Minor GC触发条件:当Eden区满时,触发Minor GC。

二.Full GC触发条件:

1.(1)调用System.gc()时,系统建议执行Full GC,但是不必然执行

2.(2)直接内存不足,会System.gc()

3.(2)元空间不足

4.(3)老年代空间不足

老年代空间不足:

<1> Minor Gc后晋升老年代的对象占用空间大于老年代剩余的空间

(1) 对象年龄+1后,大于设置的阈值(默认是15),会进入老年代。

(2) 动态年龄:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,那么该年龄以及以上年龄的对象会晋升为老年代。

(3)survive空间不足: minor Gc后 拷贝到to survive 区的对象占用空间大于survice剩余空间时会直接进入到老年代

(4) 大对象直接进入到老年代【G1除外】

<2>空间担保原则:空间分配担保目的,在年轻代进行Minor GC前,老年代本身应该还有容纳新生代所有对象的剩余空间。如果不够,则进行Full GC。

对于G1转移失败(Evacuation Failure)是指当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、多线程的Stw,Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数-XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,但是这个值最大只能50%,

G1在以下场景中会触发 Full GC,同时会在日志中记录to-space exhausted以及Evacuation Failure:

  • (1)从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
  • (2)从老年代分区转移存活对象时,无法找到可用的空闲分区
  • (3)分配巨型对象时在老年代无法找到足够的连续分区

​ 由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生;

性能调优案例

相关工具

ClassLoader Explorer JVM 内存转储文件中类加载情况

通过 ​​ClassLoader Explorer​​,可以清晰掌握以下信息:

  1. 每个 ClassLoader加载的类数量(Defined Classes)及实例数(Instances)。
  2. 类加载器的层级关系与隔离性。
  3. 重复类加载及潜在内存泄漏点。
相关指令jstat,jcmd,jmap:

jstack : jstack命令用于生成虚拟机当前时刻的线程快照。主要用于分析现场死锁,死循环等问题。

jstat :可以查看系统gc 相关信息。比如元空间有多少,使用了多少,提交了多少。保留了多少

jcmd命令:可以查询直接内存

相关监控:

打印gc日志:-XX:+PrintGCDetails -XX:+PrintGCDateStamps

打印引用:-XX:+PrintReferenceGC, 打印各种引用对象的详细回收时间

跟踪整个空间(堆外内存):-XX:NativeMemoryTracking=detail

cloud.tencent.com/developer/a…

相关案例

参考文章

gc 日志分析:

zhuanlan.zhihu.com/p/613592552

直接内存相关:

www.cnblogs.com/muzhongjian…

kkewwei.github.io/elasticsear…

kkewwei.github.io/elasticsear…

cloud.tencent.com/developer/a…

JNI Memory 内存泄漏

article.juejin.cn/post/725563…

zgc相关:

it-blog-cn.com/blogs/jvm/z…

美团zgc

tech.meituan.com/2020/08/06/…

G1 相关:

pdai.tech/md/java/jvm…

juejin.cn/post/701003…

cloud.tencent.com/developer/a…

cms:

tech.meituan.com/2020/11/12/…

跨带引用问题

www.jianshu.com/p/f1ff4ab0f…

一个 JVM 参数引发的频繁 CMS GC

cloud.tencent.com/developer/a…