JVM内存模型也叫JVM运行时区域,是认识和了解JVM工作原理的基础,从java诞生以来,JVM内存模型基本保持着大同小异的整体形态,由此可见JVM内存模型是相当稳定的,直到jdk1.8之后JVM内存模型中才将permGen(永生代),也就是过去的方法区完全去除,使用metaspace取而代之,但是从整个JVM内存形态来说其实并没有产生太大的变化,有点“换汤不换药”的味道。除此之外JVM内存模型的设计原理也充分考虑了java程序的运行过程以及GC的策略,所以JVM内存模型是一个既基础又复杂的内存结构。
jdk1.8以前的内存模型:
jdk1.8内存模型:
两种内存结构其实大同小异,这种差异在最后进行说明,图中深绿色的部分区域代表是线程私有的,而浅黄色部分代表的是所有线程共享的区域。首先来分别对不同的内存部分进行说明。
一、程序计数器
程序计数器是JVM执行字节码指令时的一个计数器指针,在对java代码进行执行的时候(如分支、循环、方法跳出、线程切换)都会使用到这个计数器,这个计数器的内存空间是非常小的,并且为线程私有,java多线程程序是通过切换线程占用CPU时间片的方式来执行的,为了使得每次线程切换之后对应的线程都能够指向正确的代码位置(字节码指令),这个时候就会使用到程序计数器,每一个线程都有自己对应的程序计数器,所以程序计数器是一个线程私有的内存区域。对于java方法程序计数器会指向对应的字节码指令,而对应native方法则计数器为空。最后,这块区域是在JVM内存模型中唯一一块没有OOM异常的区域。
二、虚拟机栈
虚拟机栈也被称为java虚拟机栈,这块区域同样是线程私有的,也就是生命周期与线程相同。虚拟机栈描述的是java方法执行时的内存模型,每个java方法在执行的时候都会创建一个方法栈帧用于存储局部变量表,操作数栈,动态的链接,方法返回值等。可以这么理解,一个java方法从开始执行到结束,实质上就是对应的一个方法栈帧从压栈到出栈的过程。
虚拟机栈中的局部变量表的空间大小是在编译的时期就已经确定了,也就是说在java方法的执行过程当中局部变量表的大小是确定不变的。在这个区域中如果线程所请求的栈的深度大于了虚拟机所允许的深度,就会抛出StackOverflowError(自己可以通过实现一个“跳不出来的”递归方法来模拟这种情况)。另外虚拟机栈的空间大小是可以动态扩展,如果是一个固定空间大小的虚拟栈(设置-Xss1m),java程序在创建线程时申请不到足够的空间会发生OOM。
三、本地方法栈
本地方法栈也是线程私有的并且与虚拟机栈的作用是完全一样的,不同点是本地方法栈是对native方法执行过程内存模型进行管理,会记录native方法在执行过程的相关数据(局部变量表等),完成对native方法栈的压栈和出栈过程。同样会发生StackOverflowError和OutOfMemoryError。
四、堆区
java 堆(heap)是JVM内存模型当中最大的一块区域, 并且是所有线程共享的一块区域,此区域主要用于存放java对象的实例,基本上所有的对象实例都会在这里分配内存(java.lang.Class对象不会,会在类加载中直接分配到方法区中)。堆是垃圾回收器主要“关照”的区域,由于有垃圾回收算法的策略影响,所以一整块大的堆实际也可以往下进行细分,新生代(Eden区,from suvivor区,to survivor区)、老年代。java的堆在物理空间上是可以允许不连续的,只要在逻辑上连续即可。可以通过-Xmx和-Xms来调整堆的大小,如果分配的对象实例已经超过了堆分配大小的剩余空间,这时候会产生OutOfMemoryError。
五、方法区
很多人把方法区叫做永生代,因为这一个区域主要是存放类加载过程中字节码文件的二进制字节流转变后的内存结构以及一些类的变量或静态变量,常量,这些数据模型都比较静态,看起来不会被垃圾回收,但是实际上并不是这样,JVM在后期的版本中实际上会对方法去中存储的数据进行垃圾回收,只是回收的效果不太明显(主要是针对于类的卸载以及常量池的回收)。在1.8以前可以设置-XX:MaxPermSize和-XX:PermSize来调整方法区的大小,如果分配的内存大小已经大于了方法区的剩余大小了,则会抛出OutOfMemoryError。可以通过一个死循环用cglib生成字节流进行加载的过程来模拟异常。
六、常量池
常量池主要存储编译时期的字面量和符号引用(类、接口、方法、变量的符号),它是方法区的一部分,因为是方法区的一部分,所以也会出现OutOfMemoryError。很多人可能认为常量池的数据是在程序编译的时期就已经确定好了,但是实际上在java运行过程当中也可以动态添加常量到池中(String的intern方法)
七、直接内存区
直接内存区域(Direct Memory)并不是JVM内存模型的一部分,而是通过native本地函数在操作系统中分配的一块内存区域,而在JVM中通过DirectByteBuffer对象对其进行引用操作(在JDK1.4引入NIO后有buffer和Channel出现),同样这一块内存区域因为会受到操作系统内存大小的限制,所以也会出现OutOfMemroyError。
七、MetaSpace
jdk1.8之后引入了一个新的区域叫做metaSpace,可以通过-XX:MaxMetaSpaceSize=256进行设置。在1.8之后已经废弃了之前的PermGen区域,所以无法通过设置PermSize和MaxPermSize来对其方法区的内存大小进行设置。MetaSpace实际上代替了PermGen区域用于存储类加载过程中的二进制字节流内存结构,而以前的常量池则被转移到了java堆中。这一块区域同样会出现OutOfMemoryError。