Java虚拟机之运行时数据区(2)

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分

1、PC程序计数器

image.png

什么是程序计数器

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

(1)区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。

(2)当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。

(3)程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。

(4)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

image.png

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

PC寄存器深入探究的工具准备

为了深入理解PC寄存器的原理,我们需要借助一个工具 - jclasslib-bytecode-viewer,这个工具可以查看编译后的class文件。
image.png

下面准备一段特别简单的代码,用来做演示,我们先对代码进行编译后,得到一个class文件
image.png

然后点击编译后的class文件上,打开view的 Show ByteCode With Jclasslib 菜单
image.png

然后就能看到class文件的反编译后的内容
image.png

单线程下,寄存器运行揭秘

下面来解释一下,这个PC寄存器究竟是怎么起作用的。
当我们拿到了这个编译后的代码,是无法直接给计算机使用的,所以需要一个解释器对代码进行解释。再此之前,先说下PC寄存器中究竟有什么内容,其实就是存放了下一跳的指令地址,相关流程如下:

(1) 解释器在PC寄存器中拿到了指令地址

(2) 解释器根据指令地址,找到了操作地址

(3) 解释器执行操作地址上面的命令,编译成机器指令

(4) PC寄存器中把下一个即将执行代码的指令地址放入,等待解释器下一次的请求

image.png

多线程下,寄存器如何工作?

在多线程序下,PC寄存器的角色就更加的重要了。因为多个线程是并发执行的,CPU时间分片对每个线程都只执行一小段时间。那么在CPU执行时怎么知道究竟是从哪个地方开始执行呢?答案就是从PC寄存器中。

(1) 线程在执行时从PC寄存器中得到指令地址,然后取出对应的操作指令进行执行

(2) 在线程切换之前,先把自己线程内部的PC寄存器中的指令地址变成下一个地址。

(3) 那么下一次线程再次进行执行时,就能知道当前需要执行哪一段代码了

image.png

2、虚拟机栈

image.png

1、什么是虚拟机栈

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

下面看一个代码,模拟堆栈情况
image.png

每个方法执行时,就会创建一个栈帧,然后塞入到虚拟机栈中
image.png

2、什么是栈帧

栈帧(Stack Frame) 是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表操作数栈动态连接方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

image.png

方法调用执行过程
image.png

3、设置虚拟机栈的大小

-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

-Xss1m
-Xss1024k
-Xss1048576
复制代码

image.png

4、局部变量表

局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

还是通过之前用过的编译分析工具,查看一下,局部变量表
image.png

5、操作数栈

操作数栈(Operand Stack) 也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

通过以下代码演示操作站执行,先准备一个简单的加法例子
image.png

底层源代码,和寄存器,局部变量表,操作数栈相关的交互
image.png

6、动态连接

Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

我们来看一个简单的程序,调用Math的Random函数。
字节码中,可以看到不是直接把Math的代码全部加载过来了,而是设置一个符号引用。
image.png

通过点击引用,可以跳转到所属方法的方法,但是此时这里还是符号引用。接下来就通过层层递归,最终找到对应的方法,对方法进行动态链接。
image.png

7、方法返回地址

方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式: 1-正常地执行完成,2-出现未处理的异常非正常的退出。
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置

(1) 正常地执行完成 方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
image.png

(2)出现未处理的异常非正常的退出。
而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

image.png

3、本地方法栈

image.png
本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

特点:

(1)本地方法栈加载native的但是方法, native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。

(2)虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

(3)是线程私有的,它的生命周期与线程相同,每个线程都有一个。

在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常:

(1)StackOverFlowError :线程请求的栈深度>所允许的深度。

(2)OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存。
复制代码

4、堆

4-1 堆的概念

1、堆的简介

image.png

对于Java应用程序来说,Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。 Java堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。“几乎”是指从实现角度来看,随着Java语 言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了

2、堆的特点

(1)是Java虚拟机所管理的内存中最大的一块。

(2)堆是jvm所有线程共享的。 堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)

(3)在虚拟机启动的时候创建。

(4)唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。

(5)Java堆是垃圾收集器管理的主要区域。

(6)因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、FromSurvivor空间、To Survivor空间。

(7)java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。

(8)方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。

(9)如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

3、设置堆空间大小

示例:VMOptions设置为 -Xmx20m -Xms5m 
说明:当下Java应用最大可用内存为20M,最小内存为5M
复制代码

