JVM简介
JVM是Java程序得以运行的平台,也是Java程序可以跨平台的底层支撑,从整体上来看,JVM的主要功能可以分为加载和执行两大块。其中类加载器负责.class文件的寻址与加载,执行引擎负责字节码指令执行及内存的管理等等。下面是JVM一个经典的体系结构图
类加载系统:关于类加载体系的详细说明在另一博客https://blog.csdn.net/w1673492580/article/details/81838835
运行时数据区
宏观角度:
从宏观角度来看,JVM运行时数据区的各部分划分很明显,方法区用于存储类数据,堆用于存储Java程序在运行过程中创建的所有对象,栈和PC寄存器属于线程独有,栈代表线程执行过程中所有方法调用信息(比如比如入参、局部变量、中间结果,返回信息等等),PC寄存器即计数器,代表指令在主存中的地址,每执行一条指令后PC+1,即指向下一条指令。本地方法栈即通过Java调用本地方法时的相关信息,但与调用Java方法不一样,调用Java方法时JVM会当前Java栈中压入一个新的栈桢,而调用本地方法则不会修改Java栈,从这个角度看,本地方法栈可以理解成JVM运行时数据区的一个扩展。
微观角度:
堆:Java程序在运行过程中(加载也是运行的一部分)创建的所有对象都位于堆内存中,且JVM中堆内存在设计上是多线程共享的,所以堆中数据的访问也必须进行安全控制;
由于运行期间很可能会创建大量的对象,而且大部分对象都是小而短的(占用空间小及生命周期短),所以堆内存的管理也尤其重要,但JVM中并没有释放对象的指令,这表示开发中不能通过代码去管理对象的释放,所以JVM内置了垃圾回收器来管理,堆也是垃圾回收主要集中的地方(对于栈由于栈桢的大小可以在编译期就根据类结构数据确定,所以这部分的回收具有一定的确定性)。
在Java代码中,通过obj.getClass()获取对象所属的类,也可以通过new ClassName()创建一个对象。这是因为在JVM中对象数据包含了一个指向方法区中对应类型信息的指针,可以通过该指针获取对应类信息。反过来JVM也可以根据方法区中类信息创建该类对象,甚至知道该类对象应该占用多少空间(但实际分配大小依赖于JVM实现)。同样通过class.getClassLoader()可以获取当前类加载器也是同样的原理。
还有个比较有意思的就是对象锁synchronized(obj),Java中每个对象都可以作为锁,同样也是因为对象本身包含了一个指向锁数据的指针,但由于绝大部分对象的锁都用不到,所以大部分JVM的实现,都只在第一个线程尝试获取对象锁时,才给该对象分配对应的锁数据。
关于堆空间本身的设计依赖具体的实现,下面是两种完全不同的可能设计,第一种把堆内存划分为句柄池和对象池两部分,对对象的引用指向句柄池,句柄池中每个元素又由两部分组成,一个指向方法区中的存放类数据的地址,一个指向对象池中的对象。这样的优势体现在,当垃圾回收器 回收内存并重新划分导致对象内存地址发生变化时,不需要更新所有引用的指向,而只需要更新句柄池中指向对象的指针,缺点就是中间需要额外经过一次查找。
第二种设计引用直接指向堆中的对象,这样的优缺点和第一种设计刚好相反,不需要额外的查找,但对象地址发生变化时,需要更新所有引用。
方法区:方法区在设计上也是所有线程共享,主要存储类相关信息(如字段/方法信息、常量池信息、对当前ClassLoader和Class的引用等等),在JVM加载某个类时,会抽取出对应.class文件中类相关的信息并以某种结构(依赖于JVM实现)存到方法区中,当程序运行时,JVM则会到方法区中去查找使用对应类信息(比如创建对象)。
值得注意的一点是由于所有线程共享方法区中的数据,所以方法区中数据的访问必须被设计成线程安全的,比如说多个线程并发加载同一类等等。另外方法区虽然也被称为“永久代”,但实际上其中的数据也是可以被垃圾回收器回收的,回收内容主要包括常量池中无用的常量、无用的类(具体判断依据请参考垃圾回收篇)。
虽然类信息具体的存储结构依赖于具体JVM实现,但为了提高方法的检索效率,部分JVM实现会为每个非抽象类生成一个方法表(方法表虽然加快了检索速度,但本身也会占用一定的内存空间,算是以空间换时间),方法表是一种数组结构,每个元素代表一个方法实例(从Java角度来说,每个元素就相当于一个Method对象)。这种情况下,对象不再直接指向方法区中的存放类信息的地址,而是指向方法表,通过方法表来间接关联对象与类型信息,从JVM的角度来看其基本指向如下图
Java栈与PC寄存器:JVM中每个线程都有自己的PC寄存器和Java栈,PC寄存器即计数器,表示指令在主存中的地址,每执行一条指令后PC+1,即指向下一条指令。Java栈代表线程在执行过程中的所有方法调用信息,JVM对栈只有压入栈桢和弹出栈桢两个操作,“栈”由"栈桢“组成,线程每调用一次方法JVM即会为它产生并压入一个“栈桢”,方法执行完毕即会弹出对应的栈桢,栈桢本质上就是一个内存片,用来存储方法局部变量和计算的中间结果,其中用于存储中间结果的部分又称为“操作数栈”,所以操作的数据即可能是“操作数栈”中的数据,也可能是“栈桢”中的数据。 值得注意的是局部变量和操作数栈的大小在编译时计算出来,并放置到class文件中,然后JVM就可以知道方法栈桢的大小,当调用一个方法时jvm将压入一个适当大小的栈桢至栈中,但栈在jvm中是有深度限制的,当线程调用的栈深度超出该限制时将抛出StockOverflowErro异常,如果jvm在扩展栈时无法获取更多内存则会抛出OutofMemoryErro。下面关于Java栈的图文描述及验证代码
public static void main(String[] args) throws InterruptedException {
// testStackOverflowError();
testOutOfMemoryError();
}
public static void testStackOverflowError(){
testStackOverflowError();
}
public static void testOutOfMemoryError(){
byte[] bytes = new byte[2000000000];
}
本地方法栈:
本地方法栈即代表了线程在执行过程中调用本地方法的一系列信息,与Java栈不同的地方在于,本地方法并不受JVM的限制,对本地方法的调用不会导致JVM往Java栈中压入栈桢。关于本地方法与Java方法的调用可以简单的假设一下,假设某个线程在执行Java方法过程中调用了本地方法C1,且本地方法C1最终又调用了某Java方法,则在这个过程中JVM会先由Java栈进入本地方法栈最终又回到Java栈中,下图简单的描述了这种情况
执行引擎:
执行引擎负责字节码指令的执行,方法的字节码流由一系列有序指令组成,指令又由一个单字节的操作码 + 0个或多个操作数组成。操作码表示需要执行的操作,操作数表示操作的数据,一般来源于当前栈桢中的局部变量或当前Java栈桢中操作数栈的顶部,至于操作数的个数,由操作码决定(操作码本身就决定了它是否需要操作数,以及操作数的形式等等)。
不同的JVM中执行引擎也可能不同,最简单同时效率也最低的执行引擎是一次性解释字节码,它在每次运行方法时都把字节码翻译成本地代码再执行; 其次是即时编译(JITC),它在第一次执行方法时,会把对应的字节码翻译成本地机器代码并缓存,后续调用就可以重用缓存的本地机器代码;另外一种是自适应优化器(特殊的即时编译器), JVM一开始也会解释字节码,但它会监视程序的活动,并记录活动过程中使用最频繁的代码,然后把这些代码编译成本地代码,而其它代码则继续采用解释的方式。下面是javap -c com.alibaba.fastjson.JSONObject.class反汇编后的部分信息
public static java.lang.String valueToString(java.lang.Object) throws org.zend.sdklib.internal.utils.json.JSONException;
Code:
0: aload_0
1: ifnull 12
4: aload_0
5: aconst_null
6: invokevirtual #304 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
9: ifeq 16
12: ldc_w #261 // String null
15: areturn
16: aload_0
17: instanceof #635 // class org/zend/sdklib/internal/utils/json/JSONString
20: ifeq 83
23: aload_0
24: checkcast #635 // class org/zend/sdklib/internal/utils/json/JSONString
27: invokeinterface #637, 1 // InterfaceMethod org/zend/sdklib/internal/utils/json/JSONString.toJSONString:()Ljava/lang/String;
32: astore_1
33: goto 46
36: astore_2
37: new #52 // class org/zend/sdklib/internal/utils/json/JSONException
40: dup
41: aload_2
42: invokespecial #640 // Method org/zend/sdklib/internal/utils/json/JSONException."":(Ljava/lang/Throwable;)V
45: athrow
46: aload_1
47: instanceof #92 // class java/lang/String
50: ifeq 58
53: aload_1
54: checkcast #92 // class java/lang/String
57: areturn
58: new #52 // class org/zend/sdklib/internal/utils/json/JSONException
61: dup
aload_0表示将第一个局部变量压入到当前操作数栈中,aload指令后跟着的操作数必须是对象引用,这里第一个局部变量也是引用,即参数Object。
ifnull 12表示弹出栈顶对象(即刚压入的参数obj),判断是否为null, 为null则跳转到偏移量为12的分支处,关于指令相关的更多信息这里就不说了