java虚拟机在执行java程序的过程中,会把内存划分为若干个不同的数据区域。每个区域都有各自的用途,创建和销毁时间,按照《java虚拟机规范(Java SE 7 版)》的规定,虚拟机运行时数据区域主要有以下几种:
1.程序计数器
程序计数器是很小的一块内存区域,可以看做是当前线程所执行字节码的行号指示器。在虚拟机的概念模型中,字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能均依赖于程序计数器。在多线程中,每个线程都有一个独立的程序计数器,每个线程的程序计数器之间互不影响,即“线程私有”。同时,程序计数器是java虚拟机规范中唯一一个没有规定OutOfMemoryError的区域。
2.java虚拟机栈
虚拟机栈描述的是java方法执行的内存模型,每个方法在执行时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从被调用执行到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。虚拟机栈也是线程私有的。在Java虚拟机规范中,虚拟机栈有两种异常状况:
(1)线程请求的栈深度大于虚拟机栈所允许的深度,将抛出*Error异常;
(2)如果虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存,或者在创建新线程时候没有足够的内存去创建虚拟机栈,就会抛出OutOfMemoryError异常。
3.本地方法栈
本地方法栈和虚拟机栈的作用类似,区别在于java虚拟机栈支持Java方法执行,而本地方法栈则支持native方法执行。有些虚拟机直接将java虚拟机栈和本地方法栈合二为一,本地方法栈的异常情况与java虚拟机栈的内存一致,即:
(1)线程请求的栈深度大于本地方法栈所允许的深度,将抛出*Error异常;
(2)如果本地方法栈可以动态扩展,但扩展时无法申请到足够的内存,或者在创建新线程时候没有足够的内存去创建本地方法栈,就会抛出OutOfMemoryError异常。
4.java堆
java堆是Java虚拟机所管理的内存中最大的一块,它是被所有线程所共享的内存区域,在虚拟机启动时候创建。java堆是供所有类实例和数组分配内存的区域,也是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。从内存回收的角度来看,由于收集器基本都采用分代收集算法,所以Java堆还可以细分为新生代(Young Gen)和老年代(Old Gen)。新生代又可以继续分为Eden空间,From Survivor空间,To Survivor空间。如下图:
Java堆的大小可以是固定的,也可以是随着程序执行动态扩展,并在不需要过多空间时候自动收缩。且Java堆所使用的内存不需要保证是连续的。如果实际所需的堆超过了自动内存管理系统所能提供的最大容量,java虚拟机将会抛出一个OutOfMemoryError异常。
Java堆常用调节参数:
-Xms128M :设置初始堆大小为128M
-Xmx512M :设置最大堆大小为512M
-XX:NewSize=n :设置年轻代大小;
-XX:NewRatio=n :设置年轻代和年老代的比值,如设置为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代的1/4;
-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值(年轻代分成1个Eden Space和2个Suvivor Space),如设置为3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5;
5.方法区
方法区和java堆一样,是所有线程共享的内存区域。它存储了每一个类的结构信息,例如运行时常量池,字段和方法数据,构造函数和普通方法的字节码内容,还包括一些在类,实例,接口初始化时用到的特殊方法。由于HotSpot虚拟机设计团队将GC分代扩展到了方法区,或者说使用“永久代”来实现方法区,这样HotSpot的垃圾收集器就可以像管理Java堆那样来管理方法区内存,故很多人将方法区称为永久代,但本质上两者不是等价的(Java8中,HotSpot对于方法区的实现从永久代变更为元空间Metaspace)。对于其他虚拟机(如BEA JRockit,IBM J9等)而言是不存在永久代的。按照java虚拟机规范,当方法区不能满足内存分配请求时,java虚拟机将抛出OutOfMemoryError异常。
方法区常用调节参数:
java8之前(设置永久代):
-XX:PermSize=64M :设置持久代初始大小
-XX:MaxPermSize=128M :设置持久代最大允许分配大小。
java8之后(设置元空间):
-XX:MetaspaceSize=8M :设置元空间大小为8M
-XX:MaxMetaspaceSize=80M :设志元空间最大为80M
-XX:MinMetaspaceFreeRatio=n :当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%
-XX:MaxMetasaceFreeRatio=n :当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%
-XX:MaxMetaspaceExpansion=n :设置Metaspace增长时的最大幅度
-XX:MinMetaspaceExpansion=n :设置Metaspace增长时的最小幅度
6.运行时常量池
运行时常量池是方法区的一部分,是class文件中每一个类或者接口的常量池表在运行时的表现形式。在加载类和接口到虚拟机后,就创建对应的运行时常量池。如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,Java虚拟机就会抛出一个OutOfMemoryError异常。
需要特别注意的是,运行时常量池和字符串常量池的区别,在jdk1.7之前,运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代。在JDK1.7 字符串常量池被从方法区拿到了堆中,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代。JDK1.8 hotspot移除了永久代引入元空间(Metaspace), 这时候字符串常量池还在堆中, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)。
参考资料:
《深入理解Java虚拟机 JVM高级特性与最佳实践 第2版》
《Java虚拟机规范(Java SE 7)》