image.png

不分配内存时的执行结果:

Xmx=18.0M
free mem=4.19561767578125M
total mem=5.5M


结论:
大家可以发现,这里打印出来的Xmx值和设置的值之间是由差异的,total Memory和最大的内存之间还是存在一定差异
的,就是说JVM一般会尽量保持内存在一个尽可能底的层面,而非贪婪做法按照最大的内存来进行分配
复制代码

再来看一下在程序分配内存后的代码
image.png

程序分配内存后的执行结果:

分配了10M空间给数组
Xmx=18.0M
free mem=3.6566009521484375M
total mem=15.0M

结论:
可以发现其实JVM在分配内存过程中是动态的,按需来分配的。
复制代码

4、堆的分类

在Java7 Hotspot虚拟机中将Java堆内存分为3个部分:青年代Young Generation老年代Old Generation永久代Permanent Generation

image.png

在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不存在了
image.png

4-2 年轻代和老年代

1、JVM中存储java对象可以被分为两类

(1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。

(2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
image.png

2、配置新生代和老年代堆结构占比

-XX:NewRatio=2 【默认值】 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3

-XX:NewPatio=4 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5

-XX:SurvivorRatio=8 Eden空间和另外两个Survivor空间占比分别为8:1:1,,这两个 Survivor 区域分别被命名为 from 和 to,以示区分

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的

几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁

image.png

从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。

4-3 对象分配过程

JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,因此还需要考虑GC执行完内存回收后是否存在空间中间产生内存碎片。

分配过程

1.new的对象先放在伊甸园区。该区域有大小限制

2.当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到伊甸园区

3.然后将伊甸园区中的剩余对象移动到幸存者0区

4.如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区

5.如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区。

6.如果累计次数到达默认的15次,这会进入养老区。

可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N

7.养老区内存不足是,会再次出发GC:Major GC 进行养老区的内存清理

8.如果养老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常.

下面来看一个案例:

(1)红色的方块代表着垃圾对象,当Eden区满了后,清除Eden中的垃圾对象,然后把幸存者对象丢入到S0区,最后标记当前的对象的计数为1
image.png

(2)当Eden区再次满了后,清除Eden中的垃圾对象,然后把Eden中的幸存者对象丢入到S1中,同时S0中的数据也要丢入到S1区,保持随时都要有一个幸存者空间是空的。转移后,自身的计算要加1。所以看到新丢入S1区的对象的技术为1,而来自S0区对象的计算则变成2了
image.png

(3) 然后不停的重复1,2 的过程,直到幸存者区的计数已经到了15,这个时候把这些对象转移到老年代中

image.png

最后看一张分配对象的流程图:
image.png

4-4 堆GC

Java 中的堆也是 GC 收集垃圾的主要区域

GC收集器的分类

部分收集器GC:

1、新生代收集(Minor GC / Young GC): 只是新生代的垃圾收集
2、老年代收集 (Major GC / Old GC): 只是老年代的垃圾收集 (CMS GC 单独回收老年代)
3、混合收集(Mixed GC):收集整个新生代及老年代的垃圾收集 (G1 GC会混合回收, region区域回收)
复制代码

整堆收集(Full GC)

收集整个java堆和方法区的垃圾收集器
复制代码

GC触发条件

年轻代GC触发条件:

1、年轻代空间不足,就会触发Minor GC,这里年轻代指的是Eden代满,Survivor不满不会引发GC
2、Minor GC会引发STW(stop the world) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复
复制代码

老年代GC (Major GC)触发机制

1、老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC
2、如果Major GC , 内存仍然不足,则报错OOM
3、Major GC的速度比Minor GC慢10倍以上
复制代码

FullGC 触发机制:

1、调用System.gc() , 系统会执行Full GC ,不是立即执行.
2、老年代空间不足
3、方法区空间不足
4、通过Minor GC进入老年代平均大小大于老年代可用内存
复制代码

5 元空间

在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 HotSpots取消了永久代,那么是不是也就没有方法区呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。它和永久代有什么不同的?

存储位置不同: 永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存

存储内容不同: 在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。

image.png

5-1 为什么要废弃永久代,引入元空间?

(1)在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出

(2)移除永久代是为融合HotSpot VM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代

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

5-2 废除永久代的好处

(1) 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。

(2)将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。

(3)将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。

5-3 Metaspace相关参数

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。如果没有使用该参数来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。如果设置了该参数,当Metaspace剩余空间不足,会抛出:java.lang.OutOfMemoryError: Metaspace space

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

6、方法区

image.png

Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但些简单的实现可能不会选择去进行垃圾收集或者进行压缩”。对HotSpot而言,方法区还有一个别名叫做Non-Heap(非堆),的就是要和堆分开。

6-1 方法区的理解

方法区(Method Area) 与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、 静态变量、 即时编译器编译后的代码缓存等数据

虚拟机中,方法区只是一个规范,只要能够实现方法区,具体怎么实现没有要求,所以元空间也好,永久代也罢,只是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的

创建对象各数据区域的声明:
image.png

方法区的特点

(1)方法区与堆一样是各个线程共享的内存区域

(2)方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续

(3)方法区的大小跟堆空间一样 可以选择固定大小或者动态变化

(4)方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类 导致方法区溢出虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久代) Java 8之后 是MetaSpace(元空间) )

