JVM学习之运行时数据区(3)

一 共享区和私有区

 

1 线程共享区: 方法区 堆 

        每个java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有线程共享的.当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息。然后把这些类型信息放到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象(包括Class类实例)都放到堆中。

        由于所有线程都共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。比如,假设同时有连个线程都企图访问一个名为Lava的类,而这个类还没有被装入虚拟机,那么这是只应该有一个线程去装载它,而另外一个线程则只能等待。

 

2 线程私有区: java栈 PC寄存器 本地方法栈

 

(1)私有内存区: 当每一个新线程被创建时,Java虚拟机为每一个线程创建其私有的内存区,即线程的PC寄存器(程序计数器)以及Java栈,私有是指任何线程都不能访问另一个线程的PC寄存器或者Java栈

 

(2)PC寄存器: 如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指向下一条将被执行的指令,当线程正在执行本地方法时,对应的PC寄存器的值是不确定的(undefine).

 

(3)Java栈: Java栈总是存储对应线程中Java方法调用的状态.包括它的局部变量,被调用时传进来的参数、返回值,以及运算的中间结果等等。而本地方法调用的状态,则是以某种依赖于具体实现的方法存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。

    

(4)栈帧及栈帧结构: 

    Java栈是由许多栈帧组成的,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧被从Java栈中弹出并抛弃。

    ■■■栈帧结构

    

(5)Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。■这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑、同时也便于Java虚拟机在那些只有很少通用寄存器的平台上实现。另外,Java虚拟机这种基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。

 

 

二 方法区解析

 

1 方法区相关知识点:

 

(1)方法区存储的内容是以类为单位的

 

(2)方法区可以被垃圾收集,因为虚拟机允许通过用户定义的类装载器来动态扩展java程序,因此一些类也会成为程序"不再引用"的类。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。

 

2 方法区存储内容概览

 

对每个装载的类型,虚拟机都会在方法区中存储以下类型信息:

(1)这个类型的全限定名

(2)这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)

(3)这个类型是类类型还是接口类型

(4)这个类型的访问修饰符(public、abstract或final的某个子集)

(5)任何直接超接口的全限定名的有序列表

 

除了上面列出的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息:

(6)该类型的运行时常量池

    虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量和对其他类型、字段和方法的符号引用。

 

(7)字段信息

    有字段名,字段类型,字段修饰符等

 

(8)方法信息

    有方法名,方法的返回类型(或void),方法参数的数量和类型(按声明顺序),方法修饰符;

    如果方法不是抽象的和本地的,还须保存方法的字节码,操作数栈和该方法的栈帧中的局部变量区的大小,异常表等

 

(9)★除了常量以外的所有类(静态)变量

    类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。它们总是作为类型信息的一部分而存储在方法区。除了在类中声明的编译时常量外(被放在常量池中或直接嵌入到方法的字节码流),虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。

 

(10)一个到ClassLoader的引用

    虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。■为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。

 

(11)一个到Class类的引用

    对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例保存在堆中,并且虚拟机以某种方式把这个实例和存储在方法区中的类型数据关联起来,从而在程序中可以通过Class类实例访问存储在方法区中的类的信息.

 

3 常量池与动态连接

    在动态连接的过程中,当虚拟机执行A类的方法时,会持有A类对应类信息在方法区的地址的指针,当要在A类的方法中构造类B的对象时,先查找A类在方法区的类信息中的常量池对应的B类的相关信息,如果是符号引用,那么要替换成直接指向B类对应在方法区中的类信息地址的指针(一般是Class实例的指针),这时虚拟机会分析类B有没有被装载,如果没有,那么先通过符号引用所标识的B类的类命名的字符串装载B类,再将类信息保存

在方法区中,并将其Class实例指针替换A类的常量池中的B的符号引用.

    注意,这个指针是本地的,常称为"直接引用"

 

 

三 堆解析

 

1 JAVA虚拟机规范并没有规定JAVA对象在堆里是如何表示的,它由虚拟机的实现者决定。

 

2 堆空间的设计参考

    一种可能的堆空间设计就是,把堆分为两部分:一个句柄池,一个对象池。一个对象引用就是指向句柄池的本地指针.句柄池的每个条目有两部分: 一个指向对象实例变量的指针,一个指向方法区类型数据的指针.这种设计的好处是有利于堆碎片的整理,缺点是每次访问对象的实例变量都需要经过两次指针传递。

    另一种设计方式是使对象指针直接指向一组数据,而数据包括对象实例数据以及指向方法区类数据的指针。这种设计方式的优点是只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂(因为移动对象后,必须在整个运行时数据区中更新指向被移动对象的引用,当引用很多时就会造成程序运行效率下降,不能在句柄池直接一次性修正)。

 

3 JAVA对象的数据结构

 

(1)JAVA对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成(★注意类变量是在方法区的,所有超类声明的实例变量包括■隐藏的实例变量)。

 

(2)可通过保存在对象数据结构的指向存储于方法区的类型信息的指针访问类型信息,这个指针可以先指向一个类对应的方法表,方法表中的项由类信息在方法区的地址(指针)以及类的对象的所有可调用的实例方法(■包括自己的和超类的非final和private方法)的指针组成,这样就能让jvm快速定位对象所调用的方法,提高程序运行速度,但这样会消耗较多的内存.

 

