JAVA虚拟机体系结构
当java虚拟机运行一个程序时,它需要内存来存储许多东西,如,字节码,从已装载的class文件中得到的其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等等。java虚拟机把这些组织到几个“运行时数据区”。
1、方法区(当虚拟机运行java程序时,会查找在方法区中的类型信息)
由于所有线程都共享方法区,因此它们对方法区数据的访问是线程安全的,如两个线程同时访问里面的L这个类,只有一个线程区装载,另一个需等待。方法区的大小不必时固定的,虚拟机可以根据应用的需要动态调整。方法区也不必时连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。方法区可以被垃圾收集。
方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量
1)存储已被虚拟机加载的类信息(包括这个类型的全限定名,直接超类的全限定名,是类类型还是接口类型,访问修饰符(public,abstract),任何直接超接口的全限定名的有序列表。)
2)常量(常量池)、
----虚拟机必须为每个被转载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,
包括直接常量(string,integer和floating point常量)和对其他类型、字段和方法的符号引用。
运行时常量池是方法区的一部分。
Class文件包括了类的版本,字段,方法,接口等描述信息以及常量池。
常量池用于存放编译器生成的各种字面量和符合引用,这部分内容在类加载后进入方法区的运行时常量池存放。
常量池可以理解为Class文件之中的资源仓库,是Class文件结构中与其他项目关联最多的数据类型。
常量池主要存放两大类常量:字面量(Literal)和符合引用(Symbolic References)。
字面量比较接近java的常量概念,如文本字符串,声明为final的常量值等。
符合引用则属于编译原理方面的概念,包括了三类常量,类和接口的全限定名,字段的名称和描述符,方 法的名称和描述符。
3)字段信息、
----字段名,字段类型,字段的修饰符(public,private ,protected,static,final,volatile,transient的某个子集)
4)方法信息、
---包括(方法名,方法的返回类型,方法参数的数量和类型,方法的修饰符(private ,public,protected,static,final,sysnchronized,native,abstract的某个子集))
如果方法不是本地的和抽象的,还需要存储 方法的字节码,操作数栈和该方法的栈帧中的局部变量区的大小,异 常表。
5)静态变量(除常量以外的所有类(静态)变量)、即时编译器编译后的代码等数据。
6) 一个到类ClassLoader的引用、
7) 一个到Class类的引用。
2、堆
一个java虚拟机只有一个堆空间,所有线程共享这个堆,每个java程序都有它自己的堆空间它们互不干扰,但同一个java程序的多个线程共享着一个堆需要考虑多线程访问堆数据的同步问题
java堆是垃圾回收的主要区域,可以分为:新生代和老年代,更细一点分为:Eden空间,From Survivor空间,To Survivor空间。存储 :对象和数组等,堆中的每个数组对象(还保存数组的长度,数组数据,以及某些指向数组的类数据的引用)
现在的商业虚拟机都采用以下收集算法来回收新生代:
将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。
1)一种可能的堆设计,把堆分为两部分:句柄池 和对象池。如下图,一个对象引用就是一个指向句柄池的本地指针。句柄池的每个条目有两部分:一个指向对象实例变量的指针,一个指向方法区类型数据的指针。这种设计的好处是有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需要更改一下指针指向对象的新地址就可以了---就是在句柄池中的那个指针。缺点是每次访问对象的示例变量都要经过两次指针传递。
2)另一种设计 是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针。它只需要一个指针就可以访问对象的实例数据,但移动对象就变得更加复杂。这种为了减少内存碎片而移动对象的时候,它必须在整个运行时数据区中更新指向被移动对象的引用。
3、程序计数器
对一个java程序,每一个线程都有它自己的PC寄存器(程序计数器),在线程启动时创建,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空(undefined).
当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个Java栈,如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指示下一条将被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态--包括它的局部变量,被调用时传进来的参数,它的返回值,以及运算中间结果等等。
4.Java栈
每当启动一个新线程时,java虚拟机都会为它分配一个java栈,java栈以帧为单位保存线程的运行状态,虚拟机只会直接对java栈执行两种操作:以帧为单位的压栈和出栈。
每当线程调用一个java方法时,虚拟机都会在改线程的java栈找那个压入一个新帧,使用这个帧来存储参数,局部变量,中间运算结果等等。用于存储局部变量,操作数栈,动态链接,方法出口等信息。
java栈由许多栈帧或者说帧组成的,一个栈帧包含一个java方法调用的状态。当线程调用一个java方法时,虚拟机压入一个新的栈帧到该线程的java栈中;当该方法返回时,这个栈帧被从java栈中弹出并抛弃。
栈帧由三部分组成:局部变量区,操作数栈和帧数据区。帧数据区--》除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些信息都保存在帧数据区。
每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。
虚拟机栈的栈元素是栈帧,当有一个方法被调用时,代表这个方法的栈帧入栈;当这个方法返回时,其栈帧出栈。因此,虚拟机栈中栈帧的入栈顺序就是方法调用顺序。什么是栈帧呢?栈帧可以理解为一个方法的运行空间。它主要由两部分构成,一部分是局部变量表,方法中定义的局部变量以及方法的参数就存放在这张表中;另一部分是操作数栈,用来存放操作数。
iconst_1
//把整数 1 压入操作数栈
iconst_2
//把整数 2 压入操作数栈
iadd
//栈顶的两个数相加后出栈,结果入栈
首先什么是 slot?slot 是局部变量表中的空间单位,虚拟机规范中有规定,对于 32 位之内的数据,用一个 slot 来存放,如 int,short,float 等;对于 64 位的数据用连续的两个 slot 来存放,如 long,double 等。引用类型的变量 JVM 并没有规定其长度,它可能是 32 位,也有可能是 64 位的,所以既有可能占一个 slot,也有可能占两个 slot。
putstatic,其作用是给特定的的静态字段赋值
上图 三个线程正在执行,线程1和线程2都正在执行Java方法,而线程3正在执行一个本地方法。
任何线程都不能访问另一个线程的PC寄存器或者Java栈。
5.本地方法栈
本地方法可以通过本地方法接口访问虚拟机的运行时数据区,可以直接使用本地处理器中的寄存器,和虚拟机拥有同样的权限或者是能力,任何本地方法接口都会调用本地方法栈,当线程调用java方法时,虚拟机会创建一个新的栈帧并压入java栈,当它调用的是本地方法时,虚拟机保持java栈不变,不再在线程的java栈压入新的栈,虚拟机只是简单地动态连接并直接调用指定的本地方法。本地方法栈占用的内存区不必是固定大小的,可以动态扩展或收缩,
直接内存
直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。直接内存的大小不受堆的大小限制,受到本机总内存(包括RAM以及SWAP区或分页文件)大小以及处理器寻址空间的限制。