(5)关闭JVM就会释放这个区域的内存

6-2 方法区的内部结构

image.png

image.png

类型信息

对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:

(1)这个类型的完整有效名称(全名 = 包名.类名)

(2)这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)

(3)这个类型的修饰符(public, abstract,final的某个子集)

(4)这个类型直接接口的一个有序列表

域信息

域信息,即为类的属性,成员变量,JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。

域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)

方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

(1)方法名称方法的返回类型(或void)

(2)方法参数的数量和类型(按顺序)

(3)方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集

(4)方法的字节码bytecodes、操作数栈、局部变量表及大小(abstract和native方法除外)

(5)异常表(abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

6-3 方法区的设置

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整

jdk7及以前

  • 通过-xx:Permsize来设置永久代初始分配空间。默认值是20.75M
  • 通过-XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
  • 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。
查看JDK PermSpace区域默认大小相关命令:

jps    #是java提供的一个显示当前所有java进程pid的命令
jinfo -flag PermSize 进程号 #查看进程的PermSize初始化空间大小
jinfo -flag MaxPermSize 进程号 #查看PermSize最大空间
复制代码

JDK8以后

元数据区大小可以使用参数 -XX:MetaspaceSize-XX:MaxMetaspaceSize指定默认值依赖于平台。

windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。

与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace

XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。

新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

一句话的解释就是:GC后,如果发现只是释放了一点点空间,则会适当提高这个元数据值,如果发现释放了大量的空间,则会把这个值降低一点。

查看元数据空间命令:

jps #查看进程号
jinfo -flag MetaspaceSize 进程号 #查看Metaspace 最大分配内存空间
jinfo -flag MaxMetaspaceSize 进程号 #查看Metaspace最大空间
复制代码

7、运行时常量池

常量池vs运行时常量池 两者究竟有什么关系?

两者所在的地址不一样

(1)常量池在字节码文件

(2) 运行时常量池在方法区

两者所代表的含义不一样

(1)常量池,存放编译期间生成的各种字面量与符号引用

(2)常量池表在运行时的表现形式

编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中

理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。

image.png

对字节码文件反编译之后,查看常量池相关信息

javap -v  PCRegister > abc.
复制代码

image.png

要弄清楚方法区的运行时常量池,需要理解清楚字节码中的常量池。

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant pool table),包括各种字面量和对类型、域和方法的符号引用。常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

常量池表Constant pool table:

image.png

在方括号中对常量池表的符号引用,直接引用上诉的常量池表中的信息
image.png

为什么需要常量池?

比如一段简单的代码,但是里面却使用了 String、 System、 PrintStream及Object等结构。如果代码多,引用到的结构会更多!这里就需要常暈池,将这些引用转变为符号引用,具体用到时,采取加载
image.png

8、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。

原始的使用内存的方式:想要读取数据,必须经过用户空间地址,内核地址空间,最后到达磁盘,返回数据时也是一样
image.png

NIO的Buffer供一个可以直接访问系统物理内存的类——DirectBuffer。DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同。

分配空间地址不同:
普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。

访问数据方式不同:
在访问普通的ByteBuffer时,系统总是会使用一个“内核缓冲区”进行操作。而DirectBuffer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuffer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuffer更快。
image.png