💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

【Jvm基础篇2】虚拟机执行子系统_jvm

  • 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
  • 导航
  • 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
  • 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
  • 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
  • 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
  • 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨


博客目录

  • 1.JVM 主要包括哪四部分?
  • 2.说说运行时数据区?
  • 3.什么是程序计数器?
  • 4.什么是 java 虚拟机栈?
  • 5.说说局部变量表?
  • 6.说说操作数栈?
  • 7.说说动态链接?
  • 8.说说方法出口?
  • 9.两个栈桢是否会有重叠?
  • 10.详细说说方法区?
  • 11.符号引用和直接引用?
  • 12.说说运行时常量池?
  • 13.说说全局字符串池?
  • 14.说说 class 文件常量池?
  • 15.三个常量池之间的关系?
  • 16.String 相加产生对象
  • 17.java 堆的结构
  • 18.什么是直接内存?


1.JVM 主要包括哪四部分?

  • 类加载器(ClassLoader):在 JVM 启动时或者在类运行时将需要的 class 加载到 JVM 中。
  • 执行引擎:负责执行 class 文件中包含的字节码指令
  • 内存区(也叫运行时数据区):是在 JVM 运行的时候操作所分配的内存区.
  • 本地方法接口:主要是调用 C 或 C++实现的本地方法及返回结果。

2.说说运行时数据区?

  • 方法区(Method Area)
  • 堆区(Heap)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method Stack)
  • 程序计数器(Program Counter Register)

【Jvm基础篇2】虚拟机执行子系统_字符串_02

3.什么是程序计数器?

程序计数器(PC Register):程序计数器(Program CounterRegister)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

4.什么是 java 虚拟机栈?

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。


5.说说局部变量表?

存储局部变量表: 局部变量表是 JVM 中的一块内存区域,用于存储方法执行过程中所需的局部变量。局部变量包括方法的参数和方法内部定义的局部变量。在方法执行时,JVM 会根据方法的签名和方法体中定义的局部变量,在局部变量表中为这些变量分配内存空间。局部变量表中的变量可以是基本数据类型和对象引用。

局部变量表作用:

  • 存储方法的参数和局部变量,提供方法执行时的临时工作空间。
  • 在方法调用时,用于传递参数和保存返回值。
  • 在方法内部,可以通过索引访问局部变量,进行数据操作和计算。

栈帧中变量的存储:

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的变量,在编译为 class 文件时,就在方法的 code 属性,设置了 max_locals 数据项中确定局部变量表的大小.

局部变量表是以变量槽为单位的,java 虚拟机规范并没有指出变量槽的具体占用空间,只是每个变量槽都应该能存放一个 boolean,byte,char,short,int,float,reference 和 returnAddress 类型的数据.这 8 种数据类型都是以 32 位或者更小的内存来存储的.但这种描述和每个变量槽用 32 位来存储是有差别的,它允许变量槽随着环境的变化而变化.

对于 64 位的类型数据,java 虚拟机会以高位对其的方式分配 2 个连续的变量槽空间.java 语言中 64 位只有 long 和 double 类型,由于局部变量表是在线程栈中创建的,线程私有,不会出现线程安全问题.

public int calc(){
  int a=100;
  int b=200;
  int c = 300;
  return (a + b) * c;
}
public calc()I
    BIPUSH 100
    ISTORE 1
    SIPUSH 200
    ISTORE 2
    SIPUSH 300
    ISTORE 3
    ILOAD 1
    ILOAD 2
    IADD
    ILOAD 3
    IMUL
    IRETURN
    MAXSTACK = 2
    MAXLOCALS = 4

【Jvm基础篇2】虚拟机执行子系统_java_03

istore 指令, _1 代表存入变量槽 1,是将局部变量存入局部变量表。槽位 0 代表了 this,所以看不到 0,如果使用到了 this,则会 ALOAD 0

栈桢的局部变量表是如何定位变量的:

java 虚拟机通过索引定位的方式来使用局部变量表。索引值从 0 开始,至局部变量表变量槽最大的数量。如果是访问的 32 位数据类型的变量,索引 n 就代表了第 n 个变量槽,如果是 64 位,则访问的是第 n 和 n+1 两个变量槽。对于 64 位的 2 个变量槽,不允许任何方式访问其中一个变量槽。如果出现这种情况,在类加载校验阶段会抛出异常。

是如何完成实参到形参传递的:

当一个方法被调用时,java 虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法,那局部变量表的第 0 位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过 this 关键字来访问到这个隐含的参数。其余参数则按照顺序在变量槽中排列,参数表分配完后,在根据方法体内部定义的变量和作用域来分配其余的变量槽。

6.说说操作数栈?

操作数栈: 操作数栈是 JVM 中的另一块内存区域,用于存储方法执行过程中的操作数和中间结果。JVM 采用后缀表达式(逆波兰表达式)来执行操作数栈中的操作。在执行方法时,JVM 会根据字节码指令,从操作数栈中弹出操作数,进行计算,并将结果再次压入操作数栈中。

作用:

  • 存储方法执行过程中的操作数和中间结果。
  • 在方法执行时,提供了一种轻量级的数据交换和计算方式,用于执行方法体内的运算。

7.说说动态链接?

动态链接(Dynamic Linking):动态链接是指在方法的调用过程中,将方法所在的类与方法的符号引用进行解析,得到方法的直接引用(方法在内存中的真实地址)。这个过程是在运行期间进行的,而不是在编译期间确定的。Java 虚拟机使用动态链接来支持多态性,以及在运行时动态绑定方法。

作用:

  • 支持多态性:允许不同的子类调用其父类中定义的同名方法,实现方法的动态绑定。
  • 提高程序的灵活性:在运行时才进行链接,可以在后续版本中动态替换类和方法的实现。

8.说说方法出口?

方法出口: 方法出口是指方法执行结束后将返回结果的地址返回给调用者的指令。在 Java 虚拟机的字节码指令中,return指令用于方法的正常返回,athrow指令用于方法抛出异常。这些指令将方法的执行结果或异常传递给调用者,并根据调用者的处理逻辑来进行相应的处理。

作用:

  • 控制方法的返回和异常抛出的行为。
  • 将方法执行结果或异常传递给调用者,实现方法的正常返回和异常处理。

9.两个栈桢是否会有重叠?

2 个不同栈桢作为不同方法的虚拟机栈的元素,是完全独立的。但是大多数的虚拟机实现里会进行一些优化处理。会存在共享存储空间的部分,比如下面栈桢的操作数栈与上面栈桢的局部变量表重叠在一起,这样做不仅节约了空间,更重要的是在进行方法调用时就可以直接共用一部分数据,不用再额外进行参数的复制传递。

【Jvm基础篇2】虚拟机执行子系统_字符串_04

10.详细说说方法区?

方法区(Method Area)是 Java 虚拟机(JVM)的一部分,用于存储类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。它是 Java 虚拟机规范中定义的一种内存区域,用于支持 JVM 的运行时环境和 Java 程序的执行。

以下是关于方法区的详细解释:

  1. 定义: 方法区是 JVM 中的一块内存区域,其在 JVM 启动时就被创建,并且随着 JVM 的关闭而销毁。在 Java 虚拟机规范中,方法区被定义为存储类和接口的元数据、运行时常量池、字段数据、方法数据、构造函数和普通方法的字节码以及一些特定于方法区的数据。
  2. 存储内容: 方法区主要存储以下内容:
  • 类信息:每个类的完整结构信息,包括类名、父类、实现的接口、字段、方法等。
  • 运行时常量池:每个类的常量池,包含编译时生成的字面量常量和符号引用。
  • 静态变量:类级别的静态变量,也称为类变量。
  • 即时编译器编译后的代码:当 JVM 执行某个方法时,可能会将该方法的字节码编译成本地机器代码,并将其存储在方法区中。
  • 方法字节码:包含每个方法的操作码(opcode)和操作数,以实现方法的功能。
  1. 线程共享: 方法区是所有线程共享的内存区域,与线程无关。在多线程的情况下,多个线程可以同时访问方法区。
  2. OutOfMemoryError: 方法区可以发生 Out of Memory 错误。当加载过多的类或者动态生成大量类时,方法区可能会耗尽内存。
  3. PermGen(永久代)与元空间(Metaspace): 在 Java 7 之前,方法区被称为"永久代"(PermGen),但是由于永久代容易导致内存溢出问题,Java 8 中将方法区的实现改为"元空间"(Metaspace),并且将其移到本地内存。元空间不再有固定的大小,而是使用本地内存,受限于操作系统的可用内存。
  4. 自动内存管理: 方法区的内存由 JVM 自动管理。垃圾收集器会负责回收不再使用的类、常量和静态变量,并且会在需要时进行内存扩展。

