03-Runtime_Data_Area
1. 阶段
- 类【加载 =>(验证 => 准备 => 解析)=> 初始化】后的阶段
- 把大厨后面的东西(切好的菜,刀,调料),比作是运行时数据区
- 而厨师可以类比于执行引擎,会将准备的东西制作成精美的菜品
- 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着OS和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异
2. 内存划分图
java.lang.Runtime
单例的,相当于运行时数据区
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寄存器的一种抽象模拟
- 它是一块很小的内存空间,几乎可以忽略不记,也是运行速度最快的存储区域
- JVM规范中,每个线程都独有PC,是线程私有的,生命周期与线程的生命周期保持一致
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。PC会存储当前线程正在执行的方法的JVM指令地址,如果是在执行native方法,则是未指定值(undefined)
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖PC来完成。字节码解释器工作时,就是通过改变PC的值来选取下一条需要执行的字节码指令
- 它是唯一在JVM规范中没有规定任何OOM情况的区域
1. 作用
PC用来存储指向下一条指令的地址(即将要执行的指令代码)。由执行引擎读取下一条指令
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. 问题
- 为什么使用PC记录当前线程的执行地址呢?
- 因为CPU需要在各个线程间切换,切换回来以后,需要知道接着从哪开始继续执行
- JVM的字节码解释器通过改变PC的值来明确下一条应该执行什么样的字节码指令
- PC为什么被设定为私有的?
- 所谓的多线程是在一个特定的时间段内,CPU会不停地做线程切换,只会执行某一个线程的一个方法,这样必然导致线程经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰
- 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个CPU、多核CPU中的一个内核,只会执行某个线程中的一条指令
3. CPU时间片
- CPU时间片:CPU分配给各个程序的时间,每个线程被分配一个时间段,称作线程的时间片
- 宏观上:可以同时打开多个应用程序,每个程序并行不悖,同时运行
- 微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行
7. Native_Method_Stack
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.Thread
的setPriority()
方法是用Java实现的,但是它本质调用的是该类里的本地方法setPriority0()
。这个本地方法是用C实现的,并被植入JVM内部。在Windows 95的平台上,这个本地方法最终将调用Win32setpriority()
API。这个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用
3. 现状
目前该方法使用的越来越少了,除非是与硬件有关的应用
- eg:通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达
- eg:可以使用Socket通信,也可以使用Web Service等等
JVM_Stack管理Java方法的调用,而Native_Method_Stack用于管理本地方法的调用,允许被实现成固定或者可动态扩展的内存大小(在内存溢出方面是相同的)
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,JVM将会抛出
StackOverflowError
异常 - 如果本地方法栈可以动态扩展,并且在尝试扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的Native_Method_Stack,那么JVM将会抛出一个
OutOfMemoryError
异常 - 具体做法是Native_Method_Stack中登记native方法,在Execution_Engine执行时加载本地方法库
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,JVM将会抛出
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受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
- 来源于NIO,通过存在Heap中的DirectByteBuffer操作Native内存
- 使用Native函数库分配堆外内存,通过DirectByteBuffer引用堆外内存
// 直接分配本地内存空间
int BUFFER = 1 * 1024 * 1024 * 1024; // 1GB
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
1. 非直接缓存区、缓存区
- BIO架构。需要从《用户态》切换成《内核态》,两份内存存储重复数据,效率低
- NIO架构。使用了OS划出的直接缓存区,被java代码直接访问
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
调整大小,内存不足时抛出OutOfMemoryError
或OutOfMemoryError:Direct buffer memory
- 线程堆栈:可通过-Xss调整大小,内存不足时抛出
StackOverflowError
(如果线程请求的栈深度大于JVM所允许的深度)或者OutOfMemoryError
(如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够的内存) - Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,可能会抛出
IOException: Too many open files
- JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中,而是占用JVM的本地方法栈和本地内存
- JVM、GCtor工作:也要消耗一定数量的内存