JVM
1 概览
同时参考 小林 conding
1.1 JVM 流程
1.2 目录
2 JVM 组成
2.1 程序计数器
线程私有的,每个线程一份,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
javap -v xx.class 打印堆栈大小,局部变量的数量和方法的参数。
2.2 Java 堆
线程共享的区域:主要用来保存 对象实例,数组 等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出 OutOfMemoryError 异常。
组成:年轻代 + 老年代
年轻代被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区,老年代主要保存生命周期长的对象,一般是一些老的对象
在 JDK 1.8 及以后的版本中,方法区(永久代)被元空间取代,使用本地内存,因为方法区的数据越来越多,防止内存溢出。
2.3 虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出。
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
2.3.1 常见问题
-
垃圾回收是否涉及栈内存?
栈内存不被 GC 回收,垃圾回收主要指的是堆内存,当栈帧弹栈以后,内存就会释放。 -
栈内存分配越大越好吗?
未必,默认的栈内存通常为 1024k,栈帧过大会导致线程数变少,例如,机器总内存为 512m,目前能活动的线程数则为 512 个,如果把栈内存改为 2048k,那么能活动的栈帧就会减半。
-
方法内的局部变量是否线程安全?
-
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
-
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。
-
-
栈内存溢出情况
-
栈帧过多导致栈内存溢出,典型问题:递归调用
-
栈帧过大导致栈内存溢出
-
-
堆和栈的区别
- 栈主要用于存储局部变量、方法调用的参数、方法返回地址以及一些临时数据,堆用于存储对象的实例(包括类的实例和数组)。
- 栈中的数据对线程是私有的,每个线程有自己的栈空间。堆中的数据对线程是共享的,所有线程都可以访问堆上的对象。
- 栈中的数据具有确定的生命周期,当一个方法调用结束时,其对应的栈帧就会被销毁,栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定,对象会在垃圾回收机制(Garbage Collection, GC)检测到对象不再被引用时才被回收。
2.4 方法区
方法区(MethodArea)是各个线程 共享的内存区域,主要存储 类的信息、运行时常量池。虚拟机启动的时候创建,关闭虚拟机时释放。如果方法区域中的内存无法满足分配请求,则会抛出 OutOfMemoryError: Metaspace
2.4.1 常量池
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
javap -v Application.class # 查看字节码结构(类的基本信息、常量池、方法定义)
常量池是 .class 文件中的,当该类被加载,它的常量池信息就会 *放入运行时常量池**,并把里面的符号地址变为真实地址
2.5 直接内存
下图所示在常规 IO 的数据拷贝流程中,因为 Java 不能直接访问数据,所以数据在内存中存储了两份。
NIO 的做法
- 直接内存并不属于 JVM 中的内存结构,不由 JVM 进行管理,是虚拟机的系统内存
- 常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理
3 类加载器
JVM 只会运行二进制文件,类加载器的作用就是将字节码文件加载到 JVM 中,从而让 Java 程序能够启动起来。
3.1 类加载器的种类
- 启动类加载器(Bootstrap Class Loader):最顶层的类加载器,负责加载 Java 的核心库(如位于 jre/lib/rt.jar 中的类),用 C++ 编写,是 JVM 的一部分。启动类加载器无法被 Java 程序直接引用。
- 扩展类加载器(Extension Class Loader): Java 语言实现,继承自 ClassLoader 类,负责加载 Java 扩展目录(jre/lib/ext 或由系统变量 Java.ext.dirs 指定的目录)下的 jar 包和类库。扩展类加载器由启动类加载器加载,并且父加载器就是启动类加载器。
- 系统类加载器(System Class Loader)/ 应用程序类加载器(Application Class Loader): Java 语言实现,负责加载用户类路径(ClassPath)上的指定类库,是平时编写 Java 程序时默认使用的类加载器。系统类加载器的父加载器是扩展类加载器。它可以通过 ClassLoader.getSystemClassLoader()方法获取到。
- 自定义类加载器(Custom Class Loader):开发者可以根据需求定制类的加载方式,比如从网络加载 class 文件、数据库、甚至是加密的文件中加载类等。自定义类加载器可以用来扩展 Java 应用程序的灵活性和安全性,是 Java 动态性的一个重要体现。
这些类加载器之间的关系形成了 双亲委派模型。
3.2 双亲委派
核心思想是当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
举个例子:自己写了一个 java.lang.String 类,当 AppClassLoader 要加载它时,会先委派给 Extension ClassLoader,再委派给 Bootstrap ClassLoader。而启动类加载器发现自己已经加载过 JDK 自带的 String 类了,就直接返回这个类,不会去加载自定义的 String 类。
JVM 采用双亲委派机制的两个核心原因:
- 保证类的唯一性和安全性:避免同一个类被不同加载器重复加载,确保不会被篡改。
- 实现类的复用:核心类只需要被顶层加载器加载一次,所有子加载器都能共享这个类,减少内存消耗。
3.2 类加载过程
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段,其中,验证、准备和解析这三个部分统称为连接(linking)。
-
加载:通过类的全限定名(包名 + 类名),获取到该类的 .class 文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的 Java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
-
连接
-
验证:确保 class 文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的 class 类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证。
-
准备:为类中的静态字段分配内存,并设置默认的初始值。
-
解析:将常量池的「符号引用」直接替换为「直接引用」的过程。
-
-
初始化:对类的 静态变量,静态代码块 执行初始化操作,简单来说就是执行类的构造器方法,要注意的是这里的构造器方法并不是开发者写的,而是编译器自动生成的。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
-
使用:使用类或者创建对象。
-
卸载:一个类要被 JVM 卸载,条件非常苛刻,需要同时满足以下三点:
- 该类所有的实例都已经被回收:这是最显而易见的前提。如果堆中还存在这个类的任何一个实例对象,那么定义这个对象的 Class 对象肯定不能被卸载。
- 加载该类的 ClassLoader 已经被回收:这是 最关键也是最难满足的条件。类与其加载器是双向绑定的共生关系。一个类由哪个类加载器加载,这个信息是存储在 Class 对象里的。要卸载一个类,必须先卸载加载它的类加载器。
- 类对应的 Java.lang.Class 对象没有任何地方被引用:不能在任何地方通过反射(如静态字段、全局变量)、静态变量、JNI 等途径引用到这个 Class 对象。一旦这个 Class 对象还存在强引用,GC 就不会回收它,那么这个类也就不会被卸载。
4 垃圾回收
垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。
4.1 判断垃圾的方法
4.1.2 引用计数法
- 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加 1;当引用失效时,计数器减 1。当计数器为 0 时,表示对象不再被任何变量引用,可以被回收。
- 缺点:不能解决 循环引用 的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为 0,导致对象无法被回收。
4.1.3 可达性分析
Java 虚拟机主要采用此算法来判断对象是否为垃圾。
原理:从一组称为 GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。
4.2 垃圾回收算法
4.2.1 标记清除算法
先标记所有可回收对象,然后直接清理它们的内存。但这样会产生很多内存碎片,后续分配大对象时可能找不到连续空间。
4.2.2 标记整理算法
为了解决碎片问题,有 “标记 - 整理”:标记后不是直接清除,而是把存活对象往一端移动,然后清理边界外的内存,这样内存更整齐,但移动对象会有性能开销。
4.2.3 复制算法
把内存分成两块,每次只用其中一块,回收时把存活对象复制到另一块,然后清空当前块。这种方式没有碎片,但会浪费一半内存,适合存活对象少的场景(比如新生代)。
实际中 JVM 会分代使用这些算法。因为对象的生命周期不同,新生代(刚创建的对象)存活率低,老年代(存活久的对象)存活率高。新生代一般用 “复制算法”:分成 Eden 区和两个 Survivor 区(比如 8:1:1),新对象先放 Eden,满了就触发 Minor GC,把存活对象复制到一个 Survivor,多次存活后移到老年代。老年代用 “标记 - 清除” 或 “标记 - 整理”,因为对象存活率高,复制成本太高。
4.2.4 分代收集算法
上面提到垃圾回收时会分为新生代和老年代,下面详细说明垃圾回收时他们的作用。
在 Java8 中,堆被分为了两份:新生代和老年代 [1:2]。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX: MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。
4.3 垃圾回收器
-
串行垃圾回收器:Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器的老年代版本;
垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
-
并行垃圾回收器:ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;Parallel Old 收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge 收集器的老年代版本;JDK8 默认使用这两个。
-
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。
4.3.1 G1 垃圾回收器
G1(Garbage First) 收集器 (标记-复制算法 [二者混合使用]): Java 堆并行收集器,G1 收集器是 JDK1.7 提供的一个新收集器,JDK 9 起成为默认 GC。G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范围是整个 Java 堆(包括新生代,老年代 [不分代]),划分成多个区域,每个区域都可以充当 eden,survivor,old,humongous(专为大对象准备),回收时分成三个阶段:新生代回收、并发标记、混合收集,如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC。
下面详细解释这三个流程:
-
新生代回收
初始时,所有区域都处于空闲状态,创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象,当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程(stop the world)。
随着时间流逝,伊甸园的内存又有不足,将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象普升至老年代。
-
Young Collection + Concurrent Mark (新生代垃圾回收+并发标记)
当老年代占用内存超过间值(默认是 45%)后,触发并发标记,这时无需暂停用户线程
-
混合回收
并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入 混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间自标 优先回收价值高(存活对象少)的区域(这也是 GabageFirst 名称的由来)。混合收集阶段中,参与复制的有 eden、survivor、old。
复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集。
4.4 minorGC、majorGC、fullGC
在 Java 中,垃圾回收机制是自动管理内存的重要组成部分。根据其作用范围和触发条件的不同,可以将 GC 分为三种类型:Minor GC(也称为 Young GC)、Major GC(有时也称为 Old GC)、以及 Full GC。以下是这三种 GC 的区别和触发场景:
Minor GC (Young GC)
- 作用范围:只针对年轻代进行回收,包括 Eden 区和两个 Survivor 区(S0 和 S1)。
- 触发条件:当 Eden 区空间不足时,JVM 会触发一次 Minor GC,将 Eden 区和一个 Survivor 区中的存活对象移动到另一个 Survivor 区或老年代(Old Generation)。
- 特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。
Major GC
- 作用范围:主要针对老年代进行回收,但不一定只回收老年代。
- 触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发 Major GC。
- 特点:相比 Minor GC,Major GC 发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。
Full GC
- 作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。
- 触发条件:
- 直接调用
System.gc()或Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但 JVM 会尝试执行 Full GC。 - Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发 Full GC,对整个堆内存进行回收。
- 当永久代(Java 8 之前的版本)或元空间(Java 8 及以后的版本)空间不足时。
- 直接调用
- 特点:Full GC 是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少 Full GC 的触发。
4.5 强引用、软引用、弱引用、虚引用
-
强引用:只有所有 GCRoots 对象都不通过【强引|用】引 I 用该对象,该对象才能被垃圾回收
User u = new User();
-
软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收
User user = new User(); SoftReference softReference = new SoftReference(user);
-
弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
User user = new User();: WeakReference weakReference = new WeakReference(user);
-
虚引用:必须配合引 l 用队列使用,被引 l 用对象回收时,会将虚引 l 用入队,由 ReferenceHandler 线程调用虚引 l 用相关方法释放直接内存
User user = new User(); ReferenceQueue referenceQueue = new ReferenceQueue(); PhantomReference phantomReference = new PhantomReference(user,queue);
5 JVM 实践
5.1 JVM 调优参数
5.1.1 配置参数
-
war 包部署在 tomcat 中设置
修改
TOMCAT_HOME/bin/catalina.sh文件
-
jar 包部署在启动参数设置
通常在 linux 系统下直接加参数启动 springboot 项目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &nohup:用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行参数
&:让命令在后台执行,终端退出后命令仍旧执行。
5.1.2 有哪些参数
对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。
https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html
-
设置堆空间大小
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值。
-Xms:设置堆的初始化大小 -Xmx:设置堆的最大大小 -Xms : 1024 -Xms : 1024k # 不指定单位默认为字节,指定单位,按照指定的单位设置 -Xms : 1024m -Xms : 1g堆空间设置多少合适?
- 最大大小的默认值是物理内存的 1/4,初始大小是物理内存的 1/64
- 堆太小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生 stw,暂停用户线程
- 堆内存大肯定是好的,存在风险,假如发生了 fullgc,它会扫描整个堆空间,暂停用户线程的时间长
- 设置参考推荐:尽量大,也要考察一下当前计算机其他程序的内存使用情况
-
设置虚拟机栈
虚拟机栈的设置:每个线程默认会开启 1M 的内存,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
-Xss 对每个线程stack大小的调整 -Xss 128k -
年轻代中 Eden 区和两个 Survivor 区的大小比例
设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。通过增大 Eden 区的大小,来减少 YGC 发生的次数,但有时我们发现,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优。-XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden=2:8 -
年轻代晋升老年代阀值
-XX:MaxTenuringThreshold=threshold默认为 15,取值范围 0-15
-
设置垃圾回收器
通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。
-XX:+UseParalleIGC -XX:+UseParallelOldGC
5.2 JVM 调优工具
5.2.1 命令行
-
jps:进程状态信息
-
jstack:查看 Java 进程内线程的堆栈信息
jstack [option] < pid >
-
jmap:用于生成堆转内存快照、内存使用情况
jmap -heap pid # 显示Java堆的信息 jmap -dump:format=b,file=heap.hprof pidformat = b 表示以 hprof 二进制格式转储 Java 堆的内存
file = < filename > 用于指定快照 dump 文件的文件名。
例如:
C:\Userslyuhon>jmap -heap 53280 Attaching to process ID 53280, please wait..Debugger attached successfully. Server compiler detected. JVM version is 25.321-b07 using thread-local object allocation. Parallel GC with 8 thread(s) # 并行的垃圾回收器 Heap Configuration: # 堆配置 MinHeapFreeRatio = 0 # 空闲堆空间的最小百分比 MaxHeapFreeRatio = 100 # 空闲堆空间的最大百分比 MaxHeapSize = 178257920(170.0MB)# 新生代堆空间的默认值 NewSize = 8524922880(8130.0MB)# 堆空间允许的最大值 MaxNewSize = 2841640960(2710.0MB)# 新生代堆空间允许的最大值 Oldsize = 356515840(340.0MB)# 老年代堆空间的默认值 NewRatio = 2# 新生代与老年代的堆空间比值,表示新生代:老年代=1:2 SurvivorRatio = 8# 两个survivor区和Eden区的堆空间比值为8,表示so:S1:Eden=1:1:8 Metaspacesize = 21807104(20.796875MB)# 元空间的默认值 CompressedClassSpaceSize =1073741824(1024.0MB) # 压缩类使用空间大小 MaxMetaspaceSize = 175921860444151MB # 元空间允许的最大值 G1HeapRegionSize = 0(O.OMB)# 在使用 G1 垃圾回收算法时,JVM 会将 Heap 空间分隔为若干个 Region,该参数用来指定每个 Region 空间的大小 Heap Usage: PS Young Generation Eden Space:# Eden使用情况 capacity = 134217728 (128.0MB) used = 10737496(10.240074157714844MB) free = 123480232 (117.75992584228516MB) 8.000057935714722% used From Space:# Survivor-From 使用情况 capacity = 22020096 (21.0MB) used = 0(0.0MB) free = 22020096(21.0MB) 0.0% used To Space:capacity # Survivor-To使用情况 capacity = 22020096(21.0MB) used = 0(0.0MB) free = 22020096(21.0MB) 0.0% used PS oid Generation # 老年代 使用情况 capacity = 356515840 (340.0MB) used = 0(0.0MB) free = 356515840(340.0MB) 0.0% used 3185 interned Strings occupying 261264 bytes它是一个进程或系统在某一给定的时间的快照。比如在进程崩溃时,甚至是任何时候,我们都可以通过工具将系统或某进程的内存备份出来供调试分析用。dump 文件中包含了程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据,方便系统技术人员进行错误排查。
例如:
jmap -dump:format=b,file=D:/learn_Java/demo.hprof 24612 -
jstat:是 JVM 统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等
5.2.2 可视化
-
jconsole:用于对 jvm 的内存,线程,类的监控,是一个基于 jmx 的 GUI 性能监控工具
打开方式:java 安装目录 bin 目录下直接启动 jconsole.exe 就行。例如:D:\learn_Java\jdks\ms-17.0.17\bin\jconsole.exe -
VisualVM:能够监控线程,内存情况,查看方法的 CPU 时间和内存中的对象,已被 GC 的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin 目录下直接启动 jvisualvm.exe 就行。但是只有 JDK 8 内置,以上版本需要官网上下载。
5.3 内存泄漏排查思路 ⭐
通过 jmap 指定打印他的内存快照 dump(Dump 文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到 dump 文件中)
-
使用 jmap 命令获取运行中程序的 dump 文件
jmap -dump: format = b, file = heap.hprof pid -
使用 vm 参数获取 dump 文件
有的情况是内存溢出之后程序则会直接中断,而 jmap 只能打印在运行中的程序,所以建议通过参数的方式的生成 dump 文件-XX:+HeapDumpOnOutOfMemoryError -XX: HeapDumpPath =/home/app/dumps/
生成的 **.hprof 文件可以使用 VisualVM 打开。通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题,找到对应的代码,通过阅读上下文的情况,进行修复即可。
5.4 CPU 飙高的排查方案
- top 查看进程使用情况,发现 2266 进程占用 CPU 使用率高。
- 打印
2266进程的所有线程。

-
使用
jstack 2266查看进程内线程的堆栈信息,但 jstack 内输出的是十六进制的线程信息,2276 的十六进制为 0x8e4。

浙公网安备 33010602011771号