JVM内存结构

一、内存结构

​ JVM 内存结构由五部分组成,如下:

  • Method Area(方法区)
  • Heap(堆)
  • JVM Stack(虚拟机栈)
  • PC Register(程序计数器)
  • Native Method Stacks(本地方法栈)
    在这里插入图片描述

1. PC Register(程序计数器)

1.1 定义

​ JVM 中的程序计数寄存器(Program Counter Register)中,Register 的命名源于 CPU 的寄存器(寄存器存储指令相关的现场信息,只有把数据装载到寄存器中,CPU 才能够运行)。

​ 这里,并非是广义上所指的物理寄存器,将其翻译为 PC 计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。

​ JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。

1.2 作用

​ 用于记录下一条JVM指令的执行地址。

1.3 特点

  • 线程私有,每个线程都有它自己的程序计数器,生命周期与线程的生命周期保持一致。
  • 在 JVM 规范中,唯一一个不存在内存溢出(Out Of Memery)情况的区域。
  • 内存空间小,运行速度快的存储区域。

2. JVM Stack(虚拟机栈)

2.1 Stack Frame(栈帧)

​ Java 虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中虚拟机栈(JVM Stacks)的栈元素。

​ 每个方法被执行时,都会在虚拟机中创建一个栈帧(Stack Frame),因此每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

​ 每个栈帧中包含:

  • 局部变量表(Local Variables Table)
  • 动态链接(Dynamic Linking)
  • 方法返回地址(Return Address)
  • 操作数栈(Opreand Stack)
  • 一些附加信息.

在这里插入图片描述

2.1.1 局部变量表

​ 局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 编译为 Class文件时,就确定了该方法所需要分配的局部变量表的最大容量。

​ 局部变量表定义为一个数字数组,由多个变量槽(Slot)组成,主要用于存放编译期可知的各种基本数据类型(8大基本数据类型),reference 类型(对象引用)和 returnAddress 类型(指向一条字节码指令地址)。

​ 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。

注意

​ 局部变量存储在局部变量表中,生命周期与线程生命周期保持一致,线程间数据不共享。但是,如果是成员变量或方法外对象的引用,它们存储在堆中。因为在堆中,线程是共享数据的。

变量槽(Slot):

​ ①局部变量表最基本的存储单元是 Slot(变量槽)。每个变量槽都可以存储32位长度的内存空间。对于64位长度的数据类型(long,double)占用两个slot。

​ ② JVM 会为局部变量表中的每一个 slot 都分配一个访问索引,供访问使用。参数值的存放总数在局部变量数组的 index0 开始,到(数组长度-1)索引结束。因此当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量会按照顺序被复制到局部变量表中的每一个 Slot 上。

​ 如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用 this 将存放在 index0 的 slot 处,其余的参数按照参数表顺序继续排列。但对于静态方法,第一个索引存放的是方法的第一个参数。(这便解释了静态方法不能通过 this 调用。)

代码:

public class Test {
    public static void main(String[] args) {
        double a = 1.0;
        long b = 2;
        int c = 3;
        Test t = new Test();
        t.testSlot();
    }

    void testSlot(){
        System.out.println("This is instance");
    }
}

​ 调试图:

栈帧局部变量表
testSlot在这里插入图片描述
main在这里插入图片描述

拓展:

​ Slot 复用:

​ 为了节省栈帧空间,局部变量表中的slot是可以复用的。如果一个局部变量过了其作用域,那么在其作用域之后申请的新的局部变量就很有可能会复用过期的局部变量的 slot 槽位。

2.1.2 操作数栈

​ 每一个独立的栈帧中除了包含局部变量表意外,还包含一个"先进后出,后进先出"的操作数栈,也可称之为表达式栈(Expression stack)。操作数栈,在方法执行过程中,根据字节码指令,向栈中写入数据或者提取数据,即为入栈(Push)/出栈(Pop)。某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如:执行复制、交换、求和等操作。

​ 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈是 JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建,这个方法的操作数栈是空的。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度也是在编译期就被定义好了。

