JVM知识总结

分代模型 垃圾回收 参数优化

Posted by songjunhao on November 15, 2022

内存区域

方法区/元数据空间:存放各种类相关的信息。线程共享。

堆内存:存放代码创建的各种对象。线程共享。

程序计数器:用来记录当前执行的字节码指令的位置。线程独占。

Java虚拟机栈:保存每个方法内的局部变量等数据的。线程独占。

栈帧:局部变量表,操作数栈,动态链接,方法出口等。调用方法时压栈帧,退出方法出栈。

本地方法栈:native 方法调用的时候,会有线程对应的本地方发展。类似java虚拟机栈。

分代模型

分代的根本原因是,一部分对象存活时间很短,一部分对象存活时间很长。

对象的生存周期不同,因此,JVM将堆内存划分了两个区域,一个是年轻代,一个是老年代。

根据年轻代和老年代的特点不同,采用的GC算法也不同。

年轻代:大部分年轻代的对象都是朝生夕死。

老年代:长时间存活的对象会放到老年代。

永久代:也叫方法区,存放一些类信息。

对象什么时候进入新生代?

大部分正常对象,优先在新生代分配内存。

对象什么情况下回进入老年代?

1.对象年龄大于等于15岁,即在新生代成功躲过15次,还没回收,则会被转移到老年代。

2.动态对象年龄判断:当前存放对象的survivor区,一批对象总大小,大于这块survivor区的内存大小的50%,此时大于等于这批对象年龄的对象,可以直接进入老年代。

3.大对象直接进入老年代:-XX:PretenureSizeThreshold,可以设置字节数,大于这个字节数的对象, 就直接放到老年代里。主要是避免一个很大的对象在内存中来回复制,降低性能。

4.Minor GC 后对象太多无法放入 survivor 区。则会直接都放入老年代。

Young GC / Minor GC 触发时机?

1.新生代,分配内存时,发现新生代eden区内存不足时。

2.G1回收时,预估到本次回收将近达到配置的最大停顿时间时。

老年代垃圾回收触发时机

1.执行Minor GC 后,如果老年代空间不足以保存存活的对象,则进行一次Old GC。

2.Minor GC 前检查,老年代可用的连续内存空间 < 新生代历次Young GC后升入老年代的对象总和平均大小,提前Old GC。

3.使用CMS垃圾回收时,-XX:CMSInitiatingOccupancyFaction 参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收。

什么情况下会发生Metaspace内存溢出

1.本身代码大于默认配置,或配置过少 2.cglib之类动态生成类过多。

核心JVM参数

  • -Xms : Java 初始分配的堆内存大小
  • -Xmx:Java 堆内存的最大大小
  • -Xmn:Java 堆内存中的新生代大小,减去新生代大小就是老年代大小
  • -XX:PermSize: 永久代初始大小(1.8之前)
  • -XX:MaxPermSize:永久代最大大小 (1.8之前)
  • -XX:MetaspaceSize:永久代初始大小(1.8之前)
  • -XX:MaxMetaspaceSize: 永久代最大大小 (1.8之前)
  • -Xss:每个线程的栈内存大小
  • -XX:+PrintGCDetils:打印详细的gc日志
  • -XX:+PrintGCTimeStamps:打印每次GC发生的时间
  • -Xloggc:xx-gc.log:指定gc日志磁盘文件
  • -XX:+HeapDumpOnOutOfMemoryError:OOM时自动dump内存快照
  • -XX:HeapDumpPath=/usr/local/app/oom:内存快照放到哪去

xms 和 xmx 一般配置成一样大小,减少jvm动态调整的性能损耗。 永久代一般设置几百M即可。 栈内存大小一般是512KB到1MB。

GC算法与垃圾回收器

Stop The World 状态

停止ava系统的所有工作线程。只进行垃圾回收的工作。

可达性分析

JVM使用可达性分析算法判断哪些对象可以被回收。

对象逐层向上查找,是否有GC Roots引用。

GC Roots: 1.局部变量 2.静态变量

垃圾回收器

Serial和Serial Old垃圾回收器
  • 分别用来回收新生代和老年代的垃圾对象
  • 单线程运行,运行时停止其他线程,一般不用
