JVM - 运行时数据区

881 阅读24分钟

Java运行时数据区

image.png

这篇文章主要还是围绕HotSpot虚拟机来解析运行时数据区

PC(Program counter 程序计数器)

Java 虚拟机可以同时支持多个执行线程 每个线程都有自己的 PC 程序计数器 也叫 PC寄存器

每个线程都在执行单一方法的代码,即该线程的当前方法。 如果该方法不是本地方法,则 pc 包含当前正在执行的 Java 虚拟机指令的地址。

如果线程当前正在执行的方法是非本地的,则 Java 虚拟机的 pc 的值是未定义的

线程在运行的时候就是不停在计数器中取PC值, 找到对应的位置 继续执行 直到这个线程执行完毕,生命周期跟随线程

一些分支、循环、异常处理、跳转、线程恢复 等功能都需要依赖程序计数器去完成

  public static void main(String[] args) {
  int i = 1;
  int j = 2;
  int v = i + j;
  }
  
  //对应字节码
  0 iconst_1
  1 istore_1
  2 iconst_2
  3 istore_2
  4 iload_1
  5 iload_2
  6 iadd
  7 istore_3
  8 return
  

image.png

PC寄存器存在的意义,cpu在执行不同线程的时候会触发上下文切换,当cpu切换上下文之后需要知道下一条指令应该从那一条开始

image.png

Stack 栈

Java是基于栈运行,基于栈的优势就是跨平台 指令集比较小(8位对齐)但是指令会比较多 性能会比较底

Android虽然是用Java开发,但是android是基于寄存器运行 性能会比较高 但是跟CPU的耦合度会比较高

栈的创建时间是线程启动的时候 栈中的数据都是对应线程私有独享的

保存局部变量和部分结果,并在方法调用和返回中发挥作用

  1. 栈只有push 和 pop 操作

  2. 栈是不需要内存是连续的 所以栈帧的数据有可能被分配在堆中的这一点被JVM虚拟机称之为堆栈

  3. 堆栈jvm是允许用户自己设定大小的 可以通过-Xss设置

  4. 如果线程使用超过设定的栈的大小 那么虚拟机会抛出StackOverflowError的错误的 比如写一个死循环的递归 就会抛出栈溢出的错误

  5. 如果虚拟机的堆栈可以动态扩容,但是扩容的大小超过了系统可用内存的大小,那么堆栈是会抛出OutOfMemoryError

image.png

栈存储的是一个一个栈帧看上图

Stack Frame 栈帧

栈帧 其实是一个方法 每一个方法就是一个栈帧 栈帧中又包含 局部变量表 操作数栈 动态连接 返回地址

通过下面伪代码理解什么叫栈帧


public void method1(){
   ...
   method2()
   ...
}

public void method2(){
  ....
  method3
  ....
}

 public void method3(){
 
 }

上面伪代码栈帧对应下图

image.png

method3 执行完后弹出栈 method2就成为当前栈顶继续执行 那么method2执行完弹出栈 method1成为栈顶继续执行,直到最后栈中栈帧执行完成后 线程结束

Local Variable Table 局部变量

 public static void main(String[] args) {
      new TestObject().testRun();
  }

image.png

image.png

可以看到局部变量表中存在 args局部变量 和 testObject局部变量 以及对应的描述符也就是数据类型指向方法区中

局部变量表就是将对应方法内部创建的变量存储 存储的顺序按照声明的顺序存储

在非静态方法 会添加一个this的变量 在局部变量表最底部 索引为0 代表当前对象的引用

Solt 槽 局部变量表 是一个数组实现的 对应数组的每一个单元叫做Solt槽 单个Solt单位是4个字节 像int就占用一个Solt

double long占用2个solt 其他的sort char 等会被转成int存储至槽中,引用变量字同样也是占用一个solt 4个字节

Operand Stack 操作数栈

操作数栈作用主要根据指令往操作数栈 添加数据或者提取数据, 在JVM执行引擎中的解释器执行是基于栈的执行引擎对应的就是使用操作数栈翻译成CPU指令执行

操作数栈是通过数组的方式实现的 既可以满足栈的特点也有索引的概念 数组对应的长度在编译的时候已经确定的,局部变量表同样也是一样 可以通过查看字节码查看

image.png

下面演示 i=i++ , i=++i 操作数栈和局部变量表的作用

i=i++的指令

JMM (3).jpg