(3)要求虚拟机必须能够通过对象引用得到类数据的理由    

    当程序在运行时需要转换某个对象引用为另一种类型时,虚拟机必须要检查这种转换是否被允许,被转换的对象是否的确是被引用的对象或者它的超类型。

    当程序在执行instanceof操作时,虚拟机也进行了同样的检查。

    当程序中调用某个实例方法时,虚拟机必须迸行动态绑定,换句话说,它不能按照引用的类型来决走将要调用的方法.而必须根据对象的'实际类'执行正确的方法

    ■private 方法 final方法 是属于静态绑定的...动态绑定与静态绑定的机制有啥区别呢...调用静态方法与调用实例方法又有什么区别呢

 

(4)对象数据结构的其他数据

    方法表: 每个对象可能都有一个方法表,加快调用实例方法时的效率;

 

    对象锁数据: 用于协调多个线程访问同一个对象时的同步,因为很多对象在其整个生命周期内都没有任何线程加锁,所以般只有当第一次需要加锁的时候才分配对应的锁数据,jvm则需要用某种间接方法将对象数据与锁数据关联.

 

    等待集合数据: 每个类都从Object那里继承了三个名为wait()的重载的等待方法和两个通知方法notify()和notifyAll(),当某个线程在一个对象上调用等待方法时,jvm就阻塞这个线程,并把它放在了这个对象的的等待集合中.直到另一个线程在同一个对象上调用通知方法,jvm才会在之后的某个时刻唤醒一个或多个在等待集合中被阻塞的线程.正象锁数据一样,很多jvm实现把等待集合数据与实际对象数据分开,只有在需要时才为此对象创建同步数据(通常是在第一次调用等待方法或通知方法时)

 

    垃圾收集器技术需要的数据:垃圾收集器必须以某种方式跟踪程序引用的每个对象,需要的数据的类型要视垃圾收集使用的算法而定.

 

 

4 数组对象的表示

(1)与其他对象一样,数组的数据结构也是保存在堆中,类型信息保存在方法区中,数组数据结构在堆中的实现形式由设计者决定

(2)数组的类型: 所有具有相同维度和类型的数组都是同一个类的实例,数组各维的长度只与实例数据有关

(3)数组的名称: 由[代表一个单位维度,最后用一个大写字母,或是类全名代表元素的类型

(4)多维数组被表示为数组的数组

(5)在堆中每个数组对象还必须保存的数据是数组长度,数组数据,及某些指向数组的类数据的引用等等

 

 

四 程序计数器

(1)对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC(程序计数器)寄存器,它是在该线程启动时创建的。

(2)PC寄存器的大小是一个字长,因此它既能够持有一个本地指针,也能够持有一个returnAddress

(3)当线程执行某个Java方法时,PC寄存器的内容总是下一条将被执行指令的“地址”,这里的“地址”可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的是“undefined”。

 

 

五 java栈及栈帧

 

1 java栈上的所有数据都是线程私有的,所以对方法的局部变量或参数的访问是不需要考虑多线程访问同步的问题

 

2 当虚拟机调用一个java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入java栈中,这个栈帧可以在堆里创建,也可以在一个连续的栈中分配,从而使两个栈栈帧的区域可重叠,节省内存

 

3 栈帧

    

(1)局部变量区:

    java栈帧的局部变量区被组织成一个以字长为单位、从0开始计数的数组。字节码指令通过以0开始的索引来使用其中的数据。类型为int、float、refence和returnAdress的值在数组内只占据一项。而类型为byte、short、char、boolean的值在存入数组时都先转换为int值,因此同样只占据一项。而类型为long和double的值在数组中却占据了连续两项(指令只需指出连续两项中第一项的索引值)

    需要注意的是,虚拟机并不直接支持boolean类型,因此编译器总是用int来表示boolean,但java虚拟机对byte,short,char是直接支持的,这此类型的值在局部变量区和操作数栈中都会被转换成int类型的值,只有当它被存回堆或方法区时,才会转换回原来的类型.

    局部变量区包含对应方法的参数和局部变量.■(为什么是编译器,不是运行时数据区吗)编译器首先按声明的顺序把这些参数放入局部变量数组.实例方法还维护一个this参数引用,索引为0.而类方法中就没有这个隐含的this变量,所以不能直接通过类方法访问类实例的变量.

    

(2)操作数栈: 

    和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组.但不同的是它是用栈及栈操作维护虚拟机执行指令时的操作数,中间结果等.而虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的.

    虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈.

    

(3)帧数据区: 用于常量池解析(如引用解析,帧数据区保存了指向常量池的指针),方法返回(如保存发起调用的方法对应的栈帧在堆中的位置),异常派发(保存一个对此方法异常表的引用)和调试相关数据存储等

 

(4)关于字长

    在32位java虚拟机中,最基本的数据单元是word,简称字。一个字大小被称作字长,32位java虚拟机的字长是4字节。也就是说,在java虚拟机中,每四个字节为一个单位,如果你存储一个byte类型,虽然byte大小事1字节,但他也要占用4字节,byte类似在被存储到虚拟机的那一刻会被转换为int。同样,如果存储一个short类型,他也会占用4字节,并且隐式转换为int类型

    java虚拟机规范没有指定局部变量区的数据结构的具体形式,假如某个虚拟机实现的字长为64位,这时就可以把整个long或double数据放在数组中相邻两数组项的低项内,而使高项保持为空.

    

 

六 本地方法栈

1 任何本地方法接口都会使用某种本地方法栈,如果某个虚拟机实现的本地方法接口是用C连接模型的话,那么它的本地方法栈就是C栈

 

2 很可能java方法调用本地方法后,本地方法接口需要回调java虚拟机中的java方法,在这种情形下,该线程会保存本地方法栈的状态并进入到另一个java栈,但虚拟机会跟踪这三个栈之间的联系

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值