前言

JVM是在不同操作系统上安装与之对应的JDK后所虚拟出的一台计算机,该计算机上可以运行任意 .class 字节码文件

官方地址
在这里插入图片描述

ClassLoader

负责将类的字节码加载到JVM内存中,并在运行时动态地链接和初始化类

类的加载步骤

在这里插入图片描述

1、把 .java 文件编译成 .class 文件
2、将.class 文件中的【静态数据结构】加载到【方法区中的运行时数据结构】
    再在【堆】中生成一个该类的【Class对象】作为访问方法区中运行时数据的入口
3、触发创建实例对象
	隐士加载:使用new关键字、调用静态属性【变量或方法】、子类加载前会优先触发其父类的加载
	显示加载:反射。运行期间动态获取类的元信息并调用对象的属性和方法

Java中有两种对象:Class对象(一个类只有一个Class对象)和实例对象(一个类可以通过Class对象new出多个实例对象)

类的加载器

在这里插入图片描述

每个加载器分别加载不同作用范围的jar包:
1、启动类加载器主要负责核心类库的加载[ **jre/lib/rt.jar和jre/lib/resource.jar等等** ]
2、扩展类加载器主要负责[ **jre/lib/ext** ]目录下的jar包和class文件
3、应用程序类加载器主要负责当前应用[ **classpath** ]目录下的所有jar包
4、自定义类加载器主要负责加载开发者自定义的类

类加载原则[双亲委派]

在这里插入图片描述

依次向上委派给父类加载器执行加载:
	加载成功,则直接返回,避免了类的重复加载且保证核心类库中的类不被破坏
	加载失败,再委派给子类加载器加载,直到最底层的类加载器

类的卸载条件

类需要同时满足下面 3 个条件才能算是 “无用的类” :
a-该类所有的实例都已经被回收
b-加载该类的 ClassLoader 已经被回收
c-该类对应的 java.lang.Class 对象没有在任何地方被引用,无法任何地方通过反射访问该类的方法

注意:静态变量的生命周期取决于类的生命周期。当Class对象被销毁时,静态变量也会被销毁

JVM内存模型

32位处理器下,一般Windows系统为1.5G-2G,Linux系统为2G-3G,而64位以上的处理器就不会有限制
默认新生代(Young)与老年代(Old) 的比例的值为 1:2(通过参数 -XX:NewRatio 指定),即新生代1/3 的堆空间大小,老年代2/3的堆空间大小
JVM初始分配内存由-Xms指定,默认物理内存1/64,最大分配内存由-Xmx指定,默认物理内存1/4
默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制
总结:服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小

运行时数据区

方法区(内存公有)

JDK1.7对其实现的是永久代,保存在堆中
JDK1.8对其实现的是本地元空间,保存在本地内存中

方法区是JVM中定义的一种规范,主要存放已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
方法区是各个线程共享的内存区域

方法区存储的是每个class的信息:
1.类加载器引用(classLoader)
2.运行时常量池:所有常量、字段引用、方法引用、属性
3.字段数据:每个方法的名字、类型(如类的全路径名、类型或接口) 、修饰符(如public、abstract、final)、属性
4.方法数据:每个方法的名字、返回类型、参数类型(按顺序)、修饰符、属性
5.方法代码
每个方法编译后代码、操作数栈大小、局部变量大小、局部变量表、异常表和每个异常处理的开始位置、结 束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

堆(内存公有)

堆是JVM所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享

栈(内存私有)

随着线程的创建而创建,用于存储方法调用的局部变量、操作数栈等。线程每执行到一个方法时便创建一个栈帧入栈,当方法执行完毕栈帧也随之出栈,所有方法执行完毕线程也就被释放,栈也就随之释放

程序计数器

一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU来调度的。假如线程A在执行到某个地方时突然失去了CPU的执行权,当线程A再获得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到的位置,用于存储当前线程执行的字节码指令地址

本地方法栈

用于存储调用本地方法的相关数据

在这里插入图片描述
新创建的对象都会被分配到Eden区【一些特殊的大对象会直接分配到Old区】,当新生带满了以后会触发Minor GC,存活下来的对象会被分到幸存区(S1或S1),如果一个对象在幸存区的S1或S1中反复存活了16次,则会被转移到老年代,如果老年代满了则会触发Magor GC【对Old区进行回收】 或 Full GC(同时对Young区和Old区进行回收)