int i =0;
i=i++;
以上语句对应下面指令
 iconst_0 //将i的值压栈
 istore_1 //存储到局部变量表
 iload_1  //将局部变量索引为1的拿出来 压栈
 iinc 1 by 1 //变量索引为1的直接在变量表中进行运算 这个时候并不影响操作栈中的值
 istore_1 //从操作数栈中弹出栈顶 并将局部变量索引为1的值设置为value。因为这个时候操作栈栈顶的值还是0 所以最终i的值依旧为0
 return
 
 所以这个指令的结果是 i=0 -> i=1 -> i=0 最终i=0 
 
i=++i的指令

JMM (4).jpg

int i =0;
i = ++i;
以上代码对应的指令
 iconst_0 
 
 istore_1
 
 iinc 1 by 1
 将局部变量表的值 直接在局部变量表的slot上进行运算 不影响操作栈中的值
 
 iload_1 
 从局部变量加载index为1的变量 压入操作栈 这个时候操作栈就为1
 
 istore_1 
 // 从操作数栈中弹出栈顶 并将局部变量索引为1的值设置为value
 
 //所以这个指令的结果是 i=0 -> i=1 -> i=1 最终i=1

通过上面i++++i大致能了解操作数栈的作用 , 我们还需要理解操作数栈的另外一个作用就是方法返回值

方法的返回值

继续伪代码

public void m1(){
   int v = sum(1,2);
}

0 aload_0
1 iconst_1
2 iconst_2
3 invokevirtual #5 <com/alaske/test/PCTest.sum : (II)I>
6 istore_1
7 iconst_0
8 istore_2
9 return

public int sum(int i,int j){
 return i+j;
}
0 iload_1
1 iload_2
2 iadd
3 ireturn


通过上面伪代码 m1调用sum传递了2个值,在sum中看到字节码 ireturn将对应的int结果return出去同时结束当前栈帧,在m1方法中偏移量为6指令istore_1 将操作数栈的数据存储到局部变量表中,也就是上一个栈帧的返回值 直接就在当前操作数栈的栈顶

image.png

Dynamic Linking 动态链接

这一块比较难理解,简单一点说就是 动态链接中存的是 指向运行常量池中方法的引用, 当调用方法的时候 会先创建动态链接 再创建对应的栈帧 动态链接存储的是指向常量池的方法的引用 伪代码调用方法

 VM vm = new VM();
 int sum = vm.sum(1, 2);
 //对应指令
 ....
 iconst_1
 iconst_2
 invokevirtual #4 <com/alaske/test/VM.sum : (II)I>
 istore_2
 ....
 

从字节码层面看

invokevirtual 基本上我们使用非静态的方法调用对应字节码大部分都是这种
#4 <com/alaske/test/VM.sum : (II)I> , #4 就是对应常量池改方法的符号引用

image.png 符号引用又会引用其他的 #2#27 总之对应的符号引用能解析出来具体的方法所包含的信息类信息,返回值,参数等

从字节码层面看 貌似都符号引用,但是在JVM运行起来之后会将对应的符号引用转换为直接引用,转换分为两种,直接转换和动态转换 也叫直接链接和动态链接

直接转换 (非虚方法)

在编译期间可以直接确定的运行也不会改变的 比如静态的方法 privat方法 final修饰的方法 以及构造方法 等 可以确定这些方法不会发生改变 不能被重写(非多态) 那么加载类的时候就会直接转换为直接引用 比如 invokestatic 调用静态方法 ,invokespecial调用构造器 invokevirtualfinal修饰的

invokestatic 调用类的静态方法

invokespecial 调用父类的 或者 私有方法

动态转换(虚方法)

在接口Class 或者 可重载的方法(多态) 等方式 在编译期间不确定 比如 invokevirtual invokeinterfaceinvokedynamic

invokevirtual 调用类的非私有方法 但是这里需要区别下final修饰的也是invokevirtual 但是final是不可继承的 所以在编译期间就可以确认

invokeinterface 显而易见调用接口的实现类的方法

invokedynamic Lambda表达式的方式调用

想更多了解指令的区别 可以直接查看JVM指令集

VTable(Virtual Method Table)

VTable 其实就是虚方法表 Java在查找 虚方法的时候 每次都需要去类的常量池查找会比较耗时 所以引入了叫VTable(virtual method table)虚方法表的方式存储 每一个类都会有一个虚方法表,在类被加载到内存中的Linking阶段的时候会创建一个虚方法表,所以在调用虚方法的时候 就直接去对应类的VTable中查找对应方法的引用