11.符号引用和直接引用?

在 Java 虚拟机(JVM)中,符号引用(Symbolic Reference)和直接引用(Direct Reference)是两种不同类型的引用,用于在运行时定位和访问类、字段、方法等。

符号引用(Symbolic Reference)::符号引用是一种用于描述所引用目标的符号名称的引用。在 Java 源代码中,类、方法和字段都使用符号引用进行引用,而不涉及具体的内存地址或偏移量。符号引用是在编译期和链接期间使用的,它们保持独立于虚拟机的内存布局,使得 Java 程序具有平台无关性。符号引用通常包括以下信息:

  • 类符号引用: 类的全限定名(包名+类名)。
  • 方法符号引用: 类符号引用 + 方法名 + 方法描述符(描述了方法参数类型和返回值类型)。
  • 字段符号引用: 类符号引用 + 字段名 + 字段描述符(描述了字段类型)。

由于符号引用并不直接指向内存中的实际位置,所以在运行时需要解析成直接引用才能定位实际的数据。

直接引用(Direct Reference): 直接引用是指向具体内存位置的指针、句柄或偏移量,用于直接访问类、字段、方法等在内存中的实际位置。直接引用是在虚拟机运行时才产生的,通过解析符号引用得到。在 JVM 的方法区内存结构中,方法表(Method Table)和字段表(Field Table)都包含直接引用。

  • 方法表中的直接引用: 方法表中的每个项对应一个类中的方法,其中包含指向该方法实际代码的直接引用,使得虚拟机可以直接定位并执行该方法的字节码。
  • 字段表中的直接引用: 字段表中的每个项对应一个类中的字段,其中包含指向该字段实际数据的直接引用,使得虚拟机可以直接访问和操作该字段的值。

直接引用是在虚拟机运行时才解析的,因此可以根据具体的内存布局和对象结构来定位实际的数据。

总结:符号引用是一种独立于具体内存布局的引用方式,在编译和链接期间使用,用于描述类、字段和方法的符号信息。而直接引用是运行时根据符号引用解析得到的具体内存地址或指针,用于在 JVM 运行时定位实际的类、字段和方法。直接引用的使用使得 JVM 具有更高的运行效率和更好的灵活性。

12.说说运行时常量池?

jvm 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm 就会将 class 常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class 常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池 StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

JDK1.6 中,JDK 1.6 方法区中的运行时常量池中包括了字符串常量池:

【Jvm基础篇2】虚拟机执行子系统_jvm_05

JDK1.7 中,字符串常量池从方法区移动到了堆中:

【Jvm基础篇2】虚拟机执行子系统_常量池_06

JDK1.8 中,字符串常量池在堆中,运行时常量池,类常量池还在方法区,方法区变为元空间:


public class GcDemo {

    public static void main(String [] args) {
        String str = new String("lonely")+new String("wolf");
        System.out.println(str == str.intern());
    }
}

