也可参考其他人的总结相互学习:https://blog.csdn.net/weixin_38766678/article/details/96566161
JVM由三个主要的子系统构成:
1.类加载器子系统
2.运行时数据区(内存)
3.执行引擎
认识虚拟机内存模型:
如以下类
package com.lean;
public class JowerJvm {
public int compute() {
int a = 2;
int b = 3;
int c = (2 + 3) * 8;
return c;
}
public static void main(String[] args) {
JowerJvm math = new JowerJvm();
math.compute();
}
}
当程序开始执行main方法时,JVM会开辟一块线程栈的内存空间
通过javap -c JowerJvm.class 命令可以查看.class文件的字节码中的执行指令。
如下:
F:\leancode\jvm\out\production\jvm\com\lean>javap -c JowerJvm.class
Compiled from "JowerJvm.java"
public class com.lean.JowerJvm {
public com.lean.JowerJvm();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: bipush 40
6: istore_3
7: iload_3
8: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/lean/JowerJvm
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
}
根据字节码生成的命令compute()方法令计算过程如下(需查找对应的命令)
public int compute();
Code:
0: iconst_2 //备注:int型常量2 放到数据栈中
1: istore_1 //备注:将数据栈中的值取出来存储到第二个变量中。即给变量a赋值
2: iconst_3 //备注:int型常量3 放到数据栈中
3: istore_2 //备注:将数据栈中的值取出来存储到第三个变量中。即给变量b赋值
4: bipush 40 //备注:将一个byte型常量40推送至数据栈顶
6: istore_3 //备注:将数据栈中的值取出来存储到第四个变量中。即给变量c赋值
7: iload_3 //备注:第四个int型局部变量进数据栈
8: ireturn //备注:当前方法返回int
注意:
- 局部变量表中默认第一个变量istore_0为this 变量。
- 方法栈中的操作数栈作为中间临时栈用来协助JVM执行计算(加减乘除)和赋值给局部变量用。
- 字节码指令前的数字0、1、2…8 为程序计数器,记录了当前运行指令的位置,JVM执行引擎会按照该code顺序执行对应的指令。
- 方法栈中的方法出口 保存了被调用方法(main方法)的调用返回位置(即main方法的程序计数器12) 12: pop 表示栈顶位置出栈
当compute方法栈执行完成后会将结果返回,同时compute方法栈就会被移出JowerJvm的线程栈,相关内存会释放掉。同理main方法栈执行完后也会释放该部分内存。
方法区:存储常量+静态变量+类元信息(Java classes在Java hotspot VM内部表示为类元数据,包括上述描述中的main 方法和compute方法的指令码),JDK1.8之后方法区用的是操作系统分配给JVM的内存以外的物理内存。
内存中对象包含很多东西,如下:
其中对象头中有类型指针就存储了方法区中对应类(JowerJvm.class)的类元信息的地址. (执行引擎执行对象方法时,通过该地址获取到对应class中方法的执行指令)
假如再new一个JowerJvm对象,如下:
package com.lean;
public class JowerJvm {
public int compute() {
int a = 2;
int b = 3;
int c = (2 + 3) * 8;
return c;
}
public static void main(String[] args) {
JowerJvm math = new JowerJvm();
math.compute();
JowerJvm math2 = new JowerJvm();
math2.compute();
}
}
重点:这个时候当程序运行到 调用math.compute()方法时,通过堆内存中的对象从方法区获取到compute方法指令序列在类源信息中的内存地址。然后存放到compute方法栈帧的“动态链接”内存中。
其中动态链接内存的主要目的就是在运行过程中动态获取当前方法(compute)的执行指令序列。
可以通过 javap -v JowerJvm.class 查看更多的执行指令,如下:
F:\leancode\jvm\out\production\jvm\com\lean>javap -v JowerJvm.class
Classfile /F:/leancode/jvm/out/production/jvm/com/lean/JowerJvm.class
Last modified 2019-11-10; size 616 bytes
MD5 checksum 77fceff8a826cd9ff400630ae9a864c0
Compiled from "JowerJvm.java"
public class com.lean.JowerJvm
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#27 // java/lang/Object."<init>":()V
#2 = Class #28 // com/lean/JowerJvm
#3 = Methodref #2.#27 // com/lean/JowerJvm."<init>":()V
#4 = Methodref #2.#29 // com/lean/JowerJvm.compute:()I
#5 = Class #30 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/lean/JowerJvm;
#13 = Utf8 compute
#14 = Utf8 ()I
#15 = Utf8 a
#16 = Utf8 I
#17 = Utf8 b
#18 = Utf8 c
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 math
#24 = Utf8 math2
#25 = Utf8 SourceFile
#26 = Utf8 JowerJvm.java
#27 = NameAndType #6:#7 // "<init>":()V
#28 = Utf8 com/lean/JowerJvm
#29 = NameAndType #13:#14 // compute:()I
#30 = Utf8 java/lang/Object
{
public com.lean.JowerJvm();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lean/JowerJvm;
public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=4, args_size=1
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: bipush 40
6: istore_3
7: iload_3
8: ireturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
line 9: 7
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/lean/JowerJvm;
2 7 1 a I
4 5 2 b I
7 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/lean/JowerJvm
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: new #2 // class com/lean/JowerJvm
16: dup
17: invokespecial #3 // Method "<init>":()V
20: astore_2
21: aload_2
22: invokevirtual #4 // Method compute:()I
25: pop
26: return
LineNumberTable:
line 13: 0
line 14: 8
line 16: 13
line 17: 21
line 18: 26
LocalVariableTable:
Start Length Slot Name Signature
0 27 0 args [Ljava/lang/String;
8 19 1 math Lcom/lean/JowerJvm;
21 6 2 math2 Lcom/lean/JowerJvm;
}
SourceFile: "JowerJvm.java"
通过main()方法的指令序列可以看到,两次math.compute()方法都存的是符号 #4 ,并通过常量池中查看到#4代表的是方法引用 值为:#2.#29 即JowerJvm.compute()方法。
#2 = Class #28 // com/lean/JowerJvm
#4 = Methodref #2.#29 // com/lean/JowerJvm.compute:()I
#28 = Utf8 com/lean/JowerJvm
#29 = NameAndType #13:#14 // compute:()I
当调用到compute()对应指令时会通过对象的头信息中找到方法区中对应的class指令中compute()方法的指令码位置,并存储到新生成的compute方法栈帧的动态链接内存中。
本地方法栈,当程序调用以native修饰的本地方法时(一般为C或者C++语言编写的,如Thread.start方法)会使用本地方法栈。一般用的较少。
垃圾收集(GC:Garbage Collection)
1、如何识别垃圾,判定对象是否可被回收?
引用计数法:给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象计数器是否为0来判断对象是否可被回收。缺点:无法解决循环引用的问题
根搜索算法:也称可达性分析法,通过“GC ROOTs”的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收(可作为GC ROOTs的对象:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象)
2、Java 中的堆是 GC 收集垃圾的主要区域,GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
Minor GC:新生代(Young Gen)空间不足时触发收集,由于Java 中的大部分对象通常不需长久存活,新生代是GC收集频繁区域,所以采用复制算法。
Full GC:老年代(Old Gen )空间不足或元空间达到高水位线执行收集动作,由于存放大对象及长久存活下的对象,占用内存空间大,回收效率低,所以采用标记-清除算法。
Java虚拟机调优主要调的是堆内存的区域
1. 当 Eden内存占满的时候
1)会执行minor gc ,轻量级的垃圾回收 会将无GC Roots根引用的游离状态的的对象进行回收。
2)会将存活的对象从Eden移至survivor From区域
2. 当 Survivor的From区域占满的时候
1)执行GC垃圾回收,同时对象的分代年龄加1(存储在对象头结构中)
2)将剩余存活的对象从From移至To区域
3. 当 Survivor的To区域占满的时候
1)执行GC垃圾回收,同时对象的分代年龄加1(存储在对象头结构中)
2)将剩余存活的对象从To移至From区域,如果当前对象的分代年龄为15(默认值)的时候,会将该存活的对象从TO移动至老年待区域。
4. 到了老年代区域的存活对象,如线程池对象、静态引用的单例对象(可以认为是老不死的对象)。
当老年代区域占满的时候,就会执行full gc (重量级的垃圾回收),但是如果老年代区域中的所有对象还是有对应的引用,继续往里加对象时就会出现OOM(Out of Memory)
注意:造成full gc的原因有很多种,老年代区域占满调用只是其中一种情况。
动态演示对象在Eden、Survivor、Old Gen区域的传递:
1.cmd通过jvisualvm 命令打开Oracle提供的虚拟机工具(需配置jdk的环境变量)。
打开之后,通过工具—插件,然后选择Visual GC在线安装
2. jvisualvm安装Visual GC插件(如果无法在线安装可下载后安装)
可参考链接进行安装:https://blog.csdn.net/shuai825644975/article/details/78970371
3. 执行以下代码
package com.lean;
import java.util.ArrayList;
import java.util.List;
public class GCTest {
private byte[] data = new byte[1024 * 100]; //100k的大小
public static void main(String[] args) throws InterruptedException {
List<GCTest> list = new ArrayList<GCTest>();
while (true) {
list.add(new GCTest());
Thread.sleep(100);
}
}
}
4.通过GC Visual插件可以动态查看各堆内存区域对象迁移情况
下图可以明显看出来Eden区域执行GC之后对象迁移至Survivor0区域,以及Survivor 的两个区域对象交替存储,以及Old Gen区域对象不断增加。
当Old Gen内存占满的时候就会出现OOM
Files\Java\jdk1.8.0_131\jre\lib\rt.jar;F:\leancode\jvm\out\production\jvm" com.lean.GCTest
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.lean.GCTest.<init>(GCTest.java:8)
at com.lean.GCTest.main(GCTest.java:13)
Process finished with exit code 1