ParNew
  • ParNew 一般都是用在新生代
  • 多线程并发,默认线程数跟CPU核心数一样
  • -XX:+UseParNewGC 开启
  • -XX:ParallelGCThreads 线程数配置,一般直接用默认值
CMS垃圾回收器
  • 用在老年代
  • 多线程并发
  • 垃圾回收和系统工作线程,尽量同时执行模式

CMS 执行过程: 1.初始标记

  • stop the world
  • 标记 GcRoots 直接引用的对象

2.并发标记

  • 系统线程可以随意创建各种新对象,继续运行
  • 是对老年代所有对象进行GC Roots追踪,是最耗时的,但因为并发所有没有性能影响

3.重新标记

  • stop the world
  • 重新标记下在第二阶段里变动过的对象(新建,失去引用)

4.并发清理

  • 让系统程序随意运行,清理掉之前标记为垃圾的对象

CMS 主要是将耗时短的阶段和耗时长的阶段分离,只有短的阶段才stop the world。

CMS 调优 问题1:占用CPU资源。

CMS默认言动的垃圾回收线程的数量是(CPU核数+3)/4

问题2:Concurrent Mode Failure

-XX:CMSInitiatingOccupancyFaction 参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收

JDK 1.6 默认 92%

预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中。

CMS垃圾回收期间,需要放入老年代的对象大于可用空间,会发生Concurrent Mode Failure。然后立即使用SerialOld垃圾回收器,慢慢回收。

问题3:内存碎片

-XX:+UseCMSCompactAtFullCollection ,该参数默认开启,Full GC 之后 stop the world,进行碎片整理。

-XX:CMSFullGCsBeforeCompaction,多少次Full GC之后再执行一次内存碎片整理。默认是0。也就是每一次。

G1垃圾回收器
  • 统一收集新生代和老年代
  • 把Java堆内存拆分为多个大小相等的Region
  • 有逻辑上的新生代和老年代的:新生代包含某些Region,老年代包含某些Reigon
  • 可设置垃圾回收的预期停顿时间,例如可以保证1小时内,STW,不超过1分钟,-XX:MaxGCPauseMills 配置,默认200ms,配置少了,会频繁gc,配置多了一次gc停顿时间太长。
  • Region随时会属于新生代也会属于老年代,不存在新生代和老年代分配多少内存,新生代和老年代的内存区域不停变动,由G1控制
  • -XX:+UseG1GC 启用
  • 默认新生代占堆总内存5%,可以通过-XX:G1NewSizePercent 配置。运行中会自动调整增加,但占比上线不会超过默认60%,可以通过-XX:G1MaxNewSizePercent配置。
  • 新生代+老年代混合回收,-XX:InitiatingHeapOccupancyPercent,默认45%,当老年代占45%个region的时候,会尝试触发混合回收。

G1回收原理:

G1通过把内存拆分为大量小Region,追踪每个Region中可回收对象大小及预估时间,尽量控制在指定的时间范围内,同时尽量回收更多的垃圾对象。

每个Region的大小 等于 堆大小/2048。JVM最多有2048个Region。Region的大小必须是2的倍数。

手动指定Region大小:-XX:G1HeapRegionSize

新生代依然有 eden survivor 概念,跟其他的一样。

一旦新生代达到了设定的占据堆内存的最大大小60%,且Eden区占满了对象(或者估算达到回收停顿时间)。会触发新生代的GC,G1会用复制算法进行垃圾回收,STW。回收时会追踪每个region回收所需时间,选择一部分region回收来保证gc停顿时间控制在指定范围内。

进入老年代算法也一样,年龄及动态年龄判断。

大对象Region 大对象判定规则:一个大对象超过了一个Region大小的50% 一个大对象如果太大,可能会横跨多个Region

新生代老年代回收的时候,会对大对象Region一起回收。

会根据配置的gc停顿时间给新生代不停分配更多Region。直到估算本次回收差不多是gc停顿时间左右,就进行一次新生代的gc。

G1垃圾回收过程

1.初始标记 STW,仅仅标记GC Roots直接引用对象,速度快。

2.并发标记 系统程序运行的同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,比较耗时

