JVM运行时数据区
Java虚拟机在执行Java程序的过程中会把它管理的内存分为若干个不同的数据区域。这些区域有着各自的用途,一级创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》中规定,jvm所管理的内存大致包括以下几个运行时数据区域,如下图所示:
灰色区域(方法区,堆):由所有线程共享。
白色区域(虚拟机栈,本地方法栈,程序计数器)则是跟随线程启动而启动,线程私有的 。
下面咱们逐个介绍一下:
1、程序计数器(Program Counter Register)
这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址。 对于JVM中的程序计数器可以看做当前线程所执行的字节码的行号指示器。 线程私有。
如果线程正在执行的是一个Java方法,这个计数器记录的则是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空(undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出,也称OOM)情况的区域。
2、Java虚拟机栈(Java VM Stack)
同程序计数器一样,Java虚拟机栈,也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧 用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
HotSpot虚拟机的栈容量是不可以动态扩展的。
下面咱们举个例子来看一下会导致StackOverflowError错误的。
package com.cc;
/**
* -Xss228k,虚拟机栈大小为228k
*/
public class StackTest {
private static int count = 0;
public static void main(String[] args) {
StackTest stackTest = new StackTest();
stackTest.test();
}
//递归调用
private void test() {
try {
count++;
test();
} catch (Throwable e) {
System.out.println("递归调用次数" + count);
e.printStackTrace();
}
}
}
运行结果:
对于单线程情况下,无论如何抛出的都是StackOverflowError。如果要抛出OOM异常,导致的原因是不断地在创建线程,直到将内存消耗殆尽。
3、本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机中使用到的Native方法服务。
在《Java虚拟机规范》 对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(如HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。
4、Java堆(Java Heap)
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
一个JVM只有一个堆内存。堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中?
类,方法,变量,保存我们所有引用类型的真实对象。
Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
上图所示的 eden区、s0区、s1区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
咱们还是通过代码来看一下:
在执行代码之前,配置一下堆的最小值与最大值(设置为一样可避免堆自动扩展)
如下图:
package com.cc;
import java.util.ArrayList;
import java.util.List;
/**
* -Xms20M -Xmx20M 堆初始大小20M 堆最大大小20M
* 通过参数 -XX:+HeapDumpOnOutOfMemoryError
* 可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
*
*/
public class HeapTest {
public static void main(String[] args) {
List<HeapTest> list = new ArrayList<HeapTest>();
int count = 0;
try {
while(true) {
count++;
list.add(new HeapTest());//不断创建线程
}
} catch (Throwable e) {
System.out.println("创建实例个数:" + count);
e.printStackTrace();
}
}
}
运行结果:
通过结果很清晰的看出堆内存溢出。
5、方法区(Method Area)
方法区是所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单来说,所有定义的方法的信息都保存在该区域。
静态变量,常量,类信息(构造方法,接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,与方法区无关。
JDK 1.8 的时候,方法区(HotSpot的永久代)被彻底移除了(JDK1.7就已经开始了),取而代之是元空间,元空间使用的是直接内存。
6、运行时常量池
运行时常量池是方法区的一部分。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
Java虚拟机对class文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被jvm认可。但对于运行时常量池,Java虚拟机规范没做任何细节要求。
运行时常量池有个重要特性是动态性,Java语言不要求常量一定只在编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也有可能将新的常量放入池中,这种特性使用最多的是String类的intern()方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制。当常量池无法再申请到内存时会抛出outOfMemeryError异常。