深入理解JVM(1)—Java虚拟机基本结构

时间:2022-12-27 15:09:59

最近开始看周志明著的《深入理解Java虚拟机》一书,此书作为Java虚拟机的经典畅销书,果然是非常优秀的,在学习它的过程中逐渐理解了Java运行机理、内存分配与回收等知识,收获颇多。
要学习Java虚拟机,首先要了解其历史与基本构造。Java虚拟机的发展历史不做详述,大家只要知道SunJDK和OpenJDK中所带的是HotSpot虚拟机,我们之后的学习也是基于HotSpot虚拟机就可以了。其他还有一些其他的虚拟机,如BEA JRockit、IBM J9VM、Microsoft JVM等,大家有兴趣的也可以了解一下。
本节笔记基于keycoding写的Java虚拟机基本结构:http://blog.csdn.net/yfqnihao,个人认为写的非常形象易懂,有助于理解虚拟机的内部结构。

1. 什么是Java虚拟机?

 百度百科给出的定义是:虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。通俗地说Java虚拟机就是处理Java程序(确切地说是Java字节码)的虚拟机。

下面给出Java虚拟机形象的说明,证明其并不是“虚拟”的,也是可以看得见的。
第一步:先来写一个类:

    package test;  

public class JVMTestForJava {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(10000000);
}
}

第二步:cmd窗口输入:java test.JVMTestForJava

第三步:打开任务管理器-进程
深入理解JVM(1)—Java虚拟机基本结构

你看到一个叫java.exe的程序没有,是滴这个就是java的虚拟机,java xxx这个命令就是用来启动一个java虚拟机,而main函数就是一个java应用的入口,main函数被执行时,java虚拟机就启动了。
为什么main函数被执行的时候,Java虚拟机就启动了,大家可以看这篇文章http://www.2cto.com/kf/201408/328035.html,具体介绍了Java虚拟机的启动,通俗地讲就是通过main函数,最终启动了JavaMain函数,该函数定义了Java虚拟机初始化的一些参数,从而完成虚拟机的初始化。有的人会说,我做的Java web项目也没见到main函数啊。对于Web应用,也是有main方法的,不过不是在你的程序中,而是在应用服务器中,如tomcat、jboss等。比如tomcat,main函数存在于tomcat的主类Bootstrap类中。

第四步:打开你的ecplise,右键run application,再run application一次

第五步:打开任务管理器-进程
深入理解JVM(1)—Java虚拟机基本结构

好了,我已经圈出来了,有两个javaw.exe,为什么会有两个?因为我们刚才运行了两次run application。这里我是要告诉你,一个java的application对应了一个java.exe/javaw.exe(java.exe和javaw.exe你可以把它看成java的虚拟机,一个有窗口界面一个没有)。你运行几个application就有几个java.exe/javaw.exe。或者更加具体的说,你运行了几个main函数就启动了几个java应用,同时也启动了几个java的虚拟机。

2. Java虚拟机的体系结构

深入理解JVM(1)—Java虚拟机基本结构

大家可以看到Java虚拟机内部的一个处理流程,类加载子系统与执行引擎大家可能不太熟悉,在后面会详细讲到,在这里大家可以简单认为类加载子系统就是加载class字节码文件到虚拟机的过程,包括加载、验证、准备、解析、初始化等。执行引擎是Java虚拟机最核心的组成部分之一,对加载的字节码文件进行处理和解析。
对该图进行解释之前,首先要了解Java虚拟机的结构布局,而要了解Java虚拟机的结构,我们需要知道操作系统的内存结构布局。
深入理解JVM(1)—Java虚拟机基本结构

那么,JVM在操作系统中是如何表示的呢?
深入理解JVM(1)—Java虚拟机基本结构

那么JVM内部又是什么样的布局呢?
深入理解JVM(1)—Java虚拟机基本结构

从上图中,你有没有发现什么规律,JVM的内存结构居然和操作系统的结构惊人的一致。 从这个图,你应该不难发现,原来JVM的设计的模型其实就是操作系统的模型,基于操作系统的角度,JVM就是个该死的java.exe/javaw.exe,也就是一个应用,而基于class文件来说,jvm就是个操作系统,而jvm的方法区,也就相当于操作系统的硬盘区。而java栈和操作系统栈是一致的,无论是生长方向还是管理的方式,至于堆嘛,虽然概念上一致目标也一致,分配内存的方式也一致(new,或者malloc等等),但是由于他们的管理方式不同,jvm是gc回收,而操作系统是程序员手动释放,所以在算法上有很多的差异,后面会讲到jvm的内存分配与回收。

再看下图,
深入理解JVM(1)—Java虚拟机基本结构