伪代码描述

  public class Test {
    @Override
    public String toString() {
        return "vTable";
    }
  }

image.png

return adress 方法返回地址

方法返回地址存储的 调用该方法的程序计数器的值 做为返回地址,也就是下一调指令的地址

主要目的就是恢复调用当前方法的上一个方法,需求修改上一个调用者的PC计数器的值 让上一个调用方法能够继续向下执行

同样伪代码

public void method1() {
 int sum = sum(10, 20);
}
0 aload_0
1 bipush 10
3 bipush 20
5 invokevirtual #2 <com/alaske/test/Test.sum : (II)I>
8 istore_1
9 return

public int sum(int i, int j) {
 return i + j;
}

可以看到用pc偏移量5的时候 调用到了sum方法 那么sum方法执行完成之后需要修改PC计数器的指针到8的位置让程序指令继续往下执行,那么当前存储的返回地址就8

Heap 堆

堆的创建时间是JVM虚拟机启动的时候

堆也被称之为共享堆

  1、堆中的数据是JAVA运行时数据区 分配数组和对象的内存 堆中的数据由垃圾回收器回收
  
  2、堆的大小可以是固定的也可以是自动收缩的 可以通过参数配置 -Xmx -Xms
  
  3、堆中的内存也是非连续内存
  
  4、如果计算需要的堆多于自动存储管理系统所能提供的堆则抛出 OutOfMemoryError

堆空间属于我们Java程序最大内存空间也是GC重点回收区 一般常见堆空间的一些空间分配思想 分代思想和Region思想

分代思想

分代模型在G1垃圾回收器诞生之前基本都是使用分代思想来规划堆的使用

比如Serial,PS-PO,ParNew,CMS都是采用分代思想并且将堆在物理上也分割为了新生代和老年代

image.png

通过JDK自带的jvisual工具查看内存分配

我设置的测试参数

-Xmx60m
-Xms60m

image.png

可以看到 我只设置的堆内存初始大小和最大大小 那么默认新生代和老年代的比例是1:2

新生代

新生代中分为Eden伊甸园区,S0S1两个幸存区 默认分为别8:1:1的比例

Eden区主要存放我们新new出来的对象,也就是最新以及最近创建的对象 开始会默认存放在Eden区,当Eden区接近满的时候 会触发新生代的GC这个我们后面GC环节再说

S0S1主要是为了新生代的GC回收的复制算法所预留的2个区 , S0S1 最多只能使用其中一个作为内存存放,另外一个需要预留到下次GC复制移动的时候存放

以我配置的60M做为基础 所以最终得出我们新生代最多能使用的内存是 Eden+S= 18M 另外2M的S区只能预留给GC的时候复制移动

老年代

对象在什么时候会进入老年代?

  • 当对象年龄达到配置晋升年龄的时候会进入,默认为15最大也只能为15 具体原因我们在后面对象布局JOL的时候在说
  • Eden区满了触发YGC的时候,survivor to区无法放入所有存活对象的时候,会直接将对象放入老年代
  • 通过配置PretenureSizeThreshold 配置对象大小的阈值 超过直接进入老年代

老年代的数据大小默认比新生代大一倍,而对应老年代的GC回收器也是调优JVM的重灾区,尽量的去避免老年代的GC也就是Major GC 老年代GC,每次GC的时候都会产生STW(stop the word) 用户线程就会被安全暂停 程序就会出现卡顿,具体的我们到后面的垃圾回收器再说

简单介绍几个GC的概念

Minor GC 新生代GC 这个直接等同于YGC

Major GC 仅老年代GC 也称之为Old GC

MixedGC 回收新生代和老年代 G1中的概念

FULL GC 回收整个堆包括方法区

有很多人理解 Major GC 等于 FULL GC 严格来说是有一定区别 但是基本也正确,

仅在CMS垃圾回收器中 Major GC仅回收老年代,不会去回收其他的内存区域 可以通过配置修改触发Old区GC的阈值

虽然CMS诞生到最后也从来都不是jdk默认垃圾回收器,但是它是第一个并发垃圾回收器 相当于里程碑的概念

其他的垃圾回收器中Major GC会和 FULL GC 等相同的意思,有时候没有必要扣这些细节,只需要理解了 几个不同的GC 在不同的垃圾回收器中是有不同的实现的