【新生带中Eden:S1:S2的内存分配比例为 8:1:1】
据IBM公司研究表明,新生代中的对象98%都是“朝生夕死”的,所以Survivor区的出现有效的减低了新生带中Eden区中每进行一次Minor GC后进入Old区的对象数量,进而减少Magor GC 或 Full GC的发生

1、对于某个对象而言,只要程序中持有该对象的引用,则说明该对象不是垃圾,否则该对象会被标记为垃圾
   缺点:如果两个对象相互引用,则会导致永远不被回收
2、通过GC Root的对象,开始向下寻找,看某个对象是否可达,如果不可达则会被标记为垃圾
1、方法区空间不够用时
2、当Eden区或者Survivor区不够用时
3、老年代空间不够用时
4、手动调用System.gc(),不建议这么做,对资源消耗比较大

垃圾收集算法

标记复制

将内存划分为两块相等的区域,当其中一块内存使用完了,就将还存活的对象复制到另外一块区域
在这里插入图片描述
然后把已经使用过的内存空间一次性清除,这样做的好处是清理速度快,但对内存空间的浪费太大
在这里插入图片描述

标记清除

对堆中所有的对象进行扫描,标记出需要回收的对象,最后再统一删除,全部过程比较耗时,清理结束后会产生大量的空间碎片

标记整理

标记过程与"标记-清除"一样,但是后续步骤不是直接对可回收对象进行清理
在这里插入图片描述
而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
在这里插入图片描述

分代收集

根据对象存活周期的不同将内存划分为新生代和老年代,根据各个年代的特点采用最适当的收集算法
新生代中的对象生命周期很短,采用复制算法进行垃圾收集,只需要付出少量存活对象的复制成本就可以完成收集
老年代中的对象存活率较高,没有额外的空间对它进行分配担保,所以必须使用”标记-整理“算法来进行回收

垃圾收集器

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1

Serial 和 Serial Old

Serial收集器对象为新生带,采用"标记-复制"算法,采用单线程工作模式,收集时需要暂停所有用户线程
ParNew是Serial收集器的多线程版本
Serial Old是Serial收集器的老年代版本,采用"标记-整理"算法,采用单线程工作模式,收集时需要暂停所有用户线程

在这里插入图片描述

Parallel Scavenge 和 Parallel Old

Parallel Scavenge收集器对象为新生带,采用"标记-复制"算法,采用多线程工作模式,相比于ParNew更关注系统的吞吐量
Parallel Old是Parallel Scavenge收集器的老年代版本,采用"标记-整理"算法,采用多线程工作模式,更加关注系统的吞吐量

CMS(JDK1.8)

官网

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,采用的是标记-清除算法,缺点是:垃圾回收后会产生空间碎片,需要通过【-XX:CMSFullGCsBeforeCompaction】参数进行优化来设置执行N次的Full GC之后对空间碎片进行依次压缩
在这里插入图片描述
由于整个过程中,并发的进行标记和清除,收集器线程可以与用户线程一起工作

G1(JDK1.9)

官网

将整个堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合

1、分代收集(仍然保留了分代的概念)
2、空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
3、可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)

在这里插入图片描述

如何选择垃圾收集器

优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
如果允许停顿时间超过1秒,选择并行或JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器

在这里插入图片描述

JVM参数

# 新生代使用UseParallelGC(吞吐量优先)
-XX:+UseParallelGC	

# 老年代使用UseParallelOldGC(吞吐量优先)
-XX:+UseParallelOldGC

# 老年代使用CMS(停顿时间优先)
-XX:+UseConcMarkSweepGC	

# 新生代和老年代使用G1GC(停顿时间优先)
-XX:+UseG1GC
# G1最大停顿时间。太小会导致G1跟不上垃圾产生的速度,最终退化成FullGC,所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态
-XX:MaxGCPauseMillis=200ms
# 设置本地原空间大小	
-XX:MetaspaceSize=50M
-XX:MaxMetaspaceSize=50M

# 设置堆大小
-XX:InitialHeapSize=100M
-XX:MaxHeapSize=100M

# 设置年轻代的大小
-XX:NewSize=20M
-XX:MaxNewSize=50M

# 设置老年代大小
-XX:OldSize=50M	

# 新老生代的比值。比如-XX:Ratio=4则表示新生代:老年代=1:4,也就是新生代占整个堆内存的1/5
-XX:NewRatio
# 设置每个线程的堆栈大小(经验值是3000-5000最佳)
-Xss128k	

JVM分析工具(jps/jmap/jstat/jstack)

查看进程PID

jps -l
14512 com.snoob.springboot.BffControllerApplication

