03-Runtime_Data_Area

image-20230723181950933
image-20230723182138276

1. 阶段

  • 类【加载 =>(验证 => 准备 => 解析)=> 初始化】后的阶段
image-20200705111640511
  • 把大厨后面的东西(切好的菜,刀,调料),比作是运行时数据区
  • 而厨师可以类比于执行引擎,会将准备的东西制作成精美的菜品
image-20200705112036630
  • 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着OS和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异

2. 内存划分图

java.lang.Runtime单例的,相当于运行时数据区

image-20200705112416101

JVM定义了若干种程序运行期间使用到的运行时数据区,其中有一些会随着JVM启动而创建,随着JVM退出而销毁。另外一些则是与线程一一对应的,数据区域会随着线程开始、结束而创建、销毁

  • 线程独享:独立包括程序计数器、栈、本地方法栈
  • 线程共享:堆、堆外内存(永久代或元空间、代码缓存)

3. 线程

  • 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行执行
  • 在Hotspot里,每个线程都与OS的本地线程直接映射。当一个线程准备好执行,此时一个OS本地线程也同时创建。Java线程执行终止后,OS本地线程也会回收
  • OS负责将所有线程,安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()

4. JVM系统线程

使用jconsole或任何一个调试工具,都能看到后台许多线程在运行。这些后台线程不包括main线程以及main线程创建的子线程

后台系统线程在Hotspot里主要有:

  • JVM线程:操作需要JVM达到安全点才会出现。这样堆才不会变化。这种线程的执行类型包括STW的GC,线程栈收集,线程挂起以及偏向锁撤销
  • 周期任务线程:是时间周期事件的体现(eg:中断),一般用于周期性操作的调度执行
  • GC线程:对在JVM里不同种类的GC行为提供支持
  • 编译线程:在运行时会将字节码编译成本地代码
  • 信号调度线程:接收信号并发送给JVM,在它内部通过调用适当的方法进行处理

5. Error、GC

运行时数据区是否存在Error是否存在GC
PC
JVM_Stack
Native_Method_Stack
Method_Area是(OOM)
Heap

6. PC

程序计数器(Program Counter Register),Register的命名源于CPU寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(指令计数器)会更加贴切(也称为程序钩子)

  • JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟
image-20200705155551919
  • 它是一块很小的内存空间,几乎可以忽略不记,也是运行速度最快的存储区域
  • JVM规范中,每个线程都独有PC,是线程私有的,生命周期与线程的生命周期保持一致
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。PC会存储当前线程正在执行的方法的JVM指令地址,如果是在执行native方法,则是未指定值(undefined)
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖PC来完成。字节码解释器工作时,就是通过改变PC的值来选取下一条需要执行的字节码指令
  • 它是唯一在JVM规范中没有规定任何OOM情况的区域

1. 作用

PC用来存储指向下一条指令的地址(即将要执行的指令代码)。由执行引擎读取下一条指令

image-20200705161007423
public class PCRegisterTest {
    public static void main(String[] args) {
        int i = 10;
        int j = 20;
        int k = i + j;
    }
}

将代码编译成字节码文件,发现字节码的左边有一个行号标识,即指令地址,用于指向当前执行位置

0: bipush        10
2: istore_1
3: bipush        20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return

2. 问题

  1. 为什么使用PC记录当前线程的执行地址呢?
    • 因为CPU需要在各个线程间切换,切换回来以后,需要知道接着从哪开始继续执行
    • JVM的字节码解释器通过改变PC的值来明确下一条应该执行什么样的字节码指令
  2. PC为什么被设定为私有的?
    • 所谓的多线程是在一个特定的时间段内,CPU会不停地做线程切换,只会执行某一个线程的一个方法,这样必然导致线程经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰
    • 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个CPU、多核CPU中的一个内核,只会执行某个线程中的一条指令

3. CPU时间片

  • CPU时间片:CPU分配给各个程序的时间,每个线程被分配一个时间段,称作线程的时间片
  • 宏观上:可以同时打开多个应用程序,每个程序并行不悖,同时运行
  • 微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行
image-20200705161849557

7. Native_Method_Stack

image-20230523195810271

1. what

"A native method is a Java method whose implementation is provided by non-java code."

本地方法是一个非Java的方法,具体实现是非Java代码的实现

  • 简单地讲,一个Native Method就是一个Java调用非Java代码的接囗
    • 这个特征并非Java所特有,很多其它的编程语言都有这一机制
    • eg:C++中,可以用extern "c"告知C++编译器去调用一个C的函数
  • 在定义一个native method时,并不提供实现体(像定义一个Java interface),因为其实现体是由非java代码在外面实现的
  • 本地接口的作用是融合不同的编程语言为Java所用,初衷是融合C/C++程序
/**
 * 1. 标识符native可以与其它java标识符连用,abstract除外
 * 2. Object类、Thread类中有很多native方法
 */
public class IhaveNatives {
    public native void Native1(int x);
    public native static long Native2();
    private native synchronized float Native3(Object o);
    native void Natives(int[] ary) throws Exception;
}





 
 
 
 

2. why

Java使用起来非常方便,然而有些层次的任务用Java实现起来并不容易,或者对程序的效率很在意

