JDK版本:8
基础知识
“class文件”、“class content”、“Class对象”、"对象"的含义
class文件:存储在磁盘上的.class文件(十六进制字节码文件)
class content:类加载器子系统加载class文件,存储到内存中的字节流,解析前这块内存区域叫class content
Class对象:InstanceMirrorKlass类型实例
对象:new出来的实例、newInstance得到的实例
JVM运行时数据区
程序计数器
线程私有,用来存储当前线程下一条需要执行的字节码的索引(如果正在执行的方法是native方法,那么程序计数器的值为undefined)
虚拟机栈
线程私有,一个线程有且仅有一个虚拟机栈,虚拟机栈生命周期与线程相同。
虚拟机栈包含多个栈帧,一次方法调用创建一个栈帧。栈帧深度大于虚拟机所允许的栈帧深度触发StackOverflowError异常;创建栈帧时无法申请到足够的内存空间触发OutOfMemoryError异常。
栈帧包含局部变量表、操作数栈、动态连接、方法出口等信息。一个栈帧默认大小:1024字节。
局部变量表:存储编译期可知的java基本数据类型和对象引用(reference),局部变量表大小在编译期就已经确定,运行期不会发生改变
操作数栈:运行期使用,像个变量值暂存区
动态连接(直接地址):指向当前正在执行的方法的元信息(方法的元信息存储在元空间)
返回地址:保存方法返回后下一行需要执行的指令的索引
栈帧维度分析方法运行
public static void main(String[] args) {
int a = 10;
int b = 20;
int c = add(a, b);
System.out.println(c);
}
private static int add(int a, int b) {
return a + b;
}
执行main方法
- 创建main方法的栈帧
- main方法栈帧的局部变量表内存地址赋值给线程的局部变量表指针
- main方法栈帧的操作数栈内存地址赋值给线程的操作数栈指针
- 开始执行方法内部逻辑(稍后再以字节码维度进行分析)
执行add方法
- 创建add方法的栈帧
- main方法下一行字节码指令的索引(程序计数器)保存至add方法栈帧
- 线程的局部变量表指针保存至add方法栈帧
- 线程的操作数栈指针保存至add方法栈帧
- add方法栈帧的局部变量表内存地址赋值给线程的局部变量表指针
- add方法栈帧的操作数栈内存地址赋值给线程的操作数栈指针
- 开始执行方法内部逻辑
字节码指令维度分析方法运行
main方法方法体字节码指令如下
main方法局部变量表如下
执行main方法
- bipush 10 分析:把int类型数值10压入操作数栈
- istore_1 分析:把操作数栈栈顶的int类型数值(也就是10)赋值给局部变量表index=1位置的变量(也就是a)
- bipush 20 分析:把int类型数值20压入操作数栈
- istore_2 分析:把操作数栈栈顶的int类型数值(也就是20)赋值给局部变量表index=2位置的变量(也就是b)
- iload_1 分析:把局部变量表index=1位置变量的int类型值(也就是10)压入操作数栈
- iload_2 分析:把局部变量表index=2位置变量的int类型值(也就是20)压入操作数栈
- invokestatic #2 <com/front/worksheet/ApplicationTest.add> 分析:调用静态方法ApplicationTest.add
- istore_3 分析:把方法返回的int类型数值(也就是30)赋值给局部变量表index=3位置的变量(也就是c)
- getstatic #3 <java/lang/System.out> 分析:获取静态变量System.out
- iload_3 分析:把局部变量表index=3位置变量的int类型值(也就是30)压入操作数栈
- invokevirtual #4 <java/io/PrintStream.println> 分析:调用静态方法PrintStream.println
- return 分析:返回void
add方法方法体字节码指令如下
add方法局部变量表如下
执行add方法
- iload_0 分析:把局部变量表index=0位置变量的int类型值(也就是10)压入操作数栈
- iload_1 分析:把局部变量表index=1位置变量的int类型值(也就是20)压入操作数栈
- iadd 分析:将操作数栈栈顶两个int类型数值相加并将结果压入操作数栈栈顶
- ireturn 分析:返回int类型数值
本地方法栈
线程私有,运行native方法所使用的区域(JNI)
虚拟机堆
线程共享,存储引用类型对象、StringTable、Class对象等
虚拟机堆划分为年轻代和年老代,年轻代又划分为eden区、from区和to区(from区和to区不是绝对的,随着minorGC的执行会相互切换)
虚拟机堆内存分布比:年轻代1/3,年老代2/3(这个比例不是绝对的,JVM有优化机制)
年轻代内存分布比:Eden 8/10,From 1/10,To 1/10(这个比例不是绝对的,JVM有优化机制)
设置JVM堆大小
-Xmx
-Xms
推荐最大值、最小值设置成一样,占物理内存的1/4(阿里内部推荐)
哪些对象会进入老年代
15次minorGC仍然存活的对象
大对象直接进入老年代:默认超过eden区大小一半的对象为大对象
动态年龄判断机制:minorGC时如果from区同一年龄的对象已经占据from区一半以上的空间,那么此次minorGC eden区存活的对象直接进入老年代
空间担保机制
每次minorGC之前判断老年代剩余可用空间是否大于eden区大小,如果大于,继续执行minorGC。
如果小于,判断是否开启空间担保,如果未开启,继续执行minorGC。
如果开启,判断老年代剩余可用空间是否大于历代进入老年代对象大小总和的平均数,如果大于,继续执行minorGC。
如果小于,触发fullGC。
方法区(元空间)
线程共享,存储类的元信息、常量、静态变量、热点代码等数据。
元空间默认大小
最大值:4294901760字节 ≈ 4G
最小值:21807104字节 ≈ 20M
设置元空间大小
-XX:MaxMetaspaceSize
-XX:MetaspaceSize
推荐最大值、最小值设置成一样,占物理内存的1/32(阿里内部推荐)
JDK8为什么用元空间取代永久
- GC算法更方便地区分引用类型对象和类的元信息对象
- 避免动态代理使用不当造成OOM的风险