文章目录
Java — JVM 实操剖析内部结构
本篇文章主要以实操剖析 JVM 环节为主,辅助于理解 JVM 相关理论知识。注:本文代码部分所采用的 JDK 环境为 1.8。
一、JVM 相关
(一) JVM运行原理
-
.java源文件通过编译器编译产生 .class字节码文件,.class字节码文件通过JVM当中的字节码解释器,编译成特定机器上的 机器码。
-
每一种平台的解释器不同,但是实现的JVM时相同的。(JVM会根据不同的系统生成不同的机器码,每个操作系统都有一个不同的JVM)。
-
查看本机 JVM 型号:
// 通过 java -version 命令,可以查看我们本机的 JVM 为 HotSpot java -version
(二) JVM 体系结构
JVM体系结构:类加载器,运行时数据区,执行引擎
1. 类加载器
-
作用:负责加载.class文件
-
代码分析:
-
测试代码:
public class JvmTest { /** * JVM体系结构1:类加载器 * 类加载器:负责加载.class文件 * * @param object */ public void classLoaderTest(Object object) { // 返回此 {@code Object} 的运行时类 final Class<?> objectClass = object.getClass(); System.out.println(objectClass); // 获取当前类加载器 final ClassLoader classLoader = objectClass.getClassLoader(); System.out.println("当前类加载器:" + classLoader); // 父类类加载器(设计双亲委派机制) System.out.println("EXC类加载器:" + classLoader.getParent()); System.out.println("BOOT类加载器:" + classLoader.getParent().getParent()); } public static void main(String[] args) { JvmTest jvmTest = new JvmTest(); // 查看 JVM类加载器 jvmTest.classLoaderTest(jvmTest); } }
-
代码输出:
当前类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2 EXC类加载器:sun.misc.Launcher$ExtClassLoader@75a1cd57 BOOT类加载器:null
我们可以看到程序中对象使用的类加载器为:AppClassLoader。
那我们为什么还要输出父级的类加载器呢?这儿涉及类加载器一个机制:双亲委派机制,解释如下:双亲委派机制的工作过程:
- 类加载器收到类加载的请求;
- 把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;(APP -> EXC -> BOOT)
- 启动类加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
- 重复步骤三;
-
2. 运行时数据区
1) 组成结构
- 方法区:一个JVM只有一个方法区,所有线程共享,存储:已经被虚拟机加载的类信息,常量(final),静态变量(static),常量池
- 堆:一个JVM只有一个堆,所有线程共享,所有对象实例都在堆上分配,堆是垃圾收集器管理的区域
- 新生区(分为伊甸园区、幸存区from区、幸存区to区),类诞生和成长的地方,甚至死亡
- 养老区
- 永久存储区:JDK8以后,永久存储区改了个名字(元空间)
- 虚拟机栈:每个方法执行的时候,会同步创建一个栈用于存放局部变量等信息
- 程序计数器:当前线程执行的字节码的行号指示器,线程私有
- 本地方法栈:为本地方法服务,调用本地方法接口,【nativate关键字】
2) 代码分析
-
为了能够看到直观 JVM 效果,我们需要提前在 IDEA 中进行如下简单配置:
-XX:+PrintGCDetails
2.2.1 JVM内存
-
首先,我们总体查看一下 JVM 内存 的情况,即对应于 JVM 运行时数据区 的情况:
-
代码如下:
public class JvmTest { /** * JVM体系结构2:运行时数据区大小情况 */ public void jvmInfo() { // 返回虚拟机试图使用的最大内存 long max = Runtime.getRuntime().maxMemory(); System.out.println("JVM 最大内存:" + (double) max / (1024 * 1024)); // 返回 JVM 初始化的总内存 long total = Runtime.getRuntime().totalMemory(); System.out.println("JVM 初始化总内存:" + (double) total / (1024 * 1024)); } public static void main(String[] args) { JvmTest jvmTest = new JvmTest(); // 查看 JVM 内存信息 jvmTest.jvmInfo(); } }
-
代码输出:
JVM 最大内存:3623.5 JVM 初始化总内存:245.5 Heap PSYoungGen total 76288K, used 11190K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000) eden space 65536K, 17% used [0x000000076b180000,0x000000076bc6dba0,0x000000076f180000) from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000) to space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000) ParOldGen total 175104K, used 0K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000) object space 175104K, 0% used [0x00000006c1400000,0x00000006c1400000,0x00000006cbf00000) Metaspace used 3533K, capacity 4500K, committed 4864K, reserved 1056768K class space used 379K, capacity 388K, committed 512K, reserved 1048576K
本人电脑是:16G 内存。
结论:JVM运行时数据区(JVM 内存),默认情况下:JVM分配的总最大内存是电脑内存的 1/4,初始化内存是 1/64。
-
2.2.2 虚拟机栈
-
JVM 虚拟机栈 主要用来管理程序调用,比如我们程序整体为:在 main 函数中调用 函数A,则该程序的虚拟机栈为:
-
代码模拟:
我们模拟一个简单的 栈溢出 的错误代码:public class JvmTest { /** * JVM体系结构2:运行时数据区 —— 栈 * 一个JVM只有一个堆,所有线程共享,所有对象实例都在堆上分配,堆是垃圾收集器管理的区域 */ public void jvmStackOverflowError() { // 循环调用,模拟栈溢出:StackOverflowError A(); } /** * 闭环调用,触发栈溢出:StackOverflowError */ public void A() { B(); } public void B() { A(); } public static void main(String[] args) { JvmTest jvmTest = new JvmTest(); // 模拟 JVM 中栈溢出 jvmTest.jvmStackOverflowError(); } }
-
代码输出:
Exception in thread "main" java.lang.StackOverflowError
-
分析:
以上程序的运行过程如下图所示:在不停的将 A,B函数 压入 JVM的虚拟机栈,最后肯定会撑爆虚拟机栈,导致栈内存溢出的问题。
(暗示:JVM的虚拟栈中肯定不可能存在垃圾,不然程序都是异常的,垃圾如果在栈中,垃圾下面的方法肯定无法被执行。
所以我们常说的 JVM垃圾回收,是回收的 JVM 堆中的垃圾)
2.2.3 JVM 堆
-
JVM堆,这部分是 运行时数据区的重点,并且JVM垃圾回收主要也是针对的是堆部分的内存。
-
代码分析1:
我们接合 2.2.1 JVM内存 的代码输出进行分析JVM 最大内存:3623.5 JVM 初始化总内存:245.5 Heap PSYoungGen total 76288K, used 11190K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000) eden space 65536K, 17% used [0x000000076b180000,0x000000076bc6dba0,0x000000076f180000) from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000) to space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000) ParOldGen total 175104K, used 0K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000) object space 175104K, 0% used [0x00000006c1400000,0x00000006c1400000,0x00000006cbf00000) Metaspace used 3533K, capacity 4500K, committed 4864K, reserved 1056768K class space used 379K, capacity 388K, committed 512K, reserved 1048576K
-
首先、控制台的输出,已经很明显表明了,堆(Heap)的逻辑结构,分为:
- PSYoungGen(新生区)
- 伊甸园 区
- from 区
- to 区
- ParOldGen(老年区)
- Metaspace(元空间,相当于1.8之前的永久代)
(细节:我们将 新生代 + 老年代的内存相加,发现刚好等于 JVM初始化内存的大小,得出结论:元空间逻辑上存在,但是在物理上不存在)
- PSYoungGen(新生区)
-
-
代码分析2:
-
前面,我们得出堆中主要的物理存储在新生代和老年代中。
我们结合新生区,和老年区的结构,分析垃圾回收算法(有:复制算法、标记-清除算法、标记-整理算法、分代收集算法) -
首先,我们模拟 堆内存溢出 的代码,我们来看一下 GC 是如何进行垃圾回收的过程:
public class JvmTest { /** * JVM体系结构2:运行时数据区 —— 堆 * 一个JVM只有一个堆,所有线程共享,所有对象实例都在堆上分配,堆是垃圾收集器管理的区域 * 堆分为:新生代(伊甸园区,from,to),老年代,永久代(元空间,逻辑上存在,物理上不存在) */ public void jvmHeapOutOfMemoryError() { // 模拟堆溢出问题 (OOM) while (true) { int[] array1 = new int[999999999]; int[] array2 = new int[999999998]; } } public static void main(String[] args) { JvmTest jvmTest = new JvmTest(); // 模拟 JVM 中堆内存溢出 jvmTest.jvmHeapOutOfMemoryError(); } }
-
输出:
我截取了部分输出日志分析出:-
前 2 次发生了 轻GC 【因为新生区的 from / to内存大小为 10752,前两次 轻GC 只是在新生代的 from和to之间移动,归结为:新生区 -> 幸存区(from/to)的移动,该过程因为有两个频繁使用的区 from/to,所以在新生区采用 复制算法 进行垃圾回收】
-
第 3 次发生了 重GC【当三次轻GC执行完毕,新生代中的 from/to 内存不够,需要进行重GC:新生区 -> 老年区的过程,该区域使用 标记-清除和标记整理 算法 进行垃圾回收】
-
由此可以得出结论:JVM 根据堆划分的不同区域,整体采用 **分代搜集算法,**其次在不同的代里面根据代的结构和生命周期,采取不同的垃圾回收算法,新生区采用复制算法,老年区采用 标记-清除 和标记整理算法。
[GC (Allocation Failure) [PSYoungGen: 9879K->1792K(76288K)] 9879K->1800K(2859008K), 0.0021556 secs] [GC (Allocation Failure) [PSYoungGen: 1792K->1796K(76288K)] 1800K->1804K(2859008K), 0.0011227 secs] [Full GC (Allocation Failure) [PSYoungGen: 1796K->0K(76288K)] [ParOldGen: 8K->1628K(72704K)] 1804K->1628K(148992K), [Metaspace: 3543K->3543K(1056768K)], 0.0327013 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(76288K)] 1628K->1628K(2859008K), 0.0004414 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 1628K->1556K(108544K)] 1628K->1556K(184832K), [Metaspace: 3543K->3543K(1056768K)], 0.0343275 secs] Heap PSYoungGen total 76288K, used 1966K [0x000000076b180000, 0x0000000774000000, 0x00000007c0000000) eden space 65536K, 3% used [0x000000076b180000,0x000000076b36b9e0,0x000000076f180000) from space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000) to space 10752K, 0% used [0x0000000773580000,0x0000000773580000,0x0000000774000000) ParOldGen total 2782720K, used 1556K [0x00000006c1400000, 0x000000076b180000, 0x000000076b180000) object space 2782720K, 0% used [0x00000006c1400000,0x00000006c1585290,0x000000076b180000) Metaspace used 3581K, capacity 4500K, committed 4864K, reserved 1056768K class space used 386K, capacity 388K, committed 512K, reserved 1048576K Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at indi.pentiumcm.jvm.JvmTest.jvmHeapOutOfMemoryError(JvmTest.java:69) at indi.pentiumcm.jvm.JvmTest.main(JvmTest.java:108)
-
-
二、JVM 调优
- 经过上面的代码分析,其实 JVM 的调优,主要围绕 JVM堆内存的大小设置,轻重GC参数的设置(多少次进行一次重GC等)。
该部分内容,等理解好之后再进行更新。
三、JVM 调优工具
- 推荐:jprofiler
IDEA 中的插件