1. 与Java环境外交互

  • 有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。Java需要与一些底层系统(OS或某些硬件)交换信息。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且无需去了解Java应用之外的繁琐的细节

2. 与OS的交互

  • JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的OS。通过使用本地方法,得以用Java实现了jre与底层系统的交互,甚至JVM的一些部分就是用C写的。还有,如果要使用一些Java语言本身没有提供封装的OS的特性时,也需要使用本地方法

3. Sun's Java

  • Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是Java实现,也通过一些本地方法与外界交互
  • eg:类java.lang.ThreadsetPriority()方法是用Java实现的,但是它本质调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部。在Windows 95的平台上,这个本地方法最终将调用Win32 setpriority() API。这个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用

3. 现状

  1. 目前该方法使用的越来越少了,除非是与硬件有关的应用

    • eg:通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达
    • eg:可以使用Socket通信,也可以使用Web Service等等
  2. JVM_Stack管理Java方法的调用,而Native_Method_Stack用于管理本地方法的调用,允许被实现成固定或者可动态扩展的内存大小(在内存溢出方面是相同的)

    • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,JVM将会抛出StackOverflowError异常
    • 如果本地方法栈可以动态扩展,并且在尝试扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的Native_Method_Stack,那么JVM将会抛出一个OutOfMemoryError异常
    • 具体做法是Native_Method_Stack中登记native方法,在Execution_Engine执行时加载本地方法库
image-20200706174708418
  1. 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受VM限制的世界。它和VM拥有同样的权限
    • 本地方法可以通过本地方法接口来访问VM内部的运行时数据区
    • 它甚至可以直接使用本地CPU中的寄存器
    • 直接从本地内存的堆中分配任意数量的内存
  • 并不是所有的JVM都支持本地方法。因为JVM规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈
  • 在Hotspot中,直接将Native_Method_Stack和JVM_Stack合二为一

Native_Method_Stacks与JVM_Stack所发挥的作用是非常相似的,其区别不过是JVM_Stack为JVM执行Java方法(也就是字节码)服务,而Native_Method_Stacks则是为JVM使用到的Native方法服务。JVM规范中对Native_Method_Stacks中的方法使用的语言、使用方式、数据结构并没有强制规定,因此具体的JVM可以自由实现它。甚至有的JVM(譬如Sun HotSpot)直接就把Native_Method_Stacks和JVM_Stack合二为一。与JVM_Stack一样,Native_Method_Stacks区域也会抛出StackOverflowError和OutOfMemoryError异常; ——以上摘自《深入理解Java虚拟机:JVM高级特性与最佳实践》

8. Direct_Memory

直接内存。32位系统下,单个进程默认可以使用2GB内存

  • 不是JVM运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁的使用
  • 直接内存是Heap外的、直接向系统申请的内存区间。不受Heap大小限制,仅受本机总内存大小以及处理器寻址空间的限制
  • 通常,访问直接内存的速度会优于Heap,即读写性能高
    • 出于性能考虑,读写频繁的场合可能会考虑使用直接内存
    • Java的NIO库允许Java使用直接内存,用于数据缓冲区
  • -XX:MaxDirectMemorySize:最大值,默认和Heap最大值一样。不足时,OutOfMemoryError: Direct buffer memory
image-20200709230647277
  • 来源于NIO,通过存在Heap中的DirectByteBuffer操作Native内存
  • 使用Native函数库分配堆外内存,通过DirectByteBuffer引用堆外内存
// 直接分配本地内存空间
int BUFFER = 1 * 1024 * 1024 * 1024; // 1GB
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);

1. 非直接缓存区、缓存区

  • BIO架构。需要从《用户态》切换成《内核态》,两份内存存储重复数据,效率低
  • NIO架构。使用了OS划出的直接缓存区,被java代码直接访问
image-20230613112719221

2. 问题

  • 可能导致OutOfMemoryError异常
  • 堆外内存,大小不直接受限于-Xmx,默认与-Xmx参数值一致,也可通过MaxDirectMemorySize设置
  • 缺点
    • 分配回收成本较高
    • 不受JVM内存回收管理
/**
 *  IO                  NIO (New IO / Non-Blocking IO)
 *  byte[] / char[]     Buffer
 *  Stream              Channel
 *
 * 查看直接内存的占用与释放
 */
public class BufferTest {
    private static final int BUFFER = 1 * 1024 * 1024 * 1024; // 1GB

    public static void main(String[] args){
        // 直接分配本地内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接内存分配完毕,请求指示!");

        Scanner scanner = new Scanner(System.in);
        scanner.next();

        System.out.println("直接内存开始释放!");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.listao.jvm.chapter11.BufferTest.main(BufferTest.java:18)

3. 堆外内存

  • 直接内存:可通过 -XX:MaxDirectMemorySize 调整大小,内存不足时抛出OutOfMemoryErrorOutOfMemoryError:Direct buffer memory
  • 线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(如果线程请求的栈深度大于JVM所允许的深度)或者OutOfMemoryError(如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)
  • Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,可能会抛出 IOException: Too many open files
  • JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中,而是占用JVM的本地方法栈和本地内存
  • JVM、GCtor工作:也要消耗一定数量的内存