Java虚拟机——内存结构
运行时数据区
程序在运行过程中会划分为不同区域,主要有下面七类:
程序计数器
程序计数器看成是当前线程所执行字节码的行号指示器
,简单来讲就是通过改变计数器的值来选择下一条需要执行的字节码指令。
程序计数器控制指令的分支、循环、跳转、异常处理、线程恢复等。
程序计数器的内存区域属于线程私有,随着线程的存在而存在,消亡而消亡,而且不同线程之间的计数器互不影响。
java虚拟机栈
java虚拟机栈存在于,方法被执行的时候,会创建一个帧栈
,描述的是方法执行的线程内存模型。
当调用一个新的方法时就向栈中压入一个新的帧栈;当方法执行完成后,帧栈从栈中被弹出。
帧栈存储:局部变量表、操作数栈、动态连接、方法出口等信息。
局部变量表存放了编译期的各种基本数据类型(boolean\byte\char\float\double...)
、对象引用
(引用的形式是reference类型,可以是一个指向对象起始位置的指针或者是代表对象的句柄)、returnAddress类型
(指向一条字节码指令的地址)
局部变量表中的存储空间以 局部变量槽来表示,long、double类型长64位占两个变量槽。在编译时候就能计算出来,在运行时不发生改变。
java虚拟机栈也是属于线程私有
当帧栈溢出 抛出 StackOverflowError异常,扩展到超过内存空间抛出 OutOfMemoryError
本地方法栈
本地方法栈于前面类似,不同的是执行的是本地(Native)方法服务
本地方法是用其他方法写的(c、c++、汇编),一般为基于本机硬件和操作系统的程序
java堆
java堆是所有线程共享的一块内存区域,存放的是对象实例
java堆是垃圾收集的主要内容之一,所以也被称为GC堆
,它在物理上可以是不连续的,但逻辑上被视为连续的。
以前虚拟机将堆划分为老年代、新生代、永久代、Eden、Survivor
,现在也有了新的发展可以不用分代,具体在垃圾回收机制方面查看。
方法区
方法区用于存放加载类的类信息、常量、静态变量、和即时编译器(jit)后的代码缓存等数据
在JDK1.8之前,一直是将方法区划分为永久代,而在之后选择用元空间(MetaSpace)
来取代永久代的说法。方法区的回收目标主要是常量池和类的卸载,用永久代管理会产生一个问题:永久区是有大小限制的,如果有大量常量很容易导致OOM内存溢出。而且对类的卸载很困难。
元空间存在于本地内存,是进程内存的一部分,摆脱了堆大小的限制。
为什么要在直接内存里拿出来一块内存作为元空间取代永久代呢?主要的说法有以下几个:
(1)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
(2)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
即方便分配管理,因为直接内存空间比较充足;便于回收,因为永久代本来回收垃圾的事件发生概率很低,直接从JVM中拿出可以提高回收效率。
总结一下metaspace的好处:
- 位置移到了堆外使用native memory, 取消了原来的大小限制
- 字符串常量和Class对象移动到了堆中管理
- 类和类加载器的生命周期一致,不需要单独回收某个类的空间, 当类加载器被回收时, 所属metaspace的内存一并回收
运行时常量池
运行时常量池属于方法区的一部分,包括:编译期生成的各种字面量和符号引用
这里讲一下java编译后的Class文件主要包含的内容有:类的版本、字段、方法、接口、常量池表。
直接内存
直接内存和JDK1.8后的元空间同属于本地内存,也称为堆外内存
操作直接内存
在 NIO (New Input/Output)类中引入了一种基于通道和缓冲的 IO 方式。
它可以通过调用本地方法直接分配 Java 虚拟机之外的内存,然后通过一个存储在堆中的DirectByteBuffer
对象直接操作该内存,而无须先将外部内存中的数据复制到堆中再进行操作,从而提高了数据操作的效率。
参考资料:
- 《深入理解Java虚拟机(第三版)》
- ****博客——JVM的内存结构(一) 运行时数据区
- JDK8的JVM内存结构,元空间替代永久代成为方法区及常量池的变化