一、内存模型概览
本文介绍的是JDK1.8的内存模型。1.8同1.7相比,最大的差别就是元空间取代了永久代。元空间的本质和永久代类似,都是堆JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不存在虚拟机中,而是使用本地内存。
内存模型概览如下:
其中程序计数器、虚拟机栈、本地方法栈是线程私有的。方法区、堆是线程公有的。
二、运行时数据区
2.1、程序计数器
程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
2.2、虚拟机栈
和程序计数器一样,虚拟机栈也是线程私有的,它的生命周期和线程相同。
虚拟机栈描述的是方法执行的线程内存模型,方法执行的时候,Java虚拟机都会在虚拟机栈里面创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
入栈出栈图解
栈的溢出(StackOverflowError)
如果栈帧的数量过多,或者某些栈帧过大会引发SOE(StackOverflowError)
如果允许虚拟机栈动态扩展,那么当内存不足时,会导致OOM(OutOfMemoryError)
栈帧图解
局部变量表
局部变量表存放了这个栈帧对应的方法的局部变量(基本类型是值、引用类型是句柄或者指针)
操作数栈
主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是出栈和入栈操作。
例如整数加法(2+3)的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了int类型的数据,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。
动态连接
动态连接
将符号引用转化为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。
直接引用:直接引用可以是直接指向目标的指针,也可以是能间接定位到目标的句柄,还可以是相对偏移量。
每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
方法出口
无论是程序正常返回或者是异常调用完成返回,都必须回到最初方法被调用时的位置。
2.3、本地方法栈
本地方法栈也是线程私有的。
本地方法栈与虚拟机栈所发挥的作用是类似的,区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用到的本地Native方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError。
2.4、方法区
方法区只是一个概念上的东西,JDK1.8元空间是方法区的实现
方法区是线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。它有一个别名叫Non-Heap。
类型信息:对于加载的类型包括class、interface、enum、annotation,JVM必须在方法区中存储以下类型信息:
① 这个类型的完整有效名称(全名=包名.类名)
② 这个类型直接父类完整有效名(对于interface或者是java.langlObject,都没有父类)
③ 这个类型的修饰符(public、abstract、final的某个子集)
④ 这个类型直接接口的一个有序列表
2.4.1、运行时常量池
Class文件中除了有类的版本、字段、方法、接口等描述信息外、还有一项信息是常量池表的引用,用于存放编译期生成的各种字面量与符号引用。
Java中的字节码需要数据支持,通常这种数据很大以至于不能直接存到字节码里面,所以就存到常量池,而字节码文件存储的就是指向常量池的引用,在动态链接的时候会用到运行时常量池。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池表的运行时表示形式,在类或接口被加载到JVM后,对应的运行时常量池就被创建出来,常量池表的字面量与符号引用就会放到运行时常量池。
2.5、堆
堆是线程共享的内存区域,它是虚拟机管理内存中最大的一块。此区域的唯一目的就是存放对象实例,Java世界里几乎所有的对象实例都在这里分配内存。当堆中没有内存分配给对象实例时,会抛出OutOfMemoryError
新生代gc流程:
- 刚刚新建的对象在Eden中,经历一次Minor GC,Eden中存活对象就会被移动到s0,Eden被清空。
- 等Eden区再满了,就出发一次Minor GC,Eden和s0中存活的对象又会被复制到s1中(这个过程非常重要,因为这种复制算法保证了s1中来自s0和Eden两部分的存活对象占用了连续的空间,避免碎片化)
- s0和Eden被清空,然后下一轮s0与s1互换角色,如此循环。
三、本地内存
3.1、元数据区
元数据区也叫元空间,是方法区的一种实现。
存储的是类的元数据信息。
3.2、直接内存
直接内存并不是虚拟机运行时数据区的一部分。但是这部分内存也经常被使用到,也有可能导致OutOfMemoryError出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(channel)与缓冲区(Buffer)的I/O方式,它可以用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免了在Java堆和Native堆中来回复制数据。