呐,最近开始对JVM产生的不轨的心,所以想看看它的外在美和内在美啵。
JVM的运行时数据区
- 程序计数器(线程私有)
程序计数器是一块较小的内存空间。它可以看做是当前程序锁执行的字节码的行号指示器。(在虚拟机概念模型里,字节码解释器工作时就是通过改变计数器的值来选取吓一跳需要执行的字节码指令,如分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成)
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。
如果是Native(本地)方法,则计数器的值为空(Undefined)。
程序计数器是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。 - JVM 栈(线程私有)
每个线程对应着一个JVM栈,JVM栈的生命周期与线程相同。(每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法被调用时,栈帧入栈。方法执行结束,栈帧出栈)
局部变量表存放了方法的局部变量,包括各种基础数据类型、对象引用以及返回地址等。(long和double类型数据会占用2个局部变量空间Slot,其它数据类型只占用1个Slot)
局部变量表的内存空间在编译期间就确定了,当进入一个方法,方法需要在栈帧中分配多少内存空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
JVM栈定义了2种异常。1种是如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflow异常。2如果虚拟机栈可以动态扩展,在扩展时无法申请到足够内存,将会抛出OOM异常。(当前大部分JVM都可以动态扩展:当栈空间不够时,JVM会自动加大栈空间。目前主要有Segmented stack和Stack copying两种方式扩展。Segmented方式在栈空间不够用时,会再分配一个栈,然后用链表将这些栈连接起来。copy方式有点类似vector动态扩展方式,即再分配一个更大的栈,然后把原来的栈复制过来。) - 本地方法栈(线程私有)
本地方法栈和JVM栈十分相同,唯一的不同是本地方法栈是为JVM使用到的Native方法服务,而JVM栈为JVM执行的Java方法服务。有些JVM直接将两者合二为一了(Sun HotSpot)
- JVM 堆(线程共享)
堆是JVM管理的内存中最大的一块,也是GC的重点区域。
堆存在的目的就是为了存放对象实例。
所有对象实例和数组都要在堆上分配。(但随着技术发展,也有在栈上分配的)
根据JVM规范规定:JVM堆逻辑上是连续的(物理上可以处于不连续的空间)。在实现时,堆可以固定大小也可以扩展的,目前主流JVM都是可以扩展的,如果堆上没有足够的内存完成实例分配并且无法再扩展时,将会抛出OOM异常。 - 方法区(线程共享)
方法区用于存储已被JVM加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
方法区和堆一样在物理上可以是不连续的空间。也可以选择固定大小或者可扩展的。
方法区可以选择不进行GC,方法区进行GC时比较少的。在方法区进行内存回收的目标主要针对常量池和堆类型的卸载。方法区进行垃圾回收条件非常苛刻而且效果也很难令人满意,所以一般不进行GC。
当方法区无法满足内存分配时,将抛出OOM异常。 - 运行时常量池
运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量,直接引用和符号引用(在编译一个类调用另外一个类或者接口的字段或方法时,class文件中是通过符号引用来实现的,因为在编译时并不知道引用类的实际地址,只能先用符号引用替代)。
运行时常量池并不只有存储编译期的常量,在运行期间也能将新的常量放入池中(比如使用String类的intern方法)
运行时常量池在无法申请内存时,会抛出OOM异常。 - 直接内存
直接内存并不是JVM运行时数据区的一部分,直接内存可以理解为JVM以外的机器内存。
JDK提供了一种基于通道与缓冲区的内存分配方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用直接进行操作。 机器内存也是有限制的,在内存不够的时候回抛出OOM异常。
To be continued...