查看进程属性值[单位Byte]

原空间

jinfo -flag MetaspaceSize 14512
jinfo -flag MaxMetaspaceSize 14512

jinfo -flag InitialHeapSize 14512
jinfo -flag MaxHeapSize 14512

新生代

jinfo -flag NewSize 14512
jinfo -flag MaxNewSize 14512 

老年代

jinfo -flag OldSize 14512

垃圾收集器

jinfo -flag UseParallelGC 14512
-XX:+UseParallelGC # + 表示使用

jinfo -flag UseParallelOldGC 14512
-XX:+UseParallelOldGC # + 表示使用

jinfo -flag UseConcMarkSweepGC 14512
-XX:-UseConcMarkSweepGC # - 表示没有使用

jinfo -flag UseG1GC 14512
-XX:-UseG1GC # - 表示没有使用

查看JVM配置和使用情况

jmap -heap 14512
Heap Configuration:
   【备注:初始元空间,替代1.8以前的Permgen】
   MetaspaceSize            = 21807104 (20.796875MB) 
   【备注:最大元空间,调整元空间会Full GC,一般将MetaspaceSize和MaxMetaspaceSize设置一样,防止修改时触发FGC】
   MaxMetaspaceSize         = 17592186044415 MB 
   
   MinHeapFreeRatio         = 0 【备注:最小堆使用比例】
   MaxHeapFreeRatio         = 100【备注:最大堆使用比例】
   MaxHeapSize              = 104857600 (100.0MB)  【备注:最大堆空间】
  
  【备注:表示新生代:老年代=1:2,也就是新生代占整个堆内存的1/3】
   NewRatio                 = 2 
  【备注:表示Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10】
   SurvivorRatio            = 8 
   
   NewSize                  = 34603008 (33.0MB)
   MaxNewSize               = 34603008 (33.0MB)
   OldSize                  = 70254592 (67.0MB)

   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   G1HeapRegionSize         = 0 (0.0MB) 
   【备注:G1垃圾回收算法时,JVM会将Heap空间分隔为若干个Region,该参数用来指定Region的大小】

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 17825792 (17.0MB)
   used     = 14837016 (14.149681091308594MB)
   free     = 2988776 (2.8503189086914062MB)
   83.2334181841682% used
From Space:
   capacity = 7864320 (7.5MB)
   used     = 851968 (0.8125MB)
   free     = 7012352 (6.6875MB)
   10.833333333333334% used
To Space:
   capacity = 8388608 (8.0MB)
   used     = 0 (0.0MB)
   free     = 8388608 (8.0MB)
   0.0% used
   
PS Old Generation
   capacity = 47710208 (45.5MB)
   used     = 13607144 (12.976783752441406MB)
   free     = 34103064 (32.523216247558594MB)
   28.520403851519575% used

监控 堆 和 JVM垃圾回收情况

jstat -gcutil PID
S0/S1:幸存者区
E:伊甸园区,初生区
0:老年代
M:元空间
YGC:年轻带回收次数
YGCT:年轻带回收花费时间(秒)
FGC:full GC
FGC:full GC花费时间(秒)
GCT:GC总时间(秒)

查看指定线程的堆栈信息

top # 查询消耗CPU最多的(进程)PID
top -Hp PID # 根据PID查询消耗(线程)CPU最多的PID
printf '%x\n' PID # 将PID转化成16进制数字2ac
jstack PID | grep 2ac -A 100 # 查询进程号7下的线程运行日志

JVM可视化工具

jmc.exe

jconsole.exe

jvisualvm.exe

JVM性能优化

java -jar \
-XX:MetaspaceSize=100M \
-Xms300M \
-Xmx300M \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \ # 设置最大GC停顿时间指标
-XX:InitiatingHeapOccupancyPercent=45 \ # 堆内存使用率大于45%时触发GC
demo.jar

常见调优

JVM调优没有固定的参数参考,需要根据不同的服务器和系统需求来调整,默认JVM是会自动优化的,所以调整JVM的操作不到万不得已不建议

不要手动设置新生代和老年代的大小【G1收集器在运行过程中,会自己调整新生代和老年代的大小】
根据项目运行情况,增加适当的堆内存,注意:最小堆内存和最大堆内存设置一致【避免JVM频繁的切换堆内存】
不断调优暂停时间目标【一般设置100ms或者200ms都可以,但如果设置太短就会导致G1跟不上垃圾产生的速度,最终退化成Full GC】
使用-XX:ConcGCThreads=n来增加标记线程的数量
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