Java虚拟机的体系结构
1.Java虚拟机浅陋的见解
Java虚拟机是一个抽象的规范概念,设计者只是用一些规范来定义这些抽象的组成部分以及他们之间的交互。在创建运行一个Java程序的时候,也就创建了一个java虚拟机实例,也就是在内存中分配一个空间供给这个Java程序使用。把虚拟机分为不同的部分也是为了程序员更加清楚的了解在我们Java程序跑起来的时候,虚拟机内部时怎么处理这些Java代码的。也许在实际的虚拟机内部中,这每个部分的所处位置和内存分配是复杂无序的,但是Java虚拟机的设计者给这些内部结构之间建立联系就可以使一段java代码有条无紊的运行。
2.Java虚拟机的体系结构
在Java虚拟机规范中,一个虚拟机实例的行为是按照子系统,内存区,数据类型以及指令这几个术语来描述的。这些组成部分一起展示了抽象的内部抽象体系结构。这个规范是任何Java类都必须遵守的行为。这篇博客主要介绍内存区。
Java虚拟机的内部体系结构图
当Java虚拟机运行一个程序时,他需要使用内存来储存很多多东西,例如:字节码,从已装载的class文件中得到其他信息(比如对其他类进行引用),程序创建的对象,方法的参数,返回值,局部变量,运算时的返回结果等等。这些数据在运行时都会存在不同的部分,我们称为运行时的数据区,这样做的目的就是方便管理。
某些运行时的数据区是由程序线程汇总共享的,比如方法区和堆。还有一些只是一个线程拥有的,其他线程不能访问,比如栈和PC寄存器。这些数据区都是在虚拟机实例被创建的时候创建,关于这些数据区将在后面一一解释。
2.1.方法区
方法区主要的作用就是当虚拟机使用某个类加载器将Class文件装载到虚拟机中,然后通过虚拟机分析并提取其中的类型信息,存至方法区,类变量也存至方法区。要注意以下几点
①由于方法区是线程共享的,所以对方法区的数据进行访问的时候为了保证线程安全,必须对加锁。比如当两个线程都去 访问一个类的时候,而此时这个类还没被加载到虚拟机中,那么此时只能有一个线程去加载这个类,另外一个线程只能 等待。
②方法区的大小是不固定,且内存可以不是连续的。虚拟机可以根据需求进行动态分配。程序员可以自己通过参数设置。
③一般来说垃圾收集大部分都会堆堆进行(后面的博客会总结垃圾回收),但是在有时候方法区也可以被进行垃圾回收。 因为在一个程序的运行时,有时候用户也会通过自定义的类加载器来动态扩展java程序,因此一些类也会成为程序“不再 引用的类"。这个时候就需要对方法区的空间进行回收。
类型信息:对于每个装载的类型,虚拟机都会在方法区中储存以下信息
这个类的全限定名(完整的包名加”.“隔开) |
这个类型的直接超类的全限定名(java.lang.Object类没有超类) |
这个类是类类型还是接口类型 |
类型的访问修饰符 |
任何直接超类接口的全限定名的有序列表 |
除了上述的基本类类型以外,还储存以下信息。
该类型的常量池:常量池就是该类型所用常量的一个有序集合。 |
字段信息:字段名,字段类型,字段修饰符 |
方法信息:方法名,方法返回类型,方法参数数量和类型,方法修饰符 |
除了常量以外的所有类(静态)变量 |
一个到类Classloader的引用:简单来说就是说明时让那一个加载器加载。 |
一个到class类的引用:对于每一个被装载的类型,虚拟机都会相应的为他创建一个java.lang.Class类的实列,储存在方法区。 |
方法区使用示例
public class EXAM{
private int speed = 5;
void flow(){
}
class Mmm{
public static void main(String args[]){
EXAM exam = new EXAM();
EXAM.flow;
}
}
}
要运行Mmm程序,首先要将这个Mmm的全限定名告诉虚拟机,之后虚拟机找到Class文件。然后从导入的二进制数据中提取类型信息并放到方法区,虚拟机在执行main()方法,并在执行时,他会一直持有指向当前类的常量池指针。
2.2、堆
Java程序在运行时创建的所有实体类或数组都放在同一个堆中,就像上面代码中EXAM实例化出来的一个对象。Java一个虚拟机只有一个堆,所有堆是线程共享的。又由于一个程序独享一个虚拟机实例,因此每个Java程序都有自己的堆空间。和C++不一样,JAVA中只有创建对象的实例,但是没有释放对象的指令。虚拟机有自己的方法来决定什么时候回收对象所占的内存,通常虚拟机会把这个任务交给垃圾收集器,这在以后会总结。
对象的内部表示:
java虚拟机并没有规定Java对象在堆中如何表示。Java对象中包含的基本数据类型和他所属的类及其所有超类申明的实例变量组成。只要有一个对象的引用,就能够快速定位对象的实例数据。另外,它也必须能够通过该对象引用访问相应的类数据,因此在对象中通常都会洋浦指向方法区的指针。
表示方法第一种方法:
把堆分为一个句柄池一个对象池。而每一个对象的引用就是一个指向句柄池的本地执政。每一个句柄池条目分为两个部分:一个指向对象实例变量的指针,一个指向方法区类型数据指针(可以理解各自指向位置的一个地址)。这样设计的好处时有利于堆碎片的整理,当移动对象池中的对象时(比如说链表的移动),句柄部分只要更改以下指向的新地址就可以了。但是缺点是:每次访问对象的实例变量都要经过两次指针移动。
划分为句柄池和句柄池的对象
第二种表示方法:
这种方法时使对象指针直接指向一组数据,而该数据包括实例数据以及指向方法区中类数据的指针,这样做的优缺点正好于前面的方法相反,他只需要一个指针就可以访问对象的实例数据,但是移动数据就变得麻烦了。当虚拟机为了减少内存碎片而移动对象的时候,他必须在整个运行时的数据区中更新指向被移动的对象引用。
那么我们为什么要求虚拟机能够通过对象引用的到类(类型)数据?按理说对象的引用不应该就是对应的一个实例化的类的对象吗?因为当程序运行时需要转换某个对象引用为另外一种类型时,就是我们说的类型转换。虚拟机要检查这种转换是否被允许,被转换的对象是否的确时被引用的对象或者是他的超类型。当程序执行insanceof操作,也要进行检查。最后,在程序中调用某个实例方法时,虚拟机要进行动态绑定,也就是他不能按照引用类型来决定调用的方法,而是要根据对象实际类。
不管虚拟机的实现使用了什么样的对象表现手法,很可能每个对象都有一个调用实例对象的方法表,加块调用方法的速度。但是对于内存资源限制严格的情况下,没有足够的资源储存方法表,只能使用一个指向对象的引用,也可以很快的访问对象的方法表。
在上图显示的还有另外两种数据:
①堆上的对象数据上面还有一个逻辑部分,那就是对象锁,这是一个互斥对象。因为堆在虚拟机中时线程共享的,所有必 须要保证线程安全。
②还有一种数据类型,是与垃圾回收算法有关的,给做标记的附加数据(下一篇总结垃圾收集再展开讨论)。
2.3、程序计数器
对于一个Java程序来说,其中的每一个线程都有自己的的程序计数器。当线程执行一个Java方法时,pc寄存器的内容总是指向被执行的指令的地址。代码的分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
2.4、Java栈
每当启动一个新的线程的时候,Java虚拟机都会为它分配一个Java栈。Java栈是以栈帧为单位保存线程的允许状态。Java虚拟机会对栈做两种操作:以栈帧为单位压栈和出栈。每当线程调用一个Java方法时,虚拟机都会在该线程的栈中压入一个新的栈帧,而现在这个方法就成了新栈。当这个方法在执行的过程中会使用这个帧来存储参数,局部变量等。Java方法可以有两种方法返回,return或者是异常返回,那么这时候虚拟机都会弹出当前栈帧然后释放,上一个方法的栈又成了当前栈。
注意两点:
①Java栈和方法区和堆一样,内存也是不连续的,具体的数据结构和大小都是程序员自己定义的。
②Java栈时线程独享的,任何线程都不能访问其他线程的栈数据,所以我们的局部变量到了方法外面就失去了作用。
栈帧
栈帧是由局部变量区,操作数栈和栈数据区构成。局部变量区和操作数栈的大小要根据方法而定。当虚拟机调时用一个方法它从对应的类型信息中的到此方法的局部变量区和操作数的大小,并据此分配栈帧内存,然后压入栈中。
注意:Java栈和帧都可以储存数据,还要分区的原因
①在软件设计的角度中,JVM栈代表了处理逻辑,而堆才是数据,这样分开可以使得处理逻辑更加清晰,这种隔离分模块 的思想在软件设计的方方面面都有体现。
②JVM堆和栈分离,可以使堆的数据可以被多个现场访问,起到了缓存的作用,数据拱栈共享。
2.5、本地方法栈
本地方法栈的功能:
为线程私有,功能和虚拟机栈非常类似。线程在调用本地方法时,来存储本地方法的局部变量表,本地方法的操作数栈等等信息。
本地方法栈的定义和作用:
简单地讲,一个本地方法是这样一个方法:该方法的实现由非java语言实现,比如C语言实现。很多其它的编程语言都有这一机制,比如在C++中,你可以告知C++编译器去调用一个C语言编写的方法。
我们知道java是高级编程语言,当对一些底层的如操作系统或某些硬件交换信息时,我们使用java来编程实现起来不容易,再者使用java来编程效率也很低下。这就不得不需要调用本地方法来解决这一问题。
虚拟机的运行需要栈和堆以及其他部分的合作完成。JVM的体系结构和内存分配管理时Java的核心技术之一。此文章参考《解析Java虚拟机开发》一书