这段代码在 jdk1.6 中打印 false,在 jdk1.7 和 jdk1.8 中打印 true。 关于 intern()方法:

  • jdk1.6:调用 String.intern()方法,会先去检查常量池中是否存在该字符串,如果不存在,则会在方法区中创建一个字符串,而 new String()创建的字符串在堆中,两个字符串的地址当然不相等。
  • jdk1.8:字符串常量池从方法区的运行时常量池移到了堆中,调用 String.intern()方法,首先会检查常量池是否存在,如果不存在,那么就会创建一个常量,并将引用指向堆,也就是说不会再重新创建一个字符串对象了,两者都会指向堆中的对象,所以返回 true。

执行 String.intern()如果在 1.7 和 1.8 中会检查字符串常量池,发现没有 lonelywolf 的字符串,所以会在字符串常量池创建一个,指向堆中的字符串。 但是在 jdk1.6 中不会指向堆,会重新创建一个 lonelywolf 的字符串放到字符串常量池,所以才会产生不同的结果

13.说说全局字符串池?

全局字符串池(Global String Pool),也称为字符串常量池(String Constant Pool),是 Java 中一种特殊的字符串缓存机制。它位于方法区内存中,用于存储在编译时期和运行时期遇到的字符串常量,以及通过 String 类的intern()方法手动添加到池中的字符串。

以下是关于全局字符串池的一些重要特点和解释:

  1. 字符串常量池的目的: Java 为了提高性能和节省内存,在全局字符串池中保存一份唯一的字符串实例。当程序中创建多个相同内容的字符串时,实际上会共享一个对象,从而减少内存占用。
  2. 字符串常量池位置: 在 Java 7 及之前,全局字符串池位于永久代(PermGen)内存中。而在 Java 8 及之后,随着永久代的移除,字符串常量池被移到了元空间(Metaspace)中。
  3. 字符串常量池特性:
  • 字符串常量池中的字符串是不可变的(Immutable),一旦创建就不可修改。这是通过在 String 类中使用 final 关键字来实现的。
  • 当通过字面值(例如:String str = "Hello";)创建字符串对象时,JVM 会首先检查全局字符串池中是否已经存在相同内容的字符串,如果存在,则直接返回引用。否则,创建一个新的字符串对象,并将其添加到字符串常量池中。
  1. String 类的 intern()方法:
  • intern()方法是 String 类的一个实例方法,它用于将字符串添加到全局字符串池中,并返回池中对应的引用。
  • 如果全局字符串池中已经存在相同内容的字符串,intern()方法将返回池中的引用;如果不存在,则将当前字符串添加到池中并返回对应引用。
  • intern()方法在某些情况下可以用于节省内存和加速字符串比较操作,但过度使用它也可能会增加全局字符串池的负担。
  1. 字符串拼接: 字符串拼接操作在 Java 中经常用到。在 Java 5 之前,使用+进行字符串拼接会导致大量临时对象产生,影响性能。但在 Java 5 之后,JVM 对字符串拼接做了优化,使用 StringBuilder 来处理,避免了临时对象的产生。

14.说说 class 文件常量池?

class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。 字面量就是我们所说的常量概念,如文本字符串、被声明为 final 的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可.

一般包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

Java 常量池中可能出现的 11 种不同表结构数据以及它们对应的标志位(1 字节):

常量类型标志位

常量类型

表结构数据

1

UTF-8

字符串内容

3

Integer

4 字节整数值

4

Float

4 字节浮点数值

5

Long

8 字节长整数值

6

Double

8 字节双精度浮点数值

7

Class Reference

指向一个类或接口的全限定名字符串(UTF-8 索引)

8

String Reference

指向一个字符串常量的值(UTF-8 索引)

9

Field Reference

指向一个字段的描述符(Class Reference 索引 + NameAndType 索引)

10

Method Reference

指向一个类中的方法(Class Reference 索引 + NameAndType 索引)

11

Interface Method Ref.

指向一个接口中的方法(Class Reference 索引 + NameAndType 索引)

12

Name and Type Descriptor

描述字段或方法的名称和类型(Name 索引 + Descriptor 索引)

15.三个常量池之间的关系?

示例1:

public class HelloWorld {
  public static void main(String []args) {
    String str1 = "abc";
    String str2 = new String("def");
    String str3 = "abc";
    String str4 = str2.intern();
    String str5 = "def";
    System.out.println(str1 == str3);//true
    System.out.println(str2 == str4);//false
    System.out.println(str4 == str5);//true
  }

上面程序的首先经过编译之后,在该类的 class 常量池中存放一些符号引用,然后类加载之后,将 class 常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中 str1 所指向的”abc”实例对象),然后将这个对象的引用存到全局 String Pool 中,也就是 StringTable 中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询 StringTable,保证 StringTable 里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

回到上面的那个程序,现在就很容易解释整个程序的内存分配过程了, 首先,在堆中会有一个”abc”实例,全局 StringTable 中存放着”abc”的一个引用值

然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且 StringTable 中存储一个”def”的引用值,还有一个是 new 出来的一个”def”的实例对象,与上面那个是不同的实例。

当在解析 str3 的时候查找 StringTable,里面有”abc”的全局驻留字符串引用,所以 str3 的引用地址与之前的那个已存在的相同。

str4 是在运行的时候调用 intern()函数,返回 StringTable 中”def”的引用值,如果没有就将 str2 的引用值添加进去,在这里,StringTable 中已经有了”def”的引用值了,所以返回上面在 new str2 的时候添加到 StringTable 中的 “def”引用值

最后 str5 在解析的时候就也是指向存在于 StringTable 中的”def”的引用值

16.String 相加产生对象

以下代码会产生几个对象:

new String("lonely")+new String("wolf")

在表达式 new String("lonely") + new String("wolf") 中,会产生六个对象:

  1. "lonely" 字符串常量:编译时,会将字符串常量放入编译时常量池中。
  2. "wolf" 字符串常量:编译时,同样会将字符串常量放入编译时常量池中。
  3. new String("lonely") 对象:在运行时,new String("lonely") 会创建一个新的 String 对象,并在堆上分配内存。这是因为new关键字会在堆上创建新的对象,即使这个对象的值在编译时已经存在于常量池中。
  4. new String("wolf") 对象:同样地,在运行时,new String("wolf") 会创建另一个新的 String 对象,并在堆上分配内存。
  5. "lonelywolf" 字符串常量:由于字符串的拼接操作使用了+运算符,会生成一个新的字符串对象。在这里,两个new String对象会被连接起来形成一个新的字符串常量"lonelywolf"
  6. 还有一个特殊的是 new StringBuilder 对象

综上所述,表达式 new String("lonely") + new String("wolf") 会产生五个对象:两个字符串常量"lonely""wolf",以及三个 String 对象。请注意,字符串常量是存储在全局字符串池中的,而new String对象是在堆上创建的。由于字符串的不可变性,它们在运行时是不可修改的。

17.java 堆的结构

  • 新⽣代通常占 JVM 堆内存的 1/3,因为新⽣代存储都是新创建的对象,⽐较⼩的对象,⽽⽼年代存的都是⽐较⼤的,活的久的 对象,所以⽼年代占 JVM 堆内存较⼤;
  • 新⽣代⾥的 Eden 区通常占年轻代的 4/5,两个 Survivor 分别占新⽣代的 1/10。因为 Survivor 中存储的是 GC 之后幸存的对象,实际上只有很少⼀部分会幸存,所以 Survivor 占的⽐例⽐较⼩。

【Jvm基础篇2】虚拟机执行子系统_jvm_07

如果从分配内存的角度看,所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变 Java 堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将 Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 Java 虚拟机都是按照可扩展来实现的(通过参数-Xmx 和-Xms 设定)。如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

18.什么是直接内存?

直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区场或。但是这部分内存也被频繁地使用,而且也可能导致 Out of MemoryError 异常出现。

在 JDK1.4 中新加入了 NIO(NewInput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 1/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

直接内存大小不受堆内存限制,但受本机总内存限制.

觉得有用的话点个赞 👍🏻 呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

【Jvm基础篇2】虚拟机执行子系统_字符串_08