栈顶缓存技术:由于操作数栈是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这一问题,HotSpot JVM 的设计者们提出了栈顶缓存技术(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

2.1.3 动态链接

​ 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。目的是:当前方法中如果需要调用其他方法的时候,能够从运行时常量池中国找到对应的符号引用,然后将符号引用转换为直接引用,然后就能直接调用对应方法,这就是动态链接。比如:invokedynamic 指令。

​ 在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 class 文件的常量池里。

​ 在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转为也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接。

2.1.4 方法返回地址

​ 方法返回地址:存放调用该方法的PC寄存器的值。

​ 一个方法的结束,有两种方式:正常执行完成和异常退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。当方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

​ 正常和异常完成出口的区别在于:通过异常完成退出的不会给它的上层调用者产生任何的返回值。

2.2 定义

​ Java 虚拟机栈(Java Virtual Machine Stack)是 Java 虚拟机用于管理方法调用和执行的内存区域之一。每个线程在创建时都会分配一个 JVM Stack。

​ 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。

在这里插入图片描述

2.3 特点

  • 线程私有,每个线程都有自己的 JVM Stack,不与其他线程共享。
  • 生命周期与线程相同,线程结束,栈也会被销毁。
  • 栈内存分配越大,可用线程数越少。
  • 需要考虑局部变量的线程安全问题。如果局部变量引用了对象,并逃离方法的作用范围,或者返回了对象,就需要考虑线程安全,反之,线程是安全的。

2.4 异常

​ 程序运行中虚拟机栈可能会出现两种错误:

  • StackOverFlowError:若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError:如果栈的内存大小可以动态拓展(Classic 虚拟机),当虚拟机在动态拓展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。如果栈的内存大小不可以动态拓展(HotSpot 虚拟机),线程申请栈空间失败也会出现OutOfMemoryError 异常。

3. Native Method Stack(本地方法栈)

3.1 native 关键字

​ 在 Java 中,native 关键字用于声明一个方法为本地方法,意味着该方法的实现将在本地代码完成,通常是 C 或 C++ 代码。使用 native关键字可以允许 Java 程序调用本地代码库中的函数,从而拓展 Java 的功能,并利用已有的本地代码资源。然而,使用 native 关键字需要谨慎,并注意安全性、性能、兼容性、维护性和资源限制等方面的问题。

3.2 定义

​ 本地方法是以非Java语言开发的 Java 方法(Java方法只提供方法声明,非 Java 语言提供具体实现)。

​ 本地方法可以访问特定于系统的功能和 API,而这些功能和 API 在 Java 中不直接可用。

​ Java 虚拟机用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用。

​ 本地方法栈也是线程私有的。

​ 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表,操作数栈,动态链接,方法返回地址等信息。

4. Heap(堆)

4.1 定义

​ Java 堆(Java Heap)是JVM内存空间占用最大的一部分,被所有线程所共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放实例对象,几乎所有的对象实例以及数组都在这里分配内存。

​ Java 中"几乎"所有的对象都在堆中分配,但随着JIT(即时编译)的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么"绝对"。从 JDK 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以尝试在栈上分配内存。

​ Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆可以被细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。

​ 在 JDK 7 版本及 JDK 7 版本之前,堆内存通常被分为下面三部分:

  1. 新生代内存(Young Generation)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

在这里插入图片描述

​ 如上图所示,JDK8版本之后 PermGen(永久代)被 MetaSpace(元空间)代替,元空间是本地内存。

​ 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收之后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区 -> Survivor 区后对象的年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对于对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。不过,设置的值应该在 0-15,否则会抛出以下错误:

MaxTenuringThreshold of 20 is invalid; must be between 0 and 15

为什么年龄只能是 0-15?

​ 因为记录年龄的区域在对象头中,这个区域的大小通常是 4 位,这 4 位可以表示的最大二进制数字是 1111,即十进制的 15。因此,对象的年龄被限制为 0到15。

4.2 堆内存溢出

​ 堆最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  1. java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  2. java.lang.OutOfMemoryError: Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过 -Xmx 参数配置,若没有特别配置,将会使用默认值:Java8 的 Xmssize(初始堆内存)为 1/64 物理内存,Xmxsize(最大堆内存)为 1/4 物理内存)。

5 方法区

5.1 定义

​ 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。

​ 《Java 虚拟机规范》只是规定了有方法区这么一个概念和它的作用,因此,在不同的虚拟机实现上,方法区的实现是不同的。

​ 当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

方法区和永久代以及元空间是什么关系呢?

​ 方法区和永久代以及元空间的关系类似于Java接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK1.8 之前的方法区实现,JDK1.8 及之后方法区的实现变成了元空间。

为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?

1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整(也就是受到 JVM 内存的限制),而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

​ 当元空间内存溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

​ 我们可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小,如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

2、元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了,而由系统的实际可用空间来控制,这样能加载的类就更多了。

3、在 JDK8,合并 HotSpot 和 JRockit 的代码时,JRockit 从来没有一个叫永久代的东西,合并之后就没有必要额外的这么一个永久代的地方。

4、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

5.2 常用参数

​ JDK1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小。

-XX:PermSize=N //方法区(永久代)初始大小
-XX:MaxPermSize=N //方法区(永久代)最大大小,超过这个值将会抛出 OOM 异常:java.lang.OutOfMemoryError: PermGen

