1.JVM 简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运⾏在⼀个完全隔离的环境中的完整计算机系统。
常⻅的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进 ⾏了裁剪。 JVM 是⼀台被定制过的现实当中不存在的计算机。
2. JVM 运行流程
JVM 是 Java运行的基础,也是实现⼀次编译到处执⾏的关键,那么 JVM 是如何执⾏的呢?
JVM 执⾏流程
程序在执⾏之前先要把java代码转换成字节码(class⽂件),JVM ⾸先需要把字节码通过⼀定的⽅式 类加载器(ClassLoader)
把⽂件加载到内存中 运⾏时数据区(Runtime Data Area) ,⽽字节码⽂ 件是 JVM的⼀套指令集规范,并不能直接交个底层操作系统去执⾏,因此需要特定的命令解析器 **执 ⾏引擎(Execution Engine)**将字节码翻译成底层系统指令再交由CPU去执⾏,⽽这个过程中需要 调⽤其他语⾔的接⼝ 本地库接⼝(Native Interface) 来实现整个程序的功能,这就是这4个主要组成 部分的职责与功能。
总结来看, JVM 主要通过分为以下 4 个部分,来执⾏ Java 程序的,它们分别是:
- 类加载器(ClassLoader)
- 运⾏时数据区(Runtime Data Area)
- 执⾏引擎(Execution Engine)
- 本地库接⼝(Native Interface)
3. JVM 运⾏时数据区
JVM 运⾏时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简
称 JMM)完全不同,属于完全不同的两个概念,它由以下 5 ⼤部分组成(JVM就是Java进程,这个进程一旦跑起来,就会从操作系统这里,申请一大块内存空间,如下):
3.1 堆(线程共享)(成员变量)
堆的作用:程序中创建的所有对象都在保存在堆中。(new出来的对象,如成员变量)
3.2 Java虚拟机栈(线程私有)(局部变量)
Java 虚拟机栈的作用:Java 虚拟机栈的⽣命周期和线程相同,Java 虚拟机栈描述的是 Java ⽅法执⾏的内存模型:每个⽅法在执⾏的同时都会创建⼀个栈帧(StackFrame)⽤于存储局部变量表、操作数栈、动态链接、⽅法出⼝等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈
Java 虚拟机栈中包含了以下 4 部分:
- 局部变量表: 存放了编译器可知的各种基本数据类型(8⼤基本数据类型)、对象引⽤。局部变量表 所需的内存空间在编译期间完成分配,当进⼊⼀个⽅法时,这个⽅法需要在帧中分配多⼤的局部变 量空间是完全确定的,在执⾏期间不会改变局部变量表⼤⼩。简单来说就是存放⽅法参数和局部变量。
- 操作栈:每个⽅法会⽣成⼀个先进后出的操作栈。
- 动态链接:指向运⾏时常量池的⽅法引⽤。
- ⽅法返回地址:PC 寄存器的地址。
什么是线程私有?
由于JVM的多线程是通过线程轮流切换并分配处理器执⾏时间的⽅式来实现,因此在任何⼀个确定的 时刻,⼀个处理器(多核处理器则指的是⼀个内核)都只会执⾏⼀条线程中的指令。因此为了切换线程后能恢复到正确的执⾏位置,每条线程都需要独⽴的程序计数器,各条线程之间计数器互不影响,独⽴ 存储。我们就把类似这类区域称之为"线程私有"的内存
3.3 本地方法栈(线程私有)
本地⽅法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使⽤的,⽽本地⽅法栈是给本地⽅法使⽤的。
3.4 程序计数器(线程私有)
程序计数器的作⽤:⽤来记录当前线程执⾏的⾏号的。
程序计数器是⼀块⽐较⼩的内存空间,可以看做是当前线程所执⾏的字节码的⾏号指⽰器。如果当前线程正在执⾏的是⼀个Java⽅法,这个计数器记录的是正在执⾏的虚拟机字节码指令的地址;如果正在执⾏的是⼀个Native⽅法,这个计数器值为空。程序计数器内存区域是唯⼀⼀个在JVM规范中没有规定任何OOM情况的区域!
3.5 方法区/元数据区(线程共享)(类对象)静态变量
⽅法区的作⽤:⽤来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的
4.JVM 类加载
1.加载(在硬盘上找到.class文件,读取文件内容)
2.验证(检查.class文件的内容是否符合要求)
3.准备(给类对象分配内存空间)
4.解析(针对字符串常量来进行初始化,把.class文件中的常量内容取出来,放到“元数据区”)
5.初始化(针对类对象进行初始化)
5.双亲委派模型
如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层 的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆ 法完成这个加载请求(它的搜索范围中没有找到所需 的类)时,⼦加载器才会尝试⾃⼰去完成加载。
这与工作时的一段经历非常相似:
先交给上一级去执行,没有上一级了就自己解决,自己没有找到这个类就让下一级去找,如此执行每一级都会查询到
双亲委派模型的优点
- 避免重复加载类:⽐如 A 类和 B 类都有⼀个⽗类 C 类,那么当 A 启动时就会将 C 类加载起来,那 么在 B 类进⾏加载时就不需要在重复加载 C 类了。
- 安全性:使⽤双亲委派模型也可以保证了 Java 的核⼼ API 不被篡改,如果没有使⽤双亲委派模 型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的 Object 类,⽽有些 Object 类⼜是⽤⼾⾃ ⼰提供的因此安全性就不能得到保证了
5.垃圾回收相关
上⾯讲了Java运⾏时内存的各个区域。对于程序计数器、虚拟机栈、本地⽅法栈这三部分区域⽽⾔,其⽣命周期与相关线程有关,随线程⽽⽣,随线程⽽灭。并且这三个区域的内存分配与回收具有确定性,因为当⽅法结束或者线程结束时,内存就⾃然跟着线程回收了。因此我们本节课所讲的有关内存分配和回收关注的为Java堆与⽅法区这两个区域。Java堆中存放着⼏乎所有的对象实例,垃圾回收器在对堆进⾏垃圾回收前,⾸先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下⼏种算法
gc回收机制回收的是内存,更准确的说回收的是对象,回收的是堆上的内存。程序计数器,元数据区,栈一般不需要额外回收,线程销毁了自然就销毁了
gc机制的两个流程:1.找到谁是垃圾2.清除这个垃圾,释放对应的内存
1.引⽤计数描述的算法:
给对象增加⼀个引⽤计数器,每当有⼀个地⽅引⽤它时,计数器就+1;当引⽤失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使⽤的,即对象已"死"。
引⽤计数法实现简单,判定效率也⽐较⾼,在⼤部分情况下都是⼀个不错的算法。⽐如Python语⾔就采⽤引⽤计数法进⾏内存管理。
但是,在主流的JVM中没有选⽤引⽤计数法来管理内存,最主要的原因就是引⽤计数法⽆法解决对象的循环引⽤问题
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进⾏垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
从结果可以看出,GC⽇志包含" 6092K->856K(125952K)",意味着虚拟机并没有因为这两个对象互相引⽤就不回收他们。即JVM并不使⽤引⽤计数法来判断对象是否存活。
b) 可达性分析算法
在上⾯我们讲了,Java并不采⽤引⽤计数法来判断对象是否已"死",⽽采⽤"可达性分析"来判断对象是否存活(同样采⽤此法的还有C#、Lisp-最早的⼀⻔采⽤动态内存分配的语⾔)。此算法的核⼼思想为 : 通过⼀系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索⾛过的路径称之为"引⽤链",当⼀个对象到GC Roots没有任何的引⽤链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可⽤的。以下图为例:
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
总结:引用计数描述算法和可达性分析就是对于垃圾进行标记
② 垃圾回收算法
通过上⾯的学习我们可以将死亡对象标记出来了,标记出来之后我们就可以进⾏垃圾回收操作了,在正式学习垃圾收集器之前,我们先看下垃圾回收机器使⽤的⼏种算法(这些算法是垃圾收集器的指导思想)。
a) 标记-清除算法
"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : ⾸先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象(标记过程⻅3.1.2章节)。后续的收集算法都是基于这种思路并对其不⾜加以改进⽽已。
"标记-清除"算法的不⾜主要有两个 :
- 效率问题 : 标记和清除这两个过程的效率都不⾼
- 空间问题 : 标记清除后会产⽣⼤量不连续的内存碎⽚,空间碎⽚太多可能会导致以后在程序运⾏中 需要分配较⼤对象时,⽆法找到⾜够连续内存⽽不得不提前触发另⼀次垃圾收集。
b) 复制算法
"复制"算法是为了解决"标记-清理"的效率问题。它将可⽤内存按容量划分为⼤⼩相等的两块,每次只使⽤其中的⼀块。当这块内存需要进⾏垃圾回收时,会将此区域还存活着的对象复制到另⼀块上⾯,然后再把已经使⽤过的内存区域⼀次清理掉。这样做的好处是每次都是对整个半区进⾏内存回收,内存分配时也就不需要考虑内存碎⽚等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运⾏⾼效。算法的执⾏流程如下图
c) 标记-整理算法
复制收集算法在对象存活率较⾼时会进⾏⽐较多的复制操作,效率会变低。因此在⽼年代⼀般不能使⽤复制算法。
针对⽼年代的特点,提出了⼀种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程⼀致,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活对象都向⼀端移动,然后直接清理掉端边界以外的内存。流程图如下
d) 分代算法
分代算法和上⾯讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从⽽实现更好的垃圾回收。这就好⽐中国的⼀国两制⽅针⼀样,对于不同的情况和地域设置更符合当地的规则,从⽽实现更好的管理,这就时分代算法的设计思想。
当前 JVM 垃圾收集都采⽤的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为⼏块。⼀般是把Java堆分为新⽣代和⽼年代。在新⽣代中,每次垃圾回收都有⼤批对象死去,只有少量存活,因此我们采⽤复制算法;⽽⽼年代中对象存活率⾼、没有额外空间对它进⾏分配担保,就必须采⽤"标记-清理"或者"标记-整理"算法。哪些对象会进⼊新⽣代?哪些对象会进⼊⽼年代?
• 新⽣代:⼀般创建的对象都会进⼊新⽣代;
• ⽼年代:⼤对象和经历了 N 次(⼀般情况默认是 15 次)垃圾回收依然存活下来的对象会从新⽣代
移动到⽼年代。
⾯试题 : 请问了解Minor GC和Full GC么,这两种GC有什么不⼀样吗
- Minor GC⼜称为新⽣代GC : 指的是发⽣在新⽣代的垃圾收集。因为Java对象⼤多都具备朝⽣夕灭 的特性,因此Minor GC(采⽤复制算法)⾮常频繁,⼀般回收速度也⽐较快。
- Full GC ⼜称为 ⽼年代GC或者Major GC : 指发⽣在⽼年代的垃圾收集。出现了Major GC,经常会伴 随⾄少⼀次的Minor GC(并⾮绝对,在Parallel Scavenge收集器中就有直接进⾏Full GC的策略选择 过程)。Major
GC的速度⼀般会⽐Minor GC慢10倍以上。
性能优化
垃圾回收调优
通过调整垃圾回收器和相关参数,可以优化应用程序的性能。常见的垃圾回收器有:
- Serial GC:适用于单线程环境。
- Parallel GC:适用于多线程环境,追求高吞吐量。
- CMS(Concurrent Mark-Sweep)GC:适用于低停顿时间要求的应用。
- G1(Garbage-First)GC:适用于大堆内存和低停顿时间要求的应用。
JIT编译器
即时编译器(Just-In-Time Compiler,JIT)是JVM执行引擎的重要组成部分。JIT编译器在程序运行时将字节码编译为本地机器码,从而提高执行效率。JVM常用的JIT编译器有以下几种:
- C1编译器(Client Compiler):适用于客户端应用,启动速度快,优化等级较低。
- C2编译器(Server Compiler):适用于服务器端应用,启动速度较慢,但优化等级高,适合长时间运行的应用程序。
JIT编译优化技术
JIT编译器使用多种优化技术来提高性能,包括:
- 内联(Inlining):将方法调用直接展开为调用点的代码,减少方法调用的开销。
- 逃逸分析(Escape Analysis):确定对象的作用范围,如果对象没有逃逸出方法,可以在栈上分配而不是堆上分配,降低垃圾回收压力。
- 循环优化(Loop Optimization):包括循环展开、循环交换和循环融合等技术,减少循环开销。
- 死代码消除(Dead Code Elimination):移除不会被执行的代码,减少不必要的计算。
JVM调优
1. 内存调优
合理配置JVM内存参数可以显著提高应用程序的性能和稳定性。常用的内存调优参数包括:
- -Xms:设置JVM初始堆大小。
- -Xmx:设置JVM最大堆大小。
- -Xmn:设置新生代大小。
- -XX:PermSize:设置初始方法区大小(在Java 8之前)。
- -XX:MaxPermSize:设置方法区最大大小(在Java 8之前)。
- -XX:MetaspaceSize:设置初始元空间大小(在Java 8及以后)。
- -XX:MaxMetaspaceSize:设置元空间最大大小(在Java 8及以后)。
# 设置初始堆大小为1GB,最大堆大小为4GB
-Xms1g -Xmx4g
# 设置新生代大小为512MB
-Xmn512m
# 设置初始元空间大小为128MB,最大元空间大小为256MB
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
2. 垃圾回收器调优
选择适合应用场景的垃圾回收器,并合理配置相关参数,可以优化垃圾回收性能。常见的垃圾回收器及其参数包括:
- Serial GC:
-XX:+UseSerialGC
- Parallel GC:
-XX:+UseParallelGC
,相关参数:-XX:ParallelGCThreads=n
:设置并行GC线程数。
- CMS GC:
-XX:+UseConcMarkSweepGC
,相关参数:-XX:CMSInitiatingOccupancyFraction=n
:设置CMS GC在多少百分比堆占用率时启动。-XX:+UseCMSCompactAtFullCollection
:设置在CMS GC后进行内存整理。
- G1 GC:
-XX:+UseG1GC
,相关参数:-XX:MaxGCPauseMillis=n
:设置G1 GC的目标最大停顿时间。-XX:G1HeapRegionSize=n
:设置G1堆区域大小。
选择适合应用场景的垃圾回收器,并调整相关参数以优化性能。例如,对于低延迟应用,可以选择G1 GC并调整最大停顿时间:
# 使用G1垃圾回收器,并将最大停顿时间设置为200毫秒
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
3. 线程调优
JVM提供了一些参数来调整线程相关的性能:
- -Xss:设置每个线程的栈大小。
- -XX:CICompilerCount:设置JIT编译器线程数。
- -XX:ParallelGCThreads:设置并行GC线程数。
适当调整线程栈大小和并行GC线程数,以提高性能:
# 设置每个线程的栈大小为1MB
-Xss1m
# 设置并行GC线程数为8
-XX:ParallelGCThreads=8
4. 编译器调优
JVM编译器相关的调优参数:
- -XX:CompileThreshold:设置方法被编译为本地代码前需要执行的次数。
- -XX:+PrintCompilation:打印被编译的方法信息,帮助调试和优化。
JVM编译器相关的调优参数可以帮助提高JIT编译的效率和性能:
# 设置方法被编译为本地代码前需要执行的次数为10000次
-XX:CompileThreshold=10000
# 打印被编译的方法信息,帮助调试和优化
-XX:+PrintCompilation
JVM调试与监控
1. JVM工具
-
jps:显示所有正在运行的JVM进程。
jps
-
jstack:打印线程堆栈,分析线程问题。
jstack <pid>
-
jmap:生成堆转储文件,分析内存问题。
jmap -heap <pid>
-
jstat:监控JVM各种运行时统计信息,如垃圾回收、类加载等。
jstat -gc <pid> 1000
-
jconsole:基于JMX的图形化监控工具。
jconsole
-
VisualVM:综合了多个JVM工具功能的图形化监控和分析工具。
jvisualvm
2. 日志和诊断
启用JVM日志和诊断参数,可以帮助分析和调试性能问题:
# 启用GC日志
-XX:+Print当然,继续深入探讨JVM的日志和诊断机制,以及进一步的优化技巧。
## JVM日志和诊断
启用JVM的各种日志和诊断参数,可以帮助分析和调试性能问题。这些日志可以提供详细的运行时信息,帮助开发者了解JVM的内部工作机制。
### 1. **GC日志**
启用垃圾回收日志,可以帮助分析垃圾回收行为和性能问题。
```sh
# 启用GC日志
-XX:+PrintGC
# 启用详细GC日志
-XX:+PrintGCDetails
# 启用GC时间戳日志
-XX:+PrintGCTimeStamps
# 启用GC日期时间日志
-XX:+PrintGCDateStamps
# 将GC日志输出到指定文件
-Xloggc:/path/to/gc.log
2. 类加载和卸载日志
启用类加载和卸载的日志,可以帮助分析类加载问题。
# 打印类加载信息
-XX:+TraceClassLoading
# 打印类卸载信息
-XX:+TraceClassUnloading
3. 编译日志
启用JIT编译器的日志,可以帮助调试和优化编译性能。
# 打印被编译的方法信息
-XX:+PrintCompilation
# 打印编译器优化的详细信息
-XX:+PrintInlining
4. 线程日志
启用线程日志,可以帮助分析线程行为和并发问题。
# 打印线程启动与终止信息
-XX:+PrintTLAB
# 打印线程栈信息
jstack <pid>
JVM调优案例
以下是一些常见的JVM调优案例,展示如何根据具体的应用场景调整JVM参数以优化性能。
案例1:Web应用服务器调优
假设我们有一个运行在服务器上的Java Web应用,需要处理高并发请求,要求低延迟和高吞吐量。
# 启用G1垃圾回收器,设置最大停顿时间为200毫秒
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# 设置初始堆大小为4GB,最大堆大小为8GB
-Xms4g -Xmx8g
# 设置新生代大小为2GB
-Xmn2g
# 设置每个线程的栈大小为1MB
-Xss1m
# 启用GC日志,输出到指定文件
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/logs/gc.log
# 启用JIT编译日志
-XX:+PrintCompilation -XX:+PrintInlining
案例2:大数据处理应用调优
假设我们有一个用于大数据处理的Java应用,需要处理大量数据,要求高吞吐量和稳定性。
# 启用并行垃圾回收器,设置并行GC线程数为8
-XX:+UseParallelGC -XX:ParallelGCThreads=8
# 设置初始堆大小为8GB,最大堆大小为16GB
-Xms8g -Xmx16g
# 设置新生代大小为4GB
-Xmn4g
# 设置每个线程的栈大小为2MB
-Xss2m
# 启用GC日志,输出到指定文件
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/logs/gc.log
# 启用JIT编译日志
-XX:+PrintCompilation -XX:+PrintInlining
案例3:低延迟交易系统调优
假设我们有一个低延迟要求的金融交易系统,要求尽可能减少GC停顿时间。
# 启用G1垃圾回收器,设置最大停顿时间为100毫秒
-XX:+UseG1GC -XX:MaxGCPauseMillis=100
# 设置初始堆大小为2GB,最大堆大小为4GB
-Xms2g -Xmx4g
# 设置新生代和老年代比例
-XX:NewRatio=1
# 设置每个线程的栈大小为512KB
-Xss512k
# 启用GC日志,输出到指定文件
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/logs/gc.log
# 启用JIT编译日志
-XX:+PrintCompilation -XX:+PrintInlining
标签:Java,XX,GC,内存,JVM,线程,机制,快速
From: https://blog.csdn.net/2301_78864183/article/details/139413830