将这个图和上面的图对比多了什么?没错,多了一个pc寄存器,我为什么要画出来,主要是要告诉你,所谓pc寄存器,无论是在虚拟机中还是在我们虚拟机所寄宿的操作系统中功能目的是一致的,计算机上的pc寄存器是计算机上的硬件,本来就是属于计算机,(这一点对于学过汇编的同学应该很容易理解,有很多的寄存器eax,esp之类的32位寄存器,jvm里的寄存器就相当于汇编里的esp寄存器),计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址,它甚至可以是操作系统指令的本地地址,当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined,所以你现在应该很明确的知道,虚拟机的pc寄存器是用于存放下一条将要执行的指令的地址(字节码流)。

再对上面的图扩展,这一次,我们会稍微的深入一点,看下面的图,
深入理解JVM(1)—Java虚拟机基本结构

多了什么?没错多了一个classLoader,其实这个图是要告诉你,当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader,如下图,
深入理解JVM(1)—Java虚拟机基本结构

那么方法区中的字节码内存块,除了记录一个class自己的class对象引用和一个加载自己的ClassLoader引用之外,还记录了什么信息呢??我们还是看图,然后我会讲给你听,听过一遍之后一辈子都不会忘记。
深入理解JVM(1)—Java虚拟机基本结构

你仔细将这个字节码和我们的类对应,是不是和一个基本的java类惊人的一致?下面你看我贴出的一个类的基本结构。

    package test;import java.io.Serializable;public final class ClassStruct extends Object implements Serializable {//1.类信息 
//2.对象字段信息
private String name;
private int id;

//4.常量池
public final int CONST_INT=0;
public final String CONST_STR="CONST_STR";

//5.类变量区
public static String static_str="static_str";


//3.方法信息
public static final String getStatic_str ()throws Exception{
return ClassStruct.static_str;
}}

你将上面的代码注解和上面的那个字节码码内存块按标号对应一下,有没有发现,其实内存的字节码块就是完整的把你整个类装到了内存而已。

所以各个信息段记录的信息可以从我们的类结构中得到,不需要你硬背,你认真的看过我下面的描述一遍估计就不可能会忘记了:

1.类信息:

 修饰符(public final) 
是类还是接口(class,interface)
类的全限定名(Test/ClassStruct.class)
直接父类的全限定名(java/lang/Object.class)
直接父接口的权限定名数组(java/io/Serializable)

也就是 public final class ClassStruct extends Object implements Serializable这段描述的信息提取

2.字段信息:

 修饰符(pirvate)
字段类型(java/lang/String.class)
字段名(name)

也就是类似private String name;这段描述信息的提取

3.方法信息:

 修饰符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
参数需要用到的局部变量的大小还有操作数栈大小(操作数栈我们后面会讲)
方法体的字节码(就是花括号里的内容)
异常表(throws Exception)

也就是对方法public static final String getStatic_str ()throws Exception的字节码的提取
4.常量池:

  4.1.直接常量:

1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;

1.2CONSTANT_String_info字符串直接常量池 public final String CONST_STR="CONST_STR";

1.3CONSTANT_DOUBLE_INFO浮点型直接常量池

等等各种基本数据类型基础常量池(待会我们会反编译一个类,来查看它的常量池等。)

4.2.方法名、方法描述符、类名、字段名,字段描述符的符号引用

也就是所以编译器能够被确定,能够被快速查找的内容都存放在这里,它像数组一样通过索引访问,就是专门用来做查找的。

编译时就能确定数值的常量类型都会复制它的所有常量到自己的常量池中,或者嵌入到它的字节码流中。作为常量池或者字节码流的一部分,编译时常量保存在方法区中,就和一般的类变量一样。但是当一般的类变量作为他们的类型的一部分数据而保存的时候,编译时常量作为使用它们的类型的一部分而保存

5.类变量:

 就是静态字段( public static String static_str="static_str";)

虚拟机在使用某个类之前,必须在方法区为这些类变量分配空间。

6.一个到classLoader的引用:

通过this.getClass().getClassLoader()来取得为什么要先经过class呢?思考一下,然后看第七点的解释,再回来思考

7.一个到class对象的引用:

这个对象存储了所有这个字节码内存块的相关信息。所以你能够看到的区域,比如:类信息,你可以通过this.getClass().getName()取得
所有的方法信息,可以通过this.getClass().getDeclaredMethods(),字段信息可以通过this.getClass().getDeclaredFields(),等等,所以在字节码中你想得到的,调用的,通过class这个引用基本都能够帮你完成。因为他就是字节码在内存块在堆中的一个对象

8.方法表:

如果学习c++的人应该都知道c++的对象内存模型有一个叫虚表的东西,java本来的名字就叫c++- -,它的方法表其实说白了就是c++的虚表,它的内容就是这个类的所有实例可能被调用的所有实例方法的直接引用。也是为了动态绑定的快速定位而做的一个类似缓存的查找表,它以数组的形式存在于内存中。不过这个表不是必须存在的,取决于虚拟机的设计者,以及运行虚拟机的机器是否有足够的内存。

3.总结

Java虚拟机在内存中的结构有点类似于操作系统的硬件布局,有着自己的堆、栈、方法区、PC计数器和指令系统。说白了,就是依托现有计算机系统资源,虚拟化一个计算机用来处理Java字节码文件。