深入理解JVM(一):运行时数据区
运行时数据区
JVM在执行java程序的过程中,会把内存分为几个不同的数据区域,如上图所示。
程序计数器
虽然图片中程序计数器所占的面积比较大,但实际上程序计数器所占的内存非常小,也是唯一一块在所有JVM中都没有规定OOM的区域。它的作用是当前线程所执行的字节码的行号指示器、字节码行号的记录器。
在JVM的概念模型中,字节码解释器就是通过读取和改变这个计数器的值来选取下一跳需要执行的字节码指令,不管是分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于这个计数器。
JVM多线程的实现是通过时间片轮转占用CPU,在某个时刻,一个处理器只会执行一条线程的指令。为了线程切换后能恢复到正确的位置,每个线程都拥有一个独立的程序计数器,使得各个线程之间的计数器互不影响,所以这块区域是线程私有的。
- 如果线程正在执行一个java方法,计数器记录的是虚拟机正在执行的字节码指令地址。
- 如果线程正在执行一个native方法,计数器值为空(undefined)
java虚拟机栈
java虚拟机栈是线程私有的,它的生命周期和线程相同。java虚拟机描述的是java方法执行的内存模型,每个方法在执行的时候就会创建一个栈帧,栈帧存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至调用完成的过程就对应这一个栈帧在java虚拟机栈中从入栈到出栈的过程。 调用一个方法时创建新的栈帧并压入java虚拟机栈,方法执行完后,这个栈帧就会弹出栈帧的元素作为这个方法的返回值,并清除这个栈帧。所以java虚拟机栈的栈顶就是当前正在执行的活动栈、也就是正在执行的方法,程序计数器也是指向这个地址。
局部变量表所需的内存空间在编译期间就完成,存放了基本数据类型、对象引用和返回地址的类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot)。
- 如果线程请求的栈深度太深(比如递归调用),超出了java虚拟机栈所允许的深度,就会出现StackOverflow
- 虚拟机栈可以动态拓展,如果拓展到无法申请足够的内存空间,会出现OOM
本地方法栈
本地方法栈和java虚拟机栈的作用类似,不同的是java虚拟机栈执行的是java方法,而本地方法栈则是执行JVM调用到的native方法。JVM规范中并没有对这块有太多规定,所以不同的虚拟机栈可以自由实现它,有的虚拟机(Sun的HotSpot)就直接把本地方法栈和java虚拟机栈合二为一了。本地方法栈是线程私有的
java堆
java堆是JVM所管理的内存中最大的一块了,是所有线程共享的,在JVM启动的时候创建。java堆唯一的目的就是存放对象实例和数组,是垃圾收集器管理的主要区域。堆可分为新生代和老年代,再细分可分为Eden空间、From Survivor空间和To Survivor空间等。主流的虚拟机都是支持堆内存拓展的,当堆上没有内存可以为对象分配空间,达到最大容量且无法继续向操作系统申请内存的时候,会抛出OOM异常。
方法区
方法区和堆一样,是线程共享的内存区域,用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
有时会把方法区称为“永久代”,是因为在HotSpot虚拟机中,把GC的分代收集算法拓展到方法区,不用再编写方法区的内存管理代码。对于其它的JVM,是不存在永久代的概念的。当方法区无法满足分配需求的时候,会出现OOM。
运行时常量池
运行时常量池是方法区的一部分。javac编译后的class文件中,除了类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池、用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。除了保存class文件中描述的符号引用外,还会把翻译出来的直接引用也存放在运行时常量池中。
运行时常量池相对于class文件常量池的另外一个重要特性是具备动态性,Java语言并不要求常量一定是在编译期产生,也就是说,并非是预置入class文件中常量池的内容内能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中,用的比较多是有String.intern()。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。