Region思想

比如 G1,ZGC 采用的分块思想将堆从逻辑上分为多个Region存储,当然G1还是在逻辑上使用了分代思想但是在物理上不在分割整个heap,在ZGC的时代就已经完全弃用掉了分代思想

G1中对应的内存块的区分 image.png

Region就是将堆空间在逻辑上划分一块一块的区域,最开始在G1中的概念,每一块的内存区域有可能是任何一个代 以及 G1中新增了一个大对象的区域 会占用多个Region 后面写G1的时候会具体说明Region的工作流程 还需要涉及到RSetCardTable的概念

TLAB

Thread Local Allocation Buffer 线程本地分配缓存区 在线程创建的时候会在堆上分配一块缓存空间 也称之为堆栈 默认只会占用Eden 1%的大小,但是同样是会收到GC回收机制管控,以及内存分配策略 所以TLAB的内存空间 也有可能在survivor区 和 old

虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定

TLAB存在的意义就是为了保证对象的内存分配过程中的线程安全性 实际这里面创建的对象都是线程私有的,只是为 解决在堆上分配空间的时候可能存在多个线程同时分配一块空间 导致线程不安全问题

Hotspot中有这么一段代码 是为了解决对象分配时候线程安全问题,优先使用TLAB,如果没有开启TLAB 那么采用CAS重试操作

....
if (UseTLAB){
    
    result = (oop)THREAD>tlab().allocate(obj_size);

}
if(result==NULL){
     
     need_zero=true;//直接在eden中分配对象
     
     retry:HeapWord*compare_to=*Universe::heap()->top_addr();
     
     HeapWord*new_top=compare_to+obj_size;
     
     if(new_top<=*Universe::heap()->end_addr()){
    
        //cmpxchg是x86中的CAS指令
        if(Atomic::cmpxchg_ptr(new_top,Universe::heap()->top_addr(),compare_to)!=compare_to){
          //返回到retry中重试
           gotoretry;
        
         }
     
        result=(oop)compare_to;
     
      }
}
.....

逃逸分析

之前有人面试经常被问到 对象数据一定被分配在堆上吗?

针对这个问题 就要围绕JVM中逃逸分析去说明了

什么是逃逸分析?

逃逸分析为了分析 在方法中创建的对象是否只作用在方法内部 还是 被其他区域使用

逃逸分析的主要作用 是为了减少在堆中创建对象数据 避免GC回收部分未逃逸的对象 直接在栈上分配对应数据

未逃逸

很简单描述,当在方法内部创建对象 并且没有逃出方法内部作用域 则为未逃逸对象,未逃逸的对象 是有限在栈上分配的 只有栈上分配不下的时候 才会考虑在堆上分配. 分配在栈上 则不需要GC垃圾回收器处理 对应方法执行完毕了 弹出栈 所产生的内存自然就被销毁了

标量替换

当发现对象没有发生逃逸情况 会使用标量替换,标量替换就是将包装数据类型,比如我们自己定义的对象 拆分为一个一个基本数据类型 存储在栈帧中的局部变量表中 提高性能

逃逸

对象的作用域不仅仅只是在方法内部,比如传递出去的对象 或者 return出去的对象 都属于逃逸对象,只有逃逸对象才会仅在堆上分配

Method Area 方法区

方法区是一个逻辑概念,实际实现方式是根据不同虚拟机对应的实现,可以把方法区当成一个接口 不同的虚拟机有不同的实现方式

方法区的创建时间是JVM虚拟机启动的时候,生命周期跟随JVM虚拟机 进程启动创建 结束销毁

方法区是所有线程共享的一个内存区域

方法区存储的是已经编译代码,类似于操作系统进程中的“文本”段

例如Class文件中的 运行常量池 字段 和 方法数据 以及构造函数的代码 包括类在初始化的时候需要使用到的特殊方法<clinit>

方法区的内存也是不连续的,同样也可以指定方法区的大小,同样可以收缩扩容 如果方法区的内存溢出同样也会抛出 OutOfMemoryError

image.png

方法区中存储的内容

在一般情况下 JDK1.6~JDK1.8有些改动 最后说明 存储的主要有 类型信息、常量、静态变量、即时编译器的代码缓存等其他信息

类型信息

类信息一般包含,父类、类名称、类型比如接口,枚举,注解以及Class类

