JAVA虚拟机解析--基于JDK1.8
java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上不只是专用于java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。
JVM的基本结构
JVM由三个主要的子系统构成:
类加载子系统
运行时数据区(内存结构)
执行引擎
------------------------------------------------------------------------------
类加载机制
类的生命周期
解析:
动态链接:程序运行期间,由符号引用转化为直接引用
类加载器的种类
启动类加载器(Bootstrap ClassLoader)
负责加载JRE核心类库,如JRE目标下的rt.jar, charset.jar等
扩展类加载器(Extension ClassLoader)
负责加载JRE扩展目录ext中jar类包
系统类加载器(Application ClassLoader)
负责加载ClassPath路径下的类包
用户自定义加载器(User ClassLoader)
负责用户自定义路径下的类包
-----------------------------------------------------------------------------------------
类加载机制:
全盘负责委托机制
当一个classlader加载一个类的时候,除非显示的使用另一个classloader,该类所依赖和引用的类也由这个classloader载入。
双亲委派机制:
指定先委托父类加载器寻找目标类,在找不到的情况下,在自己的路径中查找并载入目标类
双亲委派模式的优势:
沙箱安全机制:比如自己写的string类不会被加载,这样可以防止核心库被随意篡改
避免类的重复加载:当父classloader已经加载了该类的时候,就不需要子classloader再加载一次。
举个例子:
比如自定义一个 demo类(双亲委派机制)
编译完后
由application classcloader加载器
app进行判断它头上是否有加载器(它的父类加载器是ext(同理往上找到boot))(这些jvm来完成判断)
boot发现自定义的demo类并不是自己加载范畴,于是往下提交给ext,ext也加载不了,于是往下交给app;
此时app才可以进行加载demo
双亲委派机制的好处:
避免重复;2.安全性更好(隔离)
比如自定义一个java.lang.String类,此时无法正常加载,因为jvm真正使用的是boot加载器
全盘委派机制
提问:为什么要打破双亲委派机制?(不按照双亲机制去加载)自定义类加载器去执行
答:比如jdbc,tomcat,一些热部署技术都是打破双亲委派机制,一个tomcat容器跑了一个jvm,同个tomcat可以部署多个war包,多个war包里面共用了一个jar,为了做隔离,tomcat就通过自定义类加载器来实现类隔离,避免产生冲突(因为jar包只有一个,全限定名也一致)
-----------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------
运行时数据区(内存结构)
程序代码解析
public class Demo {
public int math() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Demo d = new Demo();
d.math();
}
}
对上面的代码进行解析(main线程栈帧解析)
编辑java文件,获得到一段16进制文件
$ javac Demo.java
进行反汇编:
javap -c demo.class
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
javap -c Demo.class > demo.txt
打开demo.txt
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
demo.txt
Compiled from "Demo.java"
public class com.test.Demo {
public com.test.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int math();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/test/Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method math:()I
12: pop
13: return
}
看math方法:(分析两个指令,其他的类似)
iconst_1
将第一个变量放到程序计数器中;
istore_1
从程序计数器中取出数据(栈:先进后出机制),然后放到局部变量表
图示过程:(长度根据变量类型来定义)
(上右图局部变量表不存在顺序(非栈结构))
。。。。(省略) |
。。。。(省略) |
常量池:-128-127(10就是从方法区中的Integer常量池中获取的,每个线程都可以获取(线程共享))
Code:就是程序计数器记录的位置(确保多线程按照顺序执行)
-----------------------------------------------------------------------------
javap -v demo.class >demodynamic.txt (获取动态链接的信息,内容更多)
红色为自己标记:
Classfile /E:/workspace/demo01/src/main/java/com/test/demo.class
Last modified 2020-3-3; size 374 bytes
MD5 checksum 642b4793cbb9e43369b45632d50d2f3d
Compiled from "Demo.java"
public class com.test.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: # 编译期间已经确定,在方法区中叫运行时常量池
#1 = Methodref #5.#16 // java/lang/Object."<init>":()V
#2 = Class #17 // com/test/Demo
#3 = Methodref #2.#16 // com/test/Demo."<init>":()V
#4 = Methodref #2.#18 // com/test/Demo.math:()I
#5 = Class #19 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 math
#11 = Utf8 ()I
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 Demo.java
#16 = NameAndType #6:#7 // "<init>":()V
#17 = Utf8 com/test/Demo #全限定名
#18 = NameAndType #10:#11 // math:()I
#19 = Utf8 java/lang/Object
{
public com.test.Demo();
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 7: 0
public int math();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 10: 0
line 11: 2
line 12: 4
line 13: 11
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/test/Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method math:()I
12: pop
13: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
}
SourceFile: "Demo.java"
程序运行时,新建一个类的程序运行状态(动态链接:符号引用(字符串)》直接引用):
方法区(Method Area)(jdk1.8以前:永久代/持久代,之后:元空间)
类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在这里定义。简单来说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是为了和java的堆区分开(jdk1.8以前hotspot虚拟机叫做永久代,持久代,jdk1.8叫元空间)
堆(Heap)
虚拟机启动时,自动分配创建,用于存放对象的实例,几乎所有对象都在对上分配内存,当对象无法在该空间申请到内存是将抛出OutOfMemoryError异常,同时也是垃圾回收(GC)管理的主要区域
新生代(Young Generation)
gc:
youngGC/MinorGC
CMS OldGC(针对老年代的回收)
majorGC/FullGC(所有的回收,包含元空间)
G1 MixedGC (混合回收)
类出生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生代分为两部分:eden和survivor space(from+to)。所有的类都在eden区被new出来,幸存区分为from+to。当eden区的空间用完后,程序又需要创建对象,jvm的gc将eden区进行垃圾回收(minor gc),将eden区中的不再被其它对象应用的对象进行销毁。然后将eden区中剩余的对象移到from区,若from也满了,再对该区进行gc,然后移到to区(这块有多种gc算法,不扩展,后续展开)
from才会接收来自eden的对象,from满了会触发gc转移到to,同时年龄+1;
然后from-to逻辑进行转换(复制算法),多次转换后当年龄==15,就转移到老年代;
年龄:(64位)对象头中针对年龄是一个4字节,0000~FFFF (0~15)
age<=15,通过jvm参数设置
分配担保机制(如果对象过大(和新生代的百分比进行比较),则直接初始化到老年代中)
引用如下进行解释分配担保机制:
https://blog.csdn.net/kavito/article/details/82292035
b. 老年代(Old Generation)
新生代经过多次GC仍然存货的对象移动到老年区。若老年代也满了,这时候将发生Major GC(也可以叫Full GC),进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出OOM(OutOfMemoryError)异常
元空间(Meta Space)
在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用
的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。
为什么移除了永久代?
参考官方解释http://openjdk.java.net/jeps/122
大概意思是移除永久代是为融合HotSpot与 JRockit而做出的努力,因为JRockit没有永久代,不需要配置永久代。
栈(Stack)
Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致
本地方法栈(Native Method Stack)
和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行native方法服务。登记native方法,在Execution Engine执行时加载本地方法库
程序计数器(Program Counter Register)
jvm还有很多内容,后期有时间继续研究,有不足的大家留言提出,谢谢!