一. JVM内存模型
从上图可以知道,JVM的内存模型包括方法区、虚拟机栈、本地方法栈、堆以及程序计数器。
二. 详细介绍
1. 方法区
方法区是一块所有线程共享的内存区域,用于存储虚拟机加载的类信息,比如类的字段、方法、常量池(用于存放编译器生成的各种符号引用)、静态变量、以及编译器编译后的代码等。
在JDK 1.6、JDK 1.7中,方法区可以理解为永久区(Perm)。永久区可以使用参数-XX:PermSize和-XX:MaxPermSize指定,默认情况下,-XX:PermSize为16MB,-XX:MaxPermSize为64MB。
在JDK 1.8中,永久区已经被彻底移除,取而代之的是元数据区,元数据区大小可以使用参数-XX:MaxMetaspaceSize指定。
2. 虚拟机栈
虚拟机栈是一块线程私有的内存空间,在虚拟机栈中保存的主要内容为栈帧。每一次函数调用,都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束,都会有一个栈帧被弹出虚拟机栈。Java虚拟机提供了-Xss参数来指定线程的最大栈空间。
在一个栈帧中,至少要包含局部变量表、操作数栈和帧数据区几个部分。
2.1 局部变量表
局部变量表用于保存函数的参数以及局部变量。局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
2.2 操作数栈
操作数栈用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
2.3 帧数据区
除了局部变量表和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回和异常处理等,这些数据就保存在帧数据区。大部分Java字节码指令需要进行常量池访问,在帧数据区中保存着访问常量池的指针,方便程序访问常量池。此外,当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去。对于异常处理,虚拟机必须有一个异常处理表,方便在发生异常的时候找到处理异常的代码,因此异常处理表也是帧数据区中重要的一部分。
3. 本地方法栈
本地方法栈用于支持native方法的执行,存储了每个native方法调用的状态。本地方法栈和虚拟机方法栈运行机制一致,它们唯一的区别就是,虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
4. 堆
堆区是理解Java GC机制最重要的区域。在JVM所管理的内存中,堆区是最大的一块,堆区也是JavaGC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区用来存储对象实例及数组值,可以认为java中所有通过new创建的对象都在此分配。
对于堆区大小,可以通过参数-Xms和-Xmx来控制,-Xms为JVM启动时申请的最新heap内存,默认为物理内存的1/64但小于1GB;-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1GB,默认当剩余堆空间小于40%时,JVM会增大Heap到-Xmx大小,可通过-XX:MinHeapFreeRadio参数来控制这个比例;当空余堆内存大于70%时,JVM会减小Heap大小到-Xms指定大小,可通过-XX:MaxHeapFreeRadio来指定这个比例。对于系统而言,为了避免在运行期间频繁的调整Heap大小,我们通常将-Xms和-Xmx设置成一样。
根据垃圾回收机制的不同,Java堆有可能拥有不同的结构。最为常见的一种构成是将整个Java堆分为新生代和老年代。
新生代:程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。
老年代:用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代。主要有两种情况:①大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②大的数组对象,且数组中无引用外部对象。老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
5. 程序计数器
程序计数器是最小的一块内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
三. 其他模块
3.1 类加载器
累加载器负责从文件系统或者网络中加载Class信息,加载的类信息存放于方法区。
3.2 执行引擎
执行引擎是Java虚拟机的最核心组件之一,它负责执行虚拟机的字节码。
3.3 直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。JDK1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
四. 堆内存与非堆内存
4.1 堆内存
Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。堆的大小可以固定,也可以扩大和缩小。堆的内存不需要是连续空间。
堆内存分配:
①JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;
②JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4;
③默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
④空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制;
⑤因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try…catch捕捉。
4.2 非堆内存
Java 虚拟机管理堆之外的内存(称为非堆内存)。方法区在逻辑上属于堆,但 Java 虚拟机实现可以选择不对其进行回收或压缩。与堆类似,方法区的大小可以固定,也可以扩大和缩小。方法区的内存不需要是连续空间。除了方法区外,Java 虚拟机实现可能需要用于内部处理或优化的内存,这种内存也是非堆内存。例如,JIT 编译器需要内存来存储从 Java 虚拟机代码转换而来的本机代码,从而获得高性能。
非堆内存分配:
①JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;
②由-XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4;
③-XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存溢出。