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
,或者叫新生代GC
Old GC
:老年代的GC
Full GC
:全面回收整个堆内存,包括新生代、老年代、永久代Major GC
:用得很少,是一个比价混淆的概念,有些人把Major GC
和Old GC
等价起来,也有人把Major GC
和Full GC
等价起来。听到有人说Major GC
的概念,可以问问是想说Old GC
还是Full GC
Mixed GC
:G1
中特有的概念,一旦老年代占据堆内存的45%
就会触发Mixed GC
,对年轻代和老年代都会进行回收
频繁出现
Full GC
频繁出现
Full GC
的几种情况1、内存分配不合理,导致
Survivor
区放不下,或者触发动态年龄审核机制,存活对象频繁进入老年代2、内存泄露问题,导致老年代大部分空间被占用,无法回收掉,每次年轻代晋升一点点对象都放不下,触发
Full GC
3、大对象,一般代码层面问题,创建太多大对象直接放入老年代,大对象过多导致频繁触发
Full GC
4、永久代满了,触发
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
多少对象存活
jstat
jstat -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
个大小相同的Region
G1
也有年轻代、老年代的概念,但只是概念,G1
里的年轻代和老年代都是基于Region
的,某些Region
属于年轻代,某些Region
属于老年代,由G1
动态控制属于年轻代的
Region
并不永远都是年轻代,如果年轻代Region
被回收了,下次这个Region
可能就存放老年代的数据了G1
年轻代和老年代是冬天的,但是有上线,系统刚开始运行时,会分配5%
的Region
来存放对象,年轻代最多可以占用60%
(默认),可以通过JVM
参数指定,达到了目标值就会强制触发Young GC
G1
年轻代也分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
存活对象达到Survivor
50%
,触发动态年龄审核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
会太频繁,太大停顿时间太久也不好垃圾回收器的选择要根据不同场景具体去分析,没有统一的标准。只要不影响系统使用,没有可顿感,都是可以的。有些内部使用的系统,即使卡顿一会也无所谓,优化的话成本也在那,不做没必要的优化