3.最终标记阶段 STW,找到并发标记阶段修改过的对象,标记哪些还存活,哪些是垃圾。

4.混合回收阶段

计算老年代中每个Region中

  • 存活对象数量
  • 存活对象的占比
  • 执行垃圾回收的预期性能和效率。

然后STW,让垃圾回收的停项时间控制在我们指定的范围内,选择Region回收。

最后这阶段可以执行多次,-XX:G1MixedGCCountTarget 参数控制,最后一个阶段执行几次混合回收。

这样可以让系统停顿时间不要过长。

G1整体都是基于 复制算法 进行回收。

-XX:G1HeapWastePercent 默认 5%, 意思是本次回收空闲出的region达到堆内存总的5%,就会停止混合回收,本次GC结束。

-XX:G1MixedGCLiveThresholdPercent,默认85%,意思Region中存活对象低于85%,才可被回收。

如果拷贝中发现没有空闲region可以承载了,则会切换到单线程标记,清理,压缩。非常慢。

G1优化

思路也是避免大量对象进入老年代。 主要还是通过控制-XX:MaxGCPauseMills 配置进行优化。

回收算法

新生代:复制算法

新生代分为三块:1个Eden区,2个Survivor区 其中Eden区,占80%,每一个Survivor区域占10%

可以使用的就是 eden 区 和 其中一个 survivor 区。

对象分配在eden区,如果eden区快满了,此时触发垃圾回收,将eden区中存活的对象 和 现在 survivor 区中上次存活的对象, 放到另一块 survivor 区。

优点是,只有10%的内存是闲置。

老年代: 标记整理算法

gc日志

使用 GCEasy 网站分析GC情况。

常用JVM相关工具

jstat

jstat -gc PID 1000 10

每隔一秒更新出最新的一次统计,共执行10次

列名 含义
SOC From Survivor区大小
S1C To Survivor区大小
SOU From Survivor区当前使用内存大小
S1U To Survivor区当前使用内存大小
EC Eden区的大小
EU Eden区当前使用的内存大小
OC 老年代的大小
OU 老年代当前使用的内存大小
MC 方法区(永久代、元数据区)的大小
MU 方法区(永久代、元数据区)的当前使用的内存大小
YGC 系统运行迄今为止的Young GC次数
YGCT Young GC的耗时
FGC 系统运行总Full GC次数
FGCT Full GC的耗时
GCT 所有GC的总耗时

该工具可以推算

  1. 每秒新增多少的对象。
  2. GC次数,平均耗时
  3. 根据YGC频率,以此时间,监控老年代增长。

jmap

jmap -histo PID 按照各种对象占用内存空间大小降序排列。可以快速查到当前内存中哪个对象占用了大量内存。

jmap -dump:live,format=b,file=dumpname.hprof PID

生成堆转储文件,可以用MAT或者jhat分析,一般用于解决内存溢出。

jhat

jhat -port 8080 dumpname.hprof 用于分析堆转储文件

开启一个自定义端口的服务,可通过浏览器访问。

经典优化

新生代垃圾回收优化

  1. Survivor空间是否足够

    如果不够,则可能频繁全扔到老年代,可以适当调大。

  2. 新生代对象,升入进入老年代的年龄。

    -XX:MaxTenuringThreshold,可以适当调小,减少在年轻代占用,可以根据young gc 频率,结合对象存活时间估算。

  3. 直接进入老年代对象大小

    -XX:PretenureSizeThreshold=1M 一般给个 1M 就行,防止在年轻代复制来复制去,消耗性能

  4. 指定垃圾回收器

    -XX:+UseParNewGC

老年代垃圾回收优化

根据触发Full GC的几个场景来看,都是老年代空间不足才触发,所以基本上只要是年轻代尽量都消灭,且老年代只保留长久不回收的例如单例对象,即可。所以在年轻代优化完备后,老年代只需保证空间充足,不过于小即可。

大内存应用

使用G1,控制每次回收时间,不等到集赞特别多才回收。

G1 停顿时间合理预估

过小GC频繁,过大停顿之间过长,预估及配合APM,jstat等监控,找到可接受范围内的找到中间值即可。