JVM内存的那些事儿
生活赋予我们一种巨大的和无限高贵的礼品,这就是青春:充满着力量,充满着期待志愿,充满着求知和斗争的志向,充满着希望信心和青春。 —— 奥斯特洛夫斯基
JVM内存简介
Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。Java虚拟机所管理的内存区域包括以下几个运行时数据区域,如下图所示:
JVM内存区域程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
在多线程环境中,每个线程都有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储,因此程序计数器是线程私有的。
如果线程正在执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计数器则为空(Undefined)。
Java虚拟机栈
Java虚拟机栈也是线程私有的,它和Java线程在同一时间创建,它保持方法的局部变量、部分结果,并参与方法的调用和返回。
Java虚拟机栈规范允许Java栈的大小是动态是动态或者固定的。在Java虚拟机栈规范中,定义了两种异常与栈空间有关:
StackOverflowError和OutOfMemoryError。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果虚拟机动态扩展时无法申请到足够内存时,将抛出OutOfMemoryError异常。
在Hotspot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。
以下示例展示了栈的溢出。
public class StackTest {
private int count = 0;
public void recursion() {
count++;
recursion();
}
@Test
public void testStack() {
try {
recursion();
} catch (Throwable e) {
System.out.println("deep of stack is " + count);
e.printStackTrace();
}
}
}
默认情况下,程序输出结果:
deep of stack is 18904
java.lang.StackOverflowError
使用参数-Xss2M再次执行程序,程序输出结果:
deep of stack is 42442
java.lang.StackOverflowError
很明显,栈的内存增大后,程序支持的函数调用深度也同时增大。
public class StackTest {
private int count = 0;
public void recursion(long a, long b, long c, long d) {
long e = 0, f = 0, g = 0;
count++;
recursion(a, b, c, d);
}
@Test
public void testStack() {
try {
recursion(1L, 2L, 3L, 4L);
} catch (Throwable e) {
System.out.println("deep of stack is " + count);
e.printStackTrace();
}
}
}
同样使用参数-Xss2M执行程序,程序输出结果:
deep of stack is 21055
java.lang.StackOverflowError
随着参数和局部变量的增多,栈帧的空间也随之增大。(函数调用次数由无参时的42442降至21055)。
虚拟机栈在运行时使用栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表、操作数栈、动态连接方法和返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作,相应地,方法的返回则表示栈帧的出栈操作。方法调用时,方法参数和局部变量相对较多,那么局部变量表会比较大,栈帧会膨胀以满足需求,因此单个方法调用所需的栈空间大小也会比较多。
栈帧结构图如下:
栈帧结构注意:对一个函数而言,它的参数越多,内部局部变量越多,它的栈帧就越大,其可达深度就越低。
-
局部变量表
用于存放方法参数和方法内部定义的局部变量,其大小在代码编译期间已经确定,在方法运行期间不会改变。局部变量表以变量槽(Slot)为最小存储单位,每个Slot能够存放一个boolean、byte、char、shot、int、float、reference和returnAddress类型的32位数据,对于64位的数据类型long和double,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。
在方法执行时,如果是实例方法,即非static方法,局部变量表中第0位Slot默认存放对象实例的引用(虚拟机通过局部变量表将当前对象传递给当前方法),方法中可以通过关键字 this 进行访问,方法参数按照参数列表顺序,从第1位Slot开始分配,方法内部变量则按照定义顺序进行分配其余的Slot。 -
操作数栈
操作数栈是一个基本的栈,那么它自然也遵守栈的后入先出的原则。其次,它里面主要存放的是一些算数运算用到的参数也可能是中间结果,也可能是在调用其他方法时需要用到的参数。通过这点可以看出,方法刚刚开始执行的时候,这个里面是空的。最后 要说明的是操作数栈中可以存放任意的Java数据类型,包括long和double,且32位的数据类型占一个栈空间,64位的数据类型占2个栈空间。 -
动态连接
在说明什么是动态连接之前先看看方法的大概调用过程。首先,在虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。这时候就有一点需要注意,如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
-
返回地址
方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法。
不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定。
本地方法栈
本地方法栈和虚拟机栈类似,两者之间的区别是本地方法栈为Native方法服务。
Java堆
Java堆是Java运行时内存中最为重要的部分,几乎所有的对象实例以及数组都都是在堆中分配空间的。Java堆是所有线程共享的内存区域。
Java堆分为新生代和老年代两部分,新生代用于存放刚刚产生的对象和年轻的对象,(大对象除外,直接进入老年代,因为大对象占用空间多,为了有足够空间容纳大对象,JVM不得不移动大量新生代中的年轻对象至老年代,这对GC来说是不利的,另外,若是由于内存空间紧张,JVM很可能不得不将部分年轻对象提前向老年代压缩
),如果对象经历过N(该次数可通过参数配置,默认是15
)次GC而未被回收,则会被移入老年代。
新生代又可进一步分为eden、from space(s0)、to space(s1)(默认eden:s0:s1=8:1:1,该比例可配置
)。eden,即对象的出生地,大部分对象刚建立时,都会存放在这里。s0和s1为survivor空间,直译为幸存者,也就是说存放在其中的对象,至少经历了一次垃圾回收,并得以幸存,如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代。
方法区
方法区(又称永久代)和Java堆一样,是所有线程共享的内存区域。方法区主要保存的是类的元数据。
方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。类型信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表。常量池包括这个类的方法、域等信息所引用的常量。域信息包括域名称、域类型和域修饰符。方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法栈帧的局部变量区大小以及异常表。总之,方法区内保存的信息,大部分是来自于class文件。
运行时常量池用于存放编译期间生成的各种字面常量(文本字符串、声明为final的常量值)和符号引用(类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符)。在JDK1.6中,常量池是方法区的一部分,在JDK1.7中,常量池存放在堆内存里,在JDK1.8中,常量池存放在MetaSpace里。
目前1.8的HotSpot中,已经将方法区移除,取而代之的是MetaSpace。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
在HotSpot虚拟机中,在永久区中的对象,同样也是可以被回收的。对永久区GC的回收,主要从以下两个方面分析:一是GC对永久区的常量池的回收,二是永久区对类元数据的回收。
- 常量池的回收
只要常量池中的常量没有被任何地方引用,就可以被回收。 - 类元数据的回收
所有该类的实例被回收,且装载该类的ClassLoader被回收。
JVM内存分配参数
- -Xms:设置堆的初始大小。
- -Xmx:设置堆的最大值。
- -Xss:设置线程栈的大小。
- -XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当堆空间的空闲内存小于这个值时,便会扩展堆空间。
- -XX:MaxHeapFreeRatio:设置堆空间最大空闲比例。当堆空间的空闲内存大于这个值时,便会压缩堆空间,得到一个较小的堆。
- -XX:NewSize:设置新生代大小。
- -XX:NewRatio:设置老年代与新生代的比例,它等于老年代大小除以新生代大小。
- -XX:SurvivorRatio:设置新生代中Eden与survivor区的比例。
- -XX:MaxPerPermSize:设置最大的持久区大小。
- -XX:PerPermSize:设置持久区的初始大小。
- -XX:TargetSurvivorRatio:设置survivor区的可使用率。当survivor区的空间使用率达到这个值时,会将对象送入老年代。