目录
基础快速掌握
什么是JVM虚拟机
JVM的的实现
操作系统-虚拟机-JRE-JDK的关系
生产环境部署JDK还是JRE
JVM内存组成部分和堆空间分布
内存组成
堆空间内存分布
内存分布
堆空间分配
JVM堆空间垃圾回收流程及JVM参数
垃圾回收流程
JVM参数分类
JVM参数格式分类
JVM堆栈内存配置参数
JVM常见的命令行参数配置
JVM虚拟机栈参数调整和溢出案例
JVM虚拟机栈参数调整
调试代码查看栈帧
栈溢出抛错测试
Jmeter5.x压测案例
压测参数
JVM参数
JDK8之后的方法区实现和元空间的联系
什么是方法区和元空间
元空间大小配置
什么时候容易出现元空间不足
JVM迄今为止都是程序员老生常谈的话题,实话说我每一次研究探索完后都会忘一次,但好在每次都有新的见解,再加上JDK每迭代更新一版都会有新的特性和改良,对此,我们就当温故知新,增进新的理解,弥补不足,在这里我们不讲大而全的知识,也不会讲老旧的知识点,主要围绕在工作和面试中最高频的点,聚焦于JDK9之后的关键内容进行剖析。
基础快速掌握
什么是JVM虚拟机
Java Virtual Machine是一种虚拟机,它是Java程序运行的环境。JVM提供了Java程序的运行时环境,包括内存管理、垃圾回收、安全性、类加载等功能,是Java语言的核心,它使Java语言具有跨平台的特性(一次编译四处运行)。
JVM的的实现
虚拟机名称 | 说明 |
---|---|
HotSpot | 是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机,主要使用C++实现,JNI接口部分用C实现 |
JRockit | 除 HotSpot 之外另一款比较厉害的 VM, 一开始是BEA 公司的,一度获取运行最快的虚拟机。 Oracle 在 2008 年收购了 BEA 公司,JRockit 与 HotSpot 同属于 Oracle |
J9 VM | 基于IBM公司开发的JDK,主要应用在IBM公司开发的的软件或服务器端,如:嵌入式、服务端、桌面等,基本上IBM本司出品的产品都是用的J9 VM |
JVM有以上产品实现,这里主要列了这几个,当然还有其他的实现,这些JVM在具体实现上会存在一些差异,而且在不同的场景下他们表现都不一样,这些差异主要体现在性能、稳定性、兼容性等多个方面,虽说有诸多区别,但所提供的基础能力都基本一致。
操作系统-虚拟机-JRE-JDK的关系
操作系统:操作系统是计算机硬件和应用程序之间的桥梁,负责管理计算机的硬件资源,如CPU、内存、硬盘和网络等。
虚拟机:虚拟机是一种软件,它在操作系统之上运行,提供了一个独立的、虚拟的计算机环境,用于执行Java程序。
JRE:Java Runtime Environment(JRE)是Java程序的运行环境,它包含Java虚拟机(JVM)和Java类库。它提供了Java程序运行所需的基本环境,但它没有提供Java程序的开发工具。
JDK:Java Development Kit(JDK)是Java程序的开发工具包,它包含JRE、编译器、调试器和其他开发工具,提供了Java程序的开发、编译、调试和部署所需的全部工具。
生产环境部署JDK还是JRE
-
在生产环境部署Java程序时,一般会选择安装JRE而不是JDK
-
JRE安装包更小,打包成镜像更省空间,但是缺少相关调试开发工具,比如以下的
-
编译器:JRE中没有Java编译器,无法将Java源代码编译成字节码文件。
-
调试器:JRE中没有Java调试器,无法进行Java程序的调试和测试
-
-
但在某些特殊情况下,如需要进行Java程序的开发、编译、调试等操作时,就需要安装JDK
JVM内存组成部分和堆空间分布
内存组成
基于JDK8的HotSpot虚拟机,不同虚拟机不同版本会有不一样
名称 | 作用 | 特点 |
---|---|---|
程序计数器 | 也叫PC寄存器,用于记录当前线程执行的字节码指令位置,以便线程在恢复执行时能够从正确的位置开始 | 线程私有 |
Java虚拟机栈 | 用于存储Java方法执行过程中的局部变量、方法参数和返回值,以及方法执行时的操作数栈 | 线程私有 |
本地方法栈 | 用于存储Java程序调用本地方法的参数和返回值等信息。 | 线程私有 |
堆 | 用于存储Java程序创建的对象,所有线程共享一个堆,堆中的对象可以被垃圾回收器回收,以便为新的对象分配空间 | 线程共享 |
元数据区 | 用于存储类的元数据信息,如类名、方法名、字段名等,以及动态生成的代理类、动态生成的字节码等 元空间是位于本地(直接)内存中的,而不是像JDK8之前方法区位于堆内存中的。 | 线程共享 |
堆空间内存分布
内存分布
用于存储Java程序创建的对象,所有线程共享一个堆,堆中的对象可以被垃圾回收器回收,以便为新的对象分配空间。
堆空间分配
JVM堆空间垃圾回收流程及JVM参数
垃圾回收流程
-
新建对象,放到Eden区,满后触发Minor GC(每次都是由Eden区满触发Minor GC,接连放对象到S0或S1)
-
存活的对象移动到Survivor的S0区,如果S0满后触发Minor GC
-
S0存活下来的对象移动到S1区,然后S0区空闲
-
S1满后触发Minor GC,再次移动到S0区,然后S1区空闲
-
反复GC每次对象涨1岁,到达一定次数后(默认15),进入老年代
-
当老年代内存不足会触发Full GC,出现STW(Stop-The-World)
-
堆被垃圾回收,基本都是采用分代收集算法,不通区域的采用不同的垃圾回收算法
-
方法结束后,堆中的对象不会马上移除,在垃圾回收的时候才会被移除(不是实时GC的,ThreadLocal里面说过)
直通车:Java高阶私房菜:深入解析多线程场景下ThreadLocal应用-CSDN博客
JVM参数分类
JVM参数格式分类
格式 | 解释 | 例子 |
---|---|---|
标准参数(-) | 所有JVM都实现这些参数的功能 | -verbose:gc 打印GC简要信息 |
非标准参数(-X) | 不保证所有JVM实现都满足 | -Xmx2048m等价 -XX:MaxHeapSize JVM最大堆内存为2048M |
非稳定参数(-XX) | 不稳定未来可能取消,但很有用 | -XX:+PrintGCDetails每次GC时打印详细信息。 |
-XX:+ | 开启对应的参数 | -XX:+PrintGCDetails 开启每次GC时打印详细信息。 |
-XX:- | 关闭对应的参数 | -XX:-DisableExplicitGC 禁止调用System.gc() |
-XX:= | 设定数字参数 | -XX:NewRatio=2 新生代和老年代内存比例 |
JVM堆栈内存配置参数
参数 | 解释 |
---|---|
-Xms | 初始堆大小,推荐和最大堆一样 |
-Xmx | 最大堆大小,推荐和初始堆一样 |
-Xmn | 年轻代大小 |
-Xss | 每个线程的栈大小 |
JVM常见的命令行参数配置
参数 | 解释 |
---|---|
-XX:+PrintGCDetails | 打印GC回收信息 |
-XX:NewRatio | 新生代和老年代空间大小的比率,由-XX:NewRatio 参数控制 -XX:NewRatio 参数的默认值是2,表示新生代和老年代的比例是1:2 如果将-XX:NewRatio 设置为4,表示新生代和老年代的比例是1:4 |
-XX:MaxMetaspaceSize | 元空间所分配内存的最大值,默认没限制 |
-XX:+UseConcMarkSweepGC | 设置并发收集器 |
JVM虚拟机栈参数调整和溢出案例
JVM虚拟机栈参数调整
栈是用来存储Java程序中的方法调用和局部变量的内存区域,每个线程都有自己的虚拟机栈,其生命周期与线程相同。当一个方法被调用时,Java虚拟机会在该线程的虚拟机栈中创建一个栈帧,用来存储该方法的局部变量、方法返回值等信息。
默认情况下,JVM虚拟机栈的大小是固定的,JDK1.5后通常为1MB。如果线程在执行方法时需要更多的栈空间,JVM会抛出StackOverflowError异常,JVM参数 xss
,比如 -Xss1m
表示1MB。
调试代码查看栈帧
public class StackFrameDemo {
public static void main(String[] args) {
StackFrameDemo demo = new StackFrameDemo();
demo.method1();
}
public void method1() {
String str = "Hello";
method2(str);
System.out.println("method1--完成");
}
public void method2(String str) {
int num = 123;
method3(str, num);
System.out.println("method2--完成");
}
public void method3(String str, int num) {
double d = 3.14;
method4(str, num, d);
System.out.println("method3--完成");
}
public void method4(String str, int num, double d) {
// 查看当前栈帧信息
System.out.println("Current Stack Frame:");
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stackTrace) {
System.out.println(element.toString());
}
System.out.println("method4--完成");
}
}
栈溢出抛错测试
-
栈越小,递归调用的次数就越少,因为栈空间不足导致栈溢出异常
-
栈越大,递归调用的次数就越多,因为有足够的栈空间来存储方法调用的信息
//栈参数,最少208k
-Xss524k
-Xss1m
-Xss10m
//代码案例
public class StackSizeDemo {
private static int count = 0;
public static void main(String[] args) {
try {
recursiveMethod();
} catch (Throwable t) {
System.out.println("Stack overflow after " + count + " invocations.");
t.printStackTrace();
}
}
private static void recursiveMethod() {
count++;
recursiveMethod();
}
}
Jmeter5.x压测案例
@RestController
@RequestMapping("api/v1/data")
public class DataController {
@RequestMapping("compute")
public String compute() {
Byte[] b = new Byte[1024*1024];
return "success";
}
}
压测参数
JVM参数
参数-Xms64m -Xmx64m
参数 -Xms640m -Xmx640m
JDK8之后的方法区实现和元空间的联系
什么是方法区和元空间
方法区是JVM中用来存储类的元数据信息的区域,包括类的结构、方法、字段信息等,Java堆类似各个线程共享的内存区域
元空间、永久代是方法区具体的落地实现Java8之前是称之为永久代(PermGen),java8后引入了一个新概念元空间用于替代旧版JVM中的永久代。
-
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系
-
类实现了接口,类就可以看作是永久代和元空间,接口可以看作是方法区
-
永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间
元空间的大小是动态的,可以根据需要进行自动扩展,如果元空间不足,JVM会抛出 OutOfMemoryError : Metaspace
元空间大小配置
-XX:MetaspaceSize
用来设置元空间初始大小的参数,它的默认值是21 MB
-XX:MaxMetaspaceSize
用来设置元空间最大大小的参数,它的默认值是-1 即不限制,使用的是本地内存,不像旧版的永久代是堆内存。如果不限制元空间的大小,可能会导致元空间占用过多的内存,从而引起内存溢出。
系统参数查看
这两个参数的单位是字节(B),可以使用K、M、G等后缀来表示更大的单位。
public class HeapDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("元空间溢出测试");
Thread.sleep(10000000);
}
}
jps #查看进程号
jinfo -flag MetaspaceSize 进程号 #查看Metaspace分配内存空间
jinfo -flag MaxMetaspaceSize 进程号 #查看Metaspace最大空间
# 调整参数
-XX:MetaspaceSize=126m -XX:MaxMetaspaceSize=524m
当空间不足时抛出异常信息: “Java.Lang.OutOfMemoryError:Metaspace”
什么时候容易出现元空间不足
-
应用程序使用大量的反射技术,例如使用Class.forName()等方法加载类,或者使用Java Reflection API进行操作
-
使用大量的动态代理技术,例如使用Java Proxy类等技术
-
使用大量的注解,例如使用Spring框架的注解等
-
使用的第三方库过多,这些库可能会在运行时动态生成新的类,导致元空间内存占用过多。
-
应用程序的业务逻辑比较复杂,需要加载大量的类
-
应用程序使用大量的JSP页面,其中每个页面都对应一个类文件(现在很少)