00_JVM 笔记总结
约 4740 字大约 16 分钟
2024-08-10
我们写好的代码,编译成字节码文件才能运行
JVM执行一个类之前需要加载类,需要类加载器类加载器是亲子结构的,遵循双亲委派机制
最上层是
Bootstrap ClassLoder,启动类加载器,加载java的核心类库(java安装目录下lib目录的class文件)第二层是
Extension ClassLoder,扩展类加载器,加载java的其他类库(java安装录下lib/ext目录的class文件)第三层是
Application ClassLoder,应用程扩展类加载器,加载我们自己写的类自定义类加载器在第四层
双亲委派机制:如果要加载一个类,先去问他的父级类加载器能不能加载,层层往上,直到顶层。如果顶层类加载器说加载不了,那么就下派到子类,当所有父类都加载不了,那么就自己加载。这么做的好处在于不会重复加载一个类
类加载过程:加载 -> 验证 -> 准备 -> 解析 -> 初始化
加载:类加载器去加载类
验证:主要是验证加载的字节码是否符合
JVM规范,不符合规范JVM无法执行准备:主要是给对象申请内存,给变量设置初始值,该设置
0的设置0,该设置null的就设置null解析:主要是把符号引用替换为直接引用
初始化:主要是给变量赋值,准备阶段只是设置了初始值,这是核心阶段,执行类的初始化,如果有父类必须先加载父类,初始化父类,父类加载完了再执行子类的初始化
类加载
&双亲委派加载示意图

加载的类信息存放在
JVM的方法区(也叫永久代、元数据空间)字节码执行引擎执行加载好的类
执行操作是由线程去执行的,每个线程都配有一个程序计数器和
Java虚拟机栈。Java支持多线程,所以必须要有程序计数器,记录这个线程执行到哪了

Java虚拟机栈执行每个方法的时候都会创建一个栈帧,main方法也一样局部变量放到栈帧中,方法执行完毕,局部变量也就失效了
栈帧如果没有执行完,都是
GC Root,垃圾回收是根据这里的局部变量引用和永久代的引用来判断对象是否存活设置
JVM参数时,Java虚拟机栈一般设置1M大小,一个系统运行最多几百个线程,不用设置太大,浪费内存局部变量保存的是对象的地址,地址指向 JVM堆内存

如果使用
ParNew + CMS垃圾回收器的,堆内存分年轻代和老年代是很明确的,与G1不同ParNew垃圾回收器ParNew垃圾回收器是回收年轻代的,是采用的多线程回收,与Serial回收器使用单线程回收不同ParNew采用复制清除算法,将年轻代分为Eden区和两个Survivor区,JVM参数默认占比是8 : 1 : 1。系统运行把对象创建到Eden区,每次Young GC标记存活对象,复制到Survivor0区,再次Young GC时,再把存活对象复制到Survivor1区,始终保持有一个Survivor区是空着的Eden区的占比可以调优条件有限,没有大内存的机器,对象创建还特别频繁,存活对象比较多,建议把
Eden区比例调低一些,让Survivor大一点,宁可Young GC多一些,也不要让Survivor触发动态年龄审核或者放不下存活对象。如果放不下,这批对象就会进入老年代,Full GC很慢的。虽然调低Eden区占比,Young GC触发很频繁,但是Young GC比较快,所以在内存不足的情况下可以这样调优有条件则加大新生代内存,毕竟
Young GC也会Stop the world的
非常适合回收年轻代,年轻代一般存活对象很少,大多数刚创建出来的很快就变成了垃圾对象,把少数存活对象标记出来,复制成本还是很低的,如果像老年代那样采用标记清除算法,就太慢了

CMS垃圾回收器CMS垃圾回收器使用的是 标记-清除+整理算法,JVM参数默认是 标记-清除5次之后才会去整理内存空间。这个默认参数不太好,可能存在大量内存碎片,如果某一次从年轻代晋升一个大对象,在老年代找不到一块连续的内存,就会触发Full GC。我们可以把这个值调为0,每次CMS垃圾回收之后都会整理内存,虽然每次回收时间会多一些,但是不会出现内存碎片CMS垃圾回收分为4个步骤1、初始标记:需要
STW,只标记GC Root直接引用对象,速度很快,影响不大。正常是单线程标记,JVM可以修改参数,初始标记阶段多线程标记,减少STW时间2、并发标记:不需要
STW,与系统并行处理,垃圾回收线程追踪第一步标记的GC Root,这个阶段很耗时,但不影响程序执行。在并发标记阶段是允许系统继续创建对象的,所以会有新对象进来,也有标记存活对象变成垃圾对象,这些变动的对象JVM都会记下来,等待下一步处理。这个阶段和并发清理都会占用CPU资源3、重新标记:需要
STW,把并发标记阶段有改动的对象重新标记,改动对象不会很多,还是比较快的。但由于要重新判断这个对象是否GC可达,是要比第一步慢的4、并发清理:不需要
STW,清理前几个阶段标记好的垃圾,与系统并行处理,虽然耗时,但不会影响系统运行最后,通过
JVM参数设置,每次Old GC后都重新整理内存,将老年代零零散散的对象排列到一起,减少内存碎片