类型名称 比如java.lang.Object,以及对应的父类信息

类型的作用域,比如 private,public,protected,default

类型的修饰符 比如 static,final,abstract

类型实现的接口列表, 接口是可以实现多个 所以这一块是一个实现接口的数组

属性信息

属性的名称,类型,作用域,修饰符等

方法信息

返回值、名称、方法的修饰符(比如 synchronized,native,final,abstract,static等)

操作数栈和局部变量表长度、字节码数据

异常表,异常处理的开始地址和异常处理的结束地址,对应代码在PC计数器中的偏移地址,异常类的引用信息

可以从字节码中查看对应信息,其实方法区至少存储的对应Class文件的所有信息 image.png

也可以使用javap -v -p class类反编译查看

类信息对应

public class com.alaske.test.MethodSpace extends java.lang.Object implements java.lang.Comparable<com.alaske.test.MethodSpace>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
   .....

常量池

Constant pool:
   #1 = Methodref          #7.#37         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#38         // com/alaske/test/MethodSpace.i:I
   #3 = Methodref          #5.#39         // com/alaske/test/MethodSpace.getI:()I
   #4 = Methodref          #40.#41        // java/lang/Integer.compare:(II)I
   ....

属性对应

private int i;
  descriptor: I
  flags: ACC_PRIVATE

public static final int CONS;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: int 100

方法描述

public synchronized int incData(int);
  descriptor: (I)I
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=3, locals=2, args_size=2
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: iload_1
       6: iadd
       7: dup_x1
       8: putfield      #2                  // Field i:I
      11: ireturn
    LineNumberTable:
      line 16: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      12     0  this   Lcom/alaske/test/MethodSpace;
          0      12     1 incValue   I
  MethodParameters:
    Name                           Flags
    incValue

PermGenSpace

永久代 在Java8之前方法区实现方式使用的是永久代方式,很容易就出现OOM java.lang.OutOfMemoryError: PermGen space,Java7和Java6还有不同的实现
在Java7的时候其实已经将 静态变量 字符串的值移到了堆中存储 已经在为移除永久代做准备,在Java7之后就将JRockit虚拟机和Hotspot虚拟机做一个融合

MetaSpace

在Hotspot 中 MetaSpace 是Java8之后方法区逻辑的具体实现 使用的是直接内存(本地内存)存储的方法区的数据,使用的是OS的内存,不会受到JVM虚拟机设置的内存大小限制,受限于OS系统的内存大小限制,如果超出了OS内存上限 同样会抛出OOM java.lang.OutOfMemoryError:  Metaspace

通过-XX:MaxMetaspaceSize 可以设置最大空间大小 以及 -XX:MetaspaceSize默认大小

方法区的不同版本的实现

JDK1.6

image.png 方法区的实现使用的是上面所说的永久代,对应存储的包含字符全常量池和静态变量名 这里需要注意如果静态变量指向的是一个object对象,那么对象是存储在堆中 只是变量名存储在永久代

JDK1.7

image.png

方法区使用的是永久代方式实现,将字符全常量池和静态变量的数据移到了堆中存储

JDK1.8

image.png 方法区使用的是Metaspace 元空间实现,将字符全常量池和静态变量的数据移到了堆中存储

为什么要采用直接内存方式

随着JVM支持了部分动态语言的特性以及动态代理等方式,以及我们在运行中会产生比较多个字符常量 很难确定永久代的空间大小,可能很容易引起OOM 并且会频繁的引起FullGC,JVM会了解决这个问题同时借鉴了JRockit虚拟机中元空间的概念实现采用直接内存的方式存储,直接内存使用的是OS系统内存空间,会降低FullGC的频率 并且仅受限于系统内存大小

为什么要数据移到堆中实现

因为我们的程序再执行的时候 会产生很多字符串的常量,放在方法区存储的时候回收效率会偏低,方法区只有在FullGC的时候才会处理垃圾数据,而很多临时的字符串数据在使用完成之后是可以立即回收的,这样也避免了方法区大量的无用数据堆积并且减少了FullGC出现的频率

运行时常量池

Run-Time Constant Pool

先查看虚拟机规范给出来的说明:

   运行时常量池是类文件中常量池表的每个类或每个接口的运行时表示

   包含多种常量,从编译时已知的数字文字到必须在运行时解析的方法和字段引用
   
   每一个运行时常量池都是从方法区分配的
   
   类或接口的运行时常量池是在 Java 虚拟机创建类或接口时就是ClassLoader加载类的时候
   
   如果运行时常量池的构造需要比 Java 虚拟机的方法区中可用的内存更多的内存,则 Java 虚拟机将抛出 OutOfMemoryError
   

