JVM 运行时内存简述

6

了解JVM内存分配,有助于理解程序运行机制,更好进行软件优化。

运行时数据区域

程序计数器

每个线程都有自己的程序计数器,可以把它想象成一个箭头(→),执行代码时,它一行一行的向下移动,所以他来选取执行到哪一条字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要程序计数器来完成。

Java的多线程是通过线程之间轮流执行切换方式来实现的,每个线程的执行时间是有不确定性的,当线程执行时间结束时,程序计数器会记录当前执行到那一条字节码指令,再次获得执行时间时,就通过程序计数器来确定本次从哪个位置继续执行。每个线程都有自己独立的计数器,互不影响,独立储存,这种内存区域成为 “线程私有内存” 。

Java栈

此区域也是线程私有的,它的生命周期与线程同步。用来描述当前线程Java方法执行的内存模型;线程中每个方法执行都会创建一个栈帧,用于储存局部变量表、操作数栈、动态链接、方法出口等。方法从开始调用到执行结束的过程,就相当于栈帧入栈和出栈的过程。

局部变量表储存编译器能够确定的各种基本数据类型和对象引用,局部变量表所需的空间在编译期间完成分配,当方法运行时他的局部变量的空间是确定的,从运行到数据都不会改变大小。

此区域在Java虚拟机规范中定义了两种异常:当线程请求的栈深度大于虚拟机所规定的深度时,将抛出 StackOverflowError 异常,如果扩展到虚拟机无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。

PS:本地方法(Native Method,Java Native Interface,JNI),下一部分是本地方法栈内存说明,了解本地方法栈之前必须知道本地方法是什么?从Java1.1开始就存在JNI标准。Java想要访问底层系统资源就需要JNI来调用C或C++等更贴近系统的语言来实现,使用其他语言编写功能后编译成和处理器相关的代码保存在动态链接库中,通过native关键词标注的方法调用。因为每个系统平台底层机制不同,所以过多使用native方法就会导致程序无法在跨平台运行。

本地方法栈

顾名思义就是为本地方法服务的,不同的虚拟机对此区域的实现方式不同,Sun HotSpot直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java堆

Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程所共享的,存放所有对象实例。它在Java虚拟机规范中的描述是:所有对象实例和数组对要在堆上分配内存,但由于JIT编译器技术于逃逸分歧技术栈上分配、标量替换优化技术的发展将会导致一些微妙的变化,所有对象都分配到Java堆上也就没有那么绝对了。

Java堆是垃圾收集器管理的主要区域,因此也被成为 “GC堆” 。

Java堆既可以实现固定大小的也可以是可以扩展的,主流虚拟机都是可扩展的,通过 -Xmx 和 -Xms控制。如果堆内存空间无法完成对象内存分配也无法在扩展时将抛出OutOfMemoryError 异常。

方法区

所有线程共享,用于存在已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。Java虚拟机规范将方法区描述为Java堆的一个逻辑部分,但它有另一个名字 “Non-Heap(非堆)”,目的应该是与Java堆区分开。当方法区无法满足内存分配时将抛出OutOfMemoryError 异常。

运行时常量池

运行时常量池是方法区的一部分,用于存放Class文件编译器生成的各种字面量和符号引用、直接引用。Java并不要求常量一定只有编译器才能产生,运行期也可以将常量放入运行时常量池中,比如 String 的 intern() 方法。

既然是方法区的一部分,当方法区无法满足内存分配时将抛出 OutOfMemoryError 异常。

直接内存

直接内存并不是虚拟机内存的一部分,也没有在Java虚拟机规范中定义,这部分内存被频繁使用,也会抛出 OutOfMemoryError 异常。他就是除了虚拟机运行所需外的其他内存受本机物理内存的限制,当虚拟机无法动态扩展获取内存时将抛出 OutOfMemoryError 异常。