​ 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

​ JDK1.8 的时候,方法区(HotSpot的永久代)就被彻底移除了(JDK1.7 就已经开始了),取而代之的是元空间,元空间使用的是本地内存。下面是一些常用参数:

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

​ 与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

二、常量池及直接内存

1. 运行时常量池

​ Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的常量池表(Constant Pool Table)。

​ 字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包含整数、浮点数和字符串字面量。常见的符号引用包含类符号引用、字段符号引用、方法符号引用、接口方法符号。

 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java 虚拟机规范》的 Class 文件格式中。
 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

​ 常量池表会在类加载后存放到方法区的运行时常量池中。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OOM 错误。

2. 字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象 "ab"
// 将字符串对象"ab"的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字段
String bb = "ab";
System.out.println(aa == bb); //true

​ HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cppStringTable 可以简单理解为一个固定大小的 HashTable,容量为 StringTableSize(可通过 -XX:StringTableSize 参数来设置),保存的是字符串(key)和字符串对象的引用(value)的映像关系,字符串对象的引用指向堆中的字符串对象。

​ JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动到了 Java 堆中。

在这里插入图片描述

在这里插入图片描述

JDK1.7 为什么要将字符串常量池移动到堆中?

​ 主要是因为永久代(方法区实现)的 GC 回收率太低,只有在整堆收集(Full GC)的时候才会被执行GC。Java 程序中通常会有大量的被创建字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。

3. 直接内存

​ 在 Java 虚拟机中,直接内存(Direct Memory)是一块与 Java 堆独立管理内存区域。它是通过 Java NIO 库引入的一种特性。与 Java 堆相比,直接内存的分配和释放不受 Java 堆大小的限制,因此可以在一些特定场景下提高更高的性能。

​ 直接内存提升了 IO 操作的效率,在传统 IO 操作中,本地文件需要先读取到内存中,然后再复制到 Java 堆中,才可以进行操作。而如果使用直接内存,在堆中创建一个 Java 对象的引用,就可以省略将数据复制到 Java 堆中的操作。通过这种方式减少数据复制的开销,提高 IO 操作效率。

​ 在 Java 中,可以通过 ByteBuffer 类来操作 JVM 直接内存。ByteBuffer 是一个可以进行高效地读写操作的数据缓冲区,它可以分配在 Java 堆上,也可以分配在直接内存上。分配在直接内存上的 ByteBuffer 称为直接缓冲区(Direct Buffer)。

​ 直接缓冲区在分配时会调用本地系统的接口进行内存分配,生成一个本地内存地址。Java 虚拟机会使用这个本地内存地址与本地系统进行交互,实现直接的 IO 操作。

​ 当不再使用直接缓存区时,必须手动调用 ByteBufferclean() 方法或显式调用 System.gc() 进行垃圾回收,释放直接内存。否则会造成内存泄漏。

三、HotSpot 虚拟机对象探秘

​ 通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。

1. 对象的创建

1.1 类加载器检查

​ 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

1.2 分配内存

​ 在类加载器检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有"指针碰撞"和"空闲列表"两种。

内存分配的两种方式:

  • 指针碰撞

    • 适用场合:堆内存规整(即没有内存碎片)的情况下。
    • 原理:用过的内存全部整合到一边,没有用过的内存放到另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
    • 使用该分配方式的 GC 收集器:Serial,ParNew
  • 空闲列表

    • 适用场合:堆内存不规整的情况下。
    • 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块儿来划分给对象实例,最后更新列表记录。
    • 使用该分配方式的 GC 收集器:CMS

​ 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。

内存分配并发问题:

​ 在创建对象时有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+ 失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到完成为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB:为每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。

1.3 初始化零值

​ 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

1.4 设置对象头

​ 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁,对象头会有不同的设置方式。

1.5 执行 init 方法

​ 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才完全产生出来。

2. 对象的内存布局

​ 在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头(Header)实例数据(Instance Data)、和对齐填充(Padding)

​ 对象头包含两部分信息:

  1. 标记字段(Mark Word):用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
  2. 类型指针(Klass Word):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

​ 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

​ 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3. 对象的访问定位

​ 建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄直接指针

3.1 句柄

​ 如果使用句柄的话,那么 Java堆中将会划分出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄地址包含了对象实例数据与对象类型数据各自的具体地址信息。

在这里插入图片描述

3.2 直接指针

​ 如果使用直接指针访问,reference 中存储的直接就是对象的地址。

在这里插入图片描述

​ 这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

​ HotSpot 虚拟机主要使用的就是这种方式来进行对象访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值