什么是常量?

比如上面说的方法区中的Class文件存储的Constant pool里面存储了各种描述符的引用方式,一般为#xx

Constant pool:
  #1 = Methodref          #7.#37         // java/lang/Object."<init>":()V
  #2 = Fieldref           #5.#38         // com/alaske/test/MethodSpace.i:I
  #3 = Methodref          #5.#39         // com/alaske/test/MethodSpace.getI:()I
  #4 = Methodref          #40.#41        // java/lang/Integer.compare:(II)I
  #5 = Class              #42            // com/alaske/test/MethodSpace
  #6 = Methodref          #5.#43         // com/alaske/test/MethodSpace.compareTo:(Lcom/alaske/test/MethodSpace;)I
  #7 = Class              #44            // java/lang/Object
  #8 = Class              #45            // java/lang/Comparable
  #9 = Utf8               i
 #10 = Utf8               I
 #11 = Utf8               CONS
 #12 = Utf8               ConstantValue
 #13 = Integer            100
 ....

image.png

对应的CONS这个常量字段 指向的是常量池中#12 ConstantValue属性名称,#13指向的Integer 100对应这个常量的值和类型

可以理解为 在Class字节码中 有一个常量值的表,我们使用的任何方法 类 字段的方式都是通过去常量池中查找对应的值以及描述信息

什么是运行时常量池

其实理解了Class文件中的常量 那么理解这个常量池就很简单了。

运行时常量池是方法区的一部分,就是在ClassLoader将Class文件加载到内存中的时候,将Class中对应的常量存储到一个常量池表中,将常量的符号引用转换为一个直接引用存储到常量池表中

还有一种就是产生的动态常量 比如String.intern()产生的动态常量

Native Method Stacks 本地方法栈

什么是本地方法

我们先了解什么是本地方法 比如java中的unsafe中的CAS操作

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

使用native关键字修饰的称之为本地方法 调用方式称之为JNI调用

JNI(Java Native Interface)

提供JNI调用的主要原因就是让Java可以轻松的使用C/C++实现的方法,操作系统内部就是使用C/C++实现,一般我们考虑到性能问题会考虑直接使用C实现,JNI就可以让Java轻松的使用C实现的本地方法库,这一块在Android使用的比较多,在目前Java服务端编码的情况下 很少会自己定义本地方法

本地方法堆栈

Java虚拟机栈管理的是Java方法调用,也就是Java中每一个方法就是一个栈帧 一个线程对应一个Java栈

本地方法栈也是同样是属于本地方法的栈,在创建线程的时候 如果需要调用到本地方法的时候就会创建一个本地方法栈

本地方法是直接使用CPU的寄存器运行的,这一块执行效率比较高 而 Java是基于虚拟机栈通过执行引擎中的解释器转译成CPU的指令执行

本地方法栈一般被称之为C的堆栈 或者叫Java 虚拟机的指令集

线程中的计算需要比允许的更大的本机方法堆栈 则Java 虚拟机抛出 StackOverflowError

如果可以动态扩容本机方法堆栈并尝试进行本机方法堆栈扩容时,但可用内存不足,或者如果可用内存不足为新线程创建初始本机方法堆栈,则抛出 OutOfMemoryError

Direct Memory 直接内存

直接内存这块其实不属于运行时数据区的一部分,这里写出来主要是方法区以及NIO的时候以及Netty中大量的使用到

如果不使用直接内存,那么写文件或者读写网络IO流的时候使用都是会从用户态访问内核态,如果使用直接内存 就不会存在用户态到内核态,直接在系统缓存中开辟一块直接内存,使用缓冲Buffer写入直接内存

如果使用直接内存 比如网卡接收到数据 数据被放到os中内核空间的缓冲区的内容 这边可以采用Direct Memory直接访问或者写入也叫zero copy(零拷贝) netty采用的就是这一块内存的使用

image.png

直接内存不属于JVM内存区域,属于操作系统直接管理的内存区域,同样会有OOM Direct buffer memory错误,直接内存不受堆空间大小设置影响 直接受系统内存大小限制,可以通过-XX:MaxDirectMemorySize设置直接内存大小