梳理一下
GC相关的概念名词Young GC/Minor GC:年轻代也可以称之为新生代,年轻代中Eden区内存被占满之后就需要触发年轻代GC,或者叫新生代GCOld GC:老年代的GCFull GC:全面回收整个堆内存,包括新生代、老年代、永久代Major GC:用得很少,是一个比价混淆的概念,有些人把Major GC和Old GC等价起来,也有人把Major GC和Full GC等价起来。听到有人说Major GC的概念,可以问问是想说Old GC还是Full GCMixed GC:G1中特有的概念,一旦老年代占据堆内存的45%就会触发Mixed GC,对年轻代和老年代都会进行回收
频繁出现
Full GC频繁出现
Full GC的几种情况1、内存分配不合理,导致
Survivor区放不下,或者触发动态年龄审核机制,存活对象频繁进入老年代2、内存泄露问题,导致老年代大部分空间被占用,无法回收掉,每次年轻代晋升一点点对象都放不下,触发
Full GC3、大对象,一般代码层面问题,创建太多大对象直接放入老年代,大对象过多导致频繁触发
Full GC4、永久代满了,触发
Full GC,JVM参数设置的256M基本够了,一般由于代码层面的bug引起的5、代码中误用了
System.gc(),这个方法表示有机会的话JVM就会发生一次Full GC。JVM可以设置参数禁用手动调用GC
什么情况下我们要警觉是不是频繁的
Full GC了1、
CPU负载折线上升,特别高2、系统卡死或者处理请求极慢
3、监控系统报警
ParNew + CMS调优调优的重点:尽量不让对象进入老年代
一个系统需要
JVM调优,实际就是Stop The World太久了,导致系统卡顿,调的就是减少STW的时间,让系统没有明显卡顿STW主要是Young GC、Old GC两个阶段,Young GC一般STW时间特别短,Old GC时间一般是Young GC的几倍到几十倍,比较占用CPU资源所以优化重点是让系统减少
Old GC次数,最好让系统只有Young GC,没有Old GC,更没有Full GC
对象进入老年代的几种情况
第一种:对象经过
15次Young GC后依然存活,晋升老年代- 如果系统
1分钟或者30秒一次Young GC,没必要非让对象存活十几分钟才进入老年代,存活两三分钟的对象大概率就是要存活很久的了,调低参数值为5,不让存活对象在两个Survivor里来回复制。如果对象小一旦还好,如果对象挺大的,容易触发Survivor动态年龄审核机制,让一批对象进入老年代
- 如果系统
第二种:
Young GC后存活对象大小超过Survivor区50%,就会触发动态年龄审核机制。比如1、2、3、4岁的对象加起来大于Survivor的50%,那么大于等于4岁的对象全部进入老年代第三种:
Young GC后存活对象大于Survivor的大小,那么这一批对象直接全部进入老年代第四种:大对象直接进入老年代,
JVM参数可以设置,一般设置的1M,大于1M的对象进入老年代,一般很少有1M的对象
第一种和第四种情况一般是可控的,优化的重点是
Survivor区的大小,避免动态年龄审核和Survivor放不下的情况。我们需要通过jstat来查看系统高峰期,JVM中每秒新增对象,每次Young GC多少对象存活
jstatjstat -gc PID 1000 10命令表示秒统计1次,共统计10次。针对Java进程执行这个命令,就可以看这个Java进程(JVM)的内存和GC情况了重点观察指标
Eden区对象的增长速度- 通过上一秒和下一秒
EU的数据可以推断出每秒增长了多少
- 通过上一秒和下一秒
Young GC频率- 通过系统启动时间到目前时间除以
Young GC次数可以计算出来。但是没必要这么干,谁去记项目启动时间呢。我们可以通过Eden区的大小除以Eden区对象增长速度来计算
- 通过系统启动时间到目前时间除以
Young GC耗时- 用
YGCT除以YGC就可以计算出每次的耗时。高峰期也可以单独看几次Young GC然后计算出时间
- 用
Young GC后存活对象大小- 这个指标比较重要,我们要确定每次存活对象
Survivor能不能放得下,保证每次存活对象要小于Survivor的50%,否则触发动态年龄审核机制
- 这个指标比较重要,我们要确定每次存活对象
老年代对象增长速度
- 老年代对象增长速度决定了
Old GC的频率。如果晋升对象特别多,我们就要根据上面的四种情况分析,什么原因导致很多对象进入老年代,然后调整优化
- 老年代对象增长速度决定了
Full GC频率多高- 和看
Young GC频率是一样的,可以看高峰时期某几次的平均值。Full GC是很耗时的,频率我们最好控制在一天1次或者几天一次,特别是时效性要求较高的系统,一定要减少Full GC次数
- 和看
一次
Full GC的耗时- 可以取平均值,也可以取某一段的。我们会发现
Full GC的耗时是Young GC的好多倍
- 可以取平均值,也可以取某一段的。我们会发现
| 列名 | 描述 |
|---|---|
| S0C | From Survivor 区大小 |
| S1C | To Survivor 区大小 |
| S0U | 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 总耗时 |
G1与ParNew + CMS如果是
4核8G的机器,尽量还是用ParNew + CMS垃圾回收器,如果是大内存机器,就用G1如果系统使用的机器是大内存,
16G、32G,那每次GC都要等Eden区放满了才会执行垃圾回收,一次回收几个G的垃圾,速度就很慢,这个时候就必须要用G1了ParNew + CMS可以优化到极致,极致到没有Full GC只有Young GC。对G1的优化只能是尽可能的优化预订停顿时间,其他的没法参与太多,连什么时候Young GC我们都不确定G1内存使用率没有ParNew + CMS高,G1有这么一个机制,如果G1的某一个Region存活对象达到了85%,那就不会去回收这个Region,那15%如果是垃圾也回收不掉G1掌控性没有ParNew + CMS好,ParNew + CMS可以确定多久Young GC,对象增长速度等。G1什么时候垃圾回收我们都不知道,如果不是几个G的内存泄露我们也很难察觉到
G1介绍G1把堆内存平均分成多个相同大小的Region,我么首先要设置堆内存的大小,G1根据堆大小除以2048,分成2048个大小相同的RegionG1也有年轻代、老年代的概念,但只是概念,G1里的年轻代和老年代都是基于Region的,某些Region属于年轻代,某些Region属于老年代,由G1动态控制属于年轻代的
Region并不永远都是年轻代,如果年轻代Region被回收了,下次这个Region可能就存放老年代的数据了G1年轻代和老年代是冬天的,但是有上线,系统刚开始运行时,会分配5%的Region来存放对象,年轻代最多可以占用60%(默认),可以通过JVM参数指定,达到了目标值就会强制触发Young GCG1年轻代也分Eden和Survivor,G1整体使用的都是复制回收算法。某些Region属于Eden,某些Region属于Survivor,垃圾回收时就会把存活对象复制到Sruvivor中G1的一个特点就是可以设置预期停顿时间,也就是STW时间,比如通过JVM参数设置为5ms的停顿,那G1在垃圾回收时就会把时间控制在5ms以内G1的垃圾回收不一定是年轻代或老年代满了才回收。G1是基于每个Region的性价比去回收的。不会等到年轻代占用60%才去回收- 比如
Region1里的20M对象回收要2ms,Region2里的50M对象回收要4ms,如果我们设置的系统停顿时间为5ms,G1会在要求的时间内尽可能回收更多的对象,那么它会选择回收Region2
- 比如
G1中年轻代对象什么情况下会进入老年代,整体和ParNew + CMS差不多,只有大对象的处理不一样1、
Young GC存活对象Survivor放不下2、
Young GC存活对象达到Survivor50%,触发动态年龄审核3、对象到达了
15岁,进入老年代4、
G1中大对象不会进入老年代,专门有一部分Region用来存放大戏爱过你,如果一个Region放不下大对象,就会横跨多个Region存放
G1的Old GC也不是我们能控制的,G1会根据自己的判断进行回收,也是基于复制算法的G1的混合回收,如果老年代占比45%就会触发混合回收,回收整个堆内存。混合回收也会控制在我们设置的停顿时间范围内,如果时间不够就分多次回收第一步,初始标记
- 初始标记需要
STW,这一步只标记GC Root直接引用的对象,速度很快
- 初始标记需要
第二步,并发标记
- 和系统并行,深入追踪
GC Root,标记所有存活对象,此时系统新创建的对象会被JVM记录,这一步不需要STW
- 和系统并行,深入追踪
第三步,重新标记
- 重新标记第二步有改动的对象,需要
STW。因为只有一小部分改动,速度很快
- 重新标记第二步有改动的对象,需要
第四步,混合回收
这一步与
CMS不一样,CMS这里的回收与系统是并行的。G1的混合回收需要STW,混合回收不仅回收老年代,还会回收新生代对象和大对象根据我们设置的预期停顿时间,
G1分几批来回收,默认是8次,也就是分8次回收,可以通过JVM参数设置。还可以设置空间Region达到百分之多少,停止回收,默认是5%
G1何时会触发Full GC,其实G1的混合回收就相当于ParNew + CMS的Full GC了,因为回收了所有区域,只不过回收时间可控。但G1的Full GC就没法控制了,可能要卡顿很久才能回收完,G1的整体是基于复制算法的,如果回收过程中找不到可以复制的Region,放不下就会Full GC,开始单线程标记、清理、整理空闲出一批Region,这个过程很慢G1的优化我们可以参与的点很少,只能合理设置停顿时间,太小GC会太频繁,太大停顿时间太久也不好垃圾回收器的选择要根据不同场景具体去分析,没有统一的标准。只要不影响系统使用,没有可顿感,都是可以的。有些内部使用的系统,即使卡顿一会也无所谓,优化的话成本也在那,不做没必要的优化