JVM运行时数据区和各个区域的作用
一、JVM主要分为5个核心区域(6个子区域),分别是:程序计数器Java虚拟机栈本地方法栈Java堆方法区*运行时常量池(属于“方法区”的一部分)二、各个区域作用和描述序号区域名称共享作用异常备注1程序计数器线程私有记录当前线程锁执行的字节码行号指示器。Java虚拟机规范中唯一一个...
一、JVM主要分为5个核心区域(6个子区域),分别是:
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
- *运行时常量池(属于“方法区”的一部分)
二、各个区域作用和描述
序号 | 区域名称 | 共享 | 作用 | 异常 | 备注 |
---|---|---|---|---|---|
1 | 程序计数器 | 线程私有 | 记录当前线程锁执行的字节码行号指示器。 | Java虚拟机规范中唯一一个没有规定OutOfMemoryError(内存不足错误)的区域。 | -- |
2 | Java虚拟机栈 | 线程私有 | 存放局部变量表、操作数据栈、动态链接、方法出口等信息。 | 栈深大于允许的最大深度,抛出StackOverflowError(栈溢出错误)。 内存不足时,抛出OutOfMemoryError(内存不足错误)。 | 常说的“栈”说的就是Java虚拟机栈,或者是Java虚拟机栈中的局部变量表。 |
3 | 本地方法栈 | 线程私有 | 和Java虚拟机栈类似,不过是为JVM用到的Native方法服务。 | 同上 | -- |
4 | Java堆 | 线程共享 | 存放实例化数据。 | 内存不足时,抛出OutOfMemoryError(内存不足错误)。 | 通过-Xmx和-Xms控制大小。 GC的主要管理对象。 |
5 | 方法区 | 线程共享 | 存放类信息(版本、字段、方法、接口等)、常量、静态变量、即时编译后的代码等数据。 | 内存不足时,抛出OutOfMemoryError(内存不足错误)。 | -- |
6 | 运行时常量池 | 线程共享 | 存放编译期生成的各种字面量和符号引用。 | 内存不足时,抛出OutOfMemoryError(内存不足错误)。 | 属于“方法区”的一部分。 |
7 | 直接内存 | -- | 如NIO可以使用Native函数库直接分配堆外内存,该内存受计算机内存限制。 | 内存不足时,抛出OutOfMemoryError(内存不足错误)。 | 不是JVM运行时数据区的一部分,也不是JVM虚拟机规范中定义的内存区域。但这部分内存也被频繁的使用。所以放到一起。 |
*参考《深入理解Java虚拟机 JVM高级特性与最佳实践》一书
JVM在执行JAVA程序时会把它管理的内存区域划分为若干个不同的数据区域,统称为运行时数据区,由图可见JVM程序所占的内可划分成5个部分:程序计数器、虚拟机栈(线程栈)、本地方法栈、堆(heap)和方法区(内含常量池),其中方法区和堆被所有线程共享。下面分别介绍各部分的功能:
程序计数器
JVM是多线程的,每一个线程都有一个独立的程序计数器(JVM是多线程的,为了线程切换后能恢复到正确的执行位置),是一块较小的内存空间,它与线程共存亡。JVM中的程序计数器指向的是正在执行的字节码地址,可以看作是当前线程所执行的字节码的行号指示器。
如果线程正在执行的是一个java方法,程序计数器记录的是正在执行的虚拟机字节码指令地址;
若线程执行的是Native方法,程序计数器则为Undefined。
程序计数器是JVM中唯一一个没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈
一个线程一个栈,并且生命周期与线程相同。它的内部由一个个栈帧构成,一个栈帧代表一个调用的方法,线程在每次方法调用执行时创建一个栈帧然后压栈,栈帧用于存放局部变量、操作数、动态链接、方法出口等信息。方法执行完成后对应的栈帧出栈。我们平时说的栈内存就是指这个栈。
一个线程中的方法可能还会调用其他方法,这样就会构成方法调用链,而且这个链可能会很长,而且每个线程都有方法处于执行状态。对于执行引擎来说,只有活动线程栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧关联的方法称为当前方法(Current Method)。
栈帧的大致结构如下图所示:
每一个栈帧的结构都包括了局部变量表、操作数栈、方法返回地址和一些额外的附加信息。某个方法的栈帧需要多大的局部变量表、多深的操作数栈都在编译程序时完全确定了,并且写入到类方法表的相应属性中了,因此某个方法的栈帧需要分配多少内存,不会受到程序运行期变量数据变化的影响,而仅仅取决于具体虚拟机的实现。
局部变量区域:存储方法的局部变量和参数,存储单位以slot(4 byte)为最小单位。局部变量存放的数据类型有:基本数据类型、对象引用和return address(指向一条字节码指令的地址)。其中64位长度的long和double类型的变量会占用2个slot,其它数据类型只占用1个slot。
类的静态方法和对象的实例方法被调用时,各自栈帧对应的局部变量结构基本类似。但有以下如图示区别:实例方法中第一个位置存放的是它所属对象的引用,而静态方法则没有对象的引用。另外静态方法里所操作的静态变量存放在方法区。
void test(Object object)
{int i=0;
Boolean b=false;
}
static void test1(int i ,Object object,boolean b)
{
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
关于局部变量表,还有一点需要强调,就是局部变量不像类的实例变量那样会有默认初始化值。所以局部变量需要手工初始化,如果一个局部变量定义了但没有赋初始值是不能使用的。
操作数栈: 所谓操作数是指那些被指令操作的数据。当需要对参数操作时如c=a+b,就将即将被操作的参数数据压栈,如将a 和b 压栈,然后由操作指令将它们弹出,并执行操作。虚拟机将操作数栈作为工作区。Java虚拟机没有寄存器,所有参数传递、值返回都是使用操作数栈来完成的。
Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。
例如:
public static int add(int a,int b){
int c=0;
c=a+b;
return c;
}
add(25,23);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
主要操作步骤:
压栈步骤:
0: ....
1: iload_0 // 把局部变量0压栈,int a;
2: iload_1 // 局部变量1压栈,int b;
3: iadd //弹出2个变量,求和,结果压栈48
4: istore_2 //弹出结果,放于局部变量2;int c;
5: ...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
动态连接:它是个指向运行时常量池中该栈帧所属方法的引用,这个引用是为了支持方法调用过程中能进行动态连接。我们知道Class文件的常量池存有方法的符号引用,字节码中的方法调用指令就以指向常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。余下部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法返回地址:
正常退出,执行引擎遇到方法返回的字节码,将返回值传递给调用者;
异常退出,遇到Exception,并且方法未捕捉异常,返回地址由异常处理器来确定,并且不会有任何返回值。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
额外附加信息:虚拟机规范没有明确规定,由具体虚拟机实现。
Java虚拟机规范规定该区域有两种异常:
StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出
OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出
另外需要提醒一下,在规范模型中,栈帧相互之间是完全独立的。但在大多数虚拟机的实现里都会做一些优化处理,这样两个栈帧可能会出现一部分重叠。这样在下面的栈帧会有部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以有部分数据共享,而无须进行额外的参数复制传递了。具体情形如下图所示:
本地方法栈
Java可以通过java本地接口JNI(Java Native Interface)来调用其它语言编写(如C)的程序,在Java里面用native修饰符来描述一个方法是本地方法。本地方法栈就是虚拟机线程调用Native方法执行时的栈,它与虚拟机栈发挥类似的作用。但是要注意,虚拟机规范中没有对本地方法栈作强制规定,虚拟机可以自由实现,所以可以不是字节码。如果是以字节码实现的话,虚拟机栈本地方法栈就可以合二为一,事实上,OpenJDK和SunJDK所自带的HotSpot虚拟机就是直接将虚拟机栈和本地方法栈合二为一的。
Java虚拟机规范规定该区域也可抛出StackOverFlowError和OutOfMemoryError。
堆
这个区域用来放置所有对象实例以及数组,不过在JIT(Just-in-time)情况下有些时候也有可能在栈上分配对象实例。堆也是java垃圾收集器管理的主要区域(所以很多时候会称它为GC堆),被所有线程共享。
从GC回收的角度看,由于现在GC基本都是采用的分代收集算法,所以堆内存结构还可以分块成:新生代和老年代;再细一点的有Eden空间、From Survivor空间、To Survivor空间等。如下图:
对象在堆内分配内存的两种方法:
为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
指针碰撞(Serial、ParNew等带Compact过程的收集器)
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
空闲列表(CMS这种基于Mark-Sweep算法的收集器)
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
方法区
它是虚拟机在加载类文件时,用于存放已加载的类的类信息,常量,静态变量,及jit编译后的代码(类方法)等数据的内存区域,是线程共享的。
方法区存放的信息包括:
1.类的基本信息:
每个类的全限定名
每个类的直接超类的全限定名(可约束类型转换)
该类是类还是接口
该类型的访问修饰符
直接超接口的全限定名的有序列表
2.已装载类的详细信息:
3.运行时常量池:
类信息除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量、符号引用,文字字符串、final变量值、类名和方法名常量,这部分内容将在类加载后存放到方法区的运行时常量池中。它们以数组形式访问,是调用方法、与类联系及类的对象化的桥梁。
这里再讲一下,JDK1.7之前运行时常量池是方法区的一部分,JDK1.7及之后版本已经将运行时常量池从方法区中移了出来,在堆(Heap)中开辟了一块区域存放运行时常量池。
运行时常量池除了存放编译期产生的Class文件的常量外,还可存放在程序运行期间生成的新常量,比较常见增加新常量方法有String类的intern()方法。String.intern()是一个Native方法,它的作用是:如果运行时常量池中已经包含一个等于此String对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此String内容相同的字符串,并返回常量池中创建的字符串的引用。不过JDK7的intern()方法的实现有所不同,当常量池中没有该字符串时,不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录堆中首次出现的该字符串的引用,并返回该引用。
4.字段信息:
字段信息存放类中声明的每一个字段(实例变量)的信息,包括字段的名、类型、修饰符。
如private String a=“”;则a为字段名,String为描述符,private为修饰符。
5.方法信息:
类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量表、操作数栈大小等完全确定并存放在字节码中,在加载载的时候,随着类一起装入方法区。)
在运行时,虚拟机线程调用方法时从常量池中获得符号引用,然后在运行时解析成方法的实际地址,最后通过常量池中的全限定名、方法和字段描述符,把当前类或接口中的代码与其它类或接口中的代码联系起来。
5.静态变量:
就是类变量,被类的所有实例对象共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块。
6.到类ClassLoader的引用:到该类的类装载器的引用。
7.到类Class的引用:虚拟机为每一个被装载的类型创建一个Class实例,用来代表这个被装载的类。
Java虚拟机规范规定该区域可抛出OutOfMemoryError。
直接内存
直接内存(Direct Memory)虽然不是程序运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁使用,而且它也可能导致OutOfMemoryError异常出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native方法库直接分配堆外内存,然后通过一个存储在Java堆里面的DirecByteBuffer对象作为这块内存的引用进行操作。这样能在某些应用场景中显著提高性能,因为它避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制,从而导致动态扩展时出现OutOfMemoryError异常。
总结
在程序运行时类是在方法区,实例对象本身在堆里面。
方法字节码在方法区。线程调用方法执行时创建栈帧并压栈,方法的参数和局部变量在栈帧的局部变量表。
对象的实例变量和对象一起在堆里,所以各个线程都可以共享访问对象的实例变量。
静态变量在方法区,所有对象共享。字符串常量等常量在运行时常量池。
各线程调用的方法,通过堆内的对象,方法区的静态数据,可以共享交互信息。
各线程调用的方法所有参数传递、方法返回值的返回,都是使用栈帧里的操作数栈来完成的。
-------------------------------------------------------------------------------------------------------------------------------
3.1.1 程序计数器
Program Counter Register
内存空间小,线程私有.它可以看做是当前线程所执行的字节码的行号指示器.也就是说,线程主要是执行任务,而执行到哪里,需要使用程序计数器来记录.字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成.
由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,所以,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,所以我们说,它是线程私有的.
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
3.1.2 虚拟机栈
java virtual Machine Stacks
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,因此一个栈帧需要分配多少内存,不会受程序运行时期变量数据的影响.
一个线程中的方法调用链可能会很长,很多方法都处于同时执行状态.对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,执行引擎运行的所有的字节码指令都只针对当前栈帧来进行操作的.
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量.
操作数栈
Operand Stack
是一个后进先出栈.其最大深度在编译的时候已经确定了.当一个方法刚刚开始执行的时候,这个方法的操作数占是空的,在方法的执行过程中,会有各种字节码指令往操作数占中写入和提取内容,这就是出栈/入栈动作.
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的,但大多数虚拟机的实现都会做一些优化处理,让两个栈帧出现部分重叠,让下面的栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用的时候就可以共用一部分数据.无须进行额外的参数的复制传递.
image
动态连接
每一个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用.持有这个引用是为了支持方法调用过程中的动态连接.
这个引用是一个符号引用,不是方法实际运行的入口地址,需要动态的找到具体的方法入口.
这个特性给java带来了更强大的动态扩展能力,但也使得java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能够确定目标方法的直接引用.
方法返回地址
正常完成出口:方法正确执行,执行引擎遇到方法返回的指令,回到上层的方法调用者.
异常完成出口:方法执行过程中发生异常,并且没有处理异常,这样是不会给上层调用者产生任何返回值.
方法正常退出,将会返回程序结束其的值给上层方法,经过调整之后以指向方法调用指令后面的一条指令,继续执行上层方法.
3.1.3 本地方法栈
Native Method Stack
区别于Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
3.1.4 堆
heap
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部可以设置划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
从内存回收的角度来看,由于现在收集器基本上都采用分代收集算法,所以对空间还可以细分为:新生代(年轻代),老年代(年老代).再细致一点,可以分为Eden空间,From Survivor空间, To Survivor空间.
不论如何划分,都与存放内容无关,都是存放的是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存.
3.1.5 方法区
Method Area
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation)(java8之前,使用永久代来实现方法区,在java8之后,废除永久代,将字符串常量池移动到堆中,并新增Meta space,直接在系统内存中.),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。
Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。
3.1.6 运行时常量池
Runtime Constant Pool
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。内存有限,无法申请时抛出 OutOfMemoryError.
3.1.7 直接内存
Direct Memory
非虚拟机运行时数据区的部分.
在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
本机直接内存的分配不会受到java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存大小的限制.所以我们在配置虚拟机参数时,不要忽略直接内存,否则可能因为动态扩展导致出现OutOfMemoryError.
3.2 基于栈的执行过程分析
下面我们通过一个小程序,来分析一下,虚拟机中实际是如何执行代码的.
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
代码非常简单,我们可以直接使用javap命令(javap -c CalcTest.class > calc.txt),通过反汇编操作,来查看对应的字节码指令.
image
查阅虚拟机字节码指令表,我们先将上面的反汇编代码翻译一下:
image
我们从这段程序的执行中,也可以回过来再次认识栈结构.整个过程的中间变量都是以操作数栈的出栈,入栈为信息交换途径.
3.3 HotSpot虚拟机对象探秘
堆,是我们最实用的一块内存空间,分析完栈帧的执行过程之后,现在我们再来分析一下,虚拟机在java堆中对象的创建,布局和访问的过程.
3.3.1 对象的创建
Java是一门面向对象的语言,在运行过程中无时无刻都有对象的创建,在语言层面,仅仅是一个关键字new,那么在虚拟机中,对象是如何创建出来的呢?
① 检查类是否已经被加载:虚拟机遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
② 为新生对象分配内存:类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。
如果堆内存绝对规整,使用指针碰撞.否则使用空闲列表,找到一块足够大的内存划分给对象实例.
image
堆内存是否规整,主要是看GC回收了内存之后是否包含压缩或者整理功能.如果有,那么内存就比较规整.否则如果没有,创建对象就需要采用空闲列表的方式.
比如:
serial,ParNew等带有整理的收集器,可以使用指针碰撞.
CMS使用简单清除的算法,可以使用空闲列表.
如果线程支持在堆中都有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
③ 内存空间分配完成后会将整个空间都初始化为零值(不包括对象头).
④ 接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
⑤ 执行 new 指令后,执行 init 方法(由字节码指令invokespecial决定,执行初始化方法)后,才算一份真正可用的对象创建完成.
3.3.2 对象的内存布局
在上文中,我们讲到一个步骤是,填充对象头.那什么是对象头呢?
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding).
对象头(Header):
包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,根据不同的系统为数固定大小.官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。
实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。
3.3.3 对象的访问定位
使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。
由于java虚拟机只规定要一个执行对象的引用,而没有规定以何种方式去定位.所以对象访问方式取决于虚拟机的实现.主流的方式有两种:
1.通过句柄访问.Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址.
2.使用指针访问.reference 中直接存储对象地址.
比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好.
HotSpot使用第二种方式进行对象访问的.
更多推荐
所有评论(0)