JVM
内存区域包括 PC计数器、Java虚拟机栈、本地方法栈、堆、方法区、运行时常量池和 直接内存。java
本文主要介绍各个内存区域的做用和特性,同时分别阐述各个区域发生内存溢出的可能性和异常类型。编程
Java
虚拟机执行Java
程序的过程当中,会把所管理的内存划分为若干不一样的数据区域。这些内存区域各有各的用途,以及建立和销毁时间。有的区域随着虚拟机进程的启动而存在,有的区域伴随着用户线程的启动和结束而建立和销毁。后端
JVM
内存区域也称为Java
运行时数据区域。其中包括:程序计数器、虚拟机栈、本地方法栈、堆、静态方法区、静态常量池等。数组
注意:程序计数器、虚拟机栈、本地方法栈属于每一个线程私有的;堆和方法区属于线程共享访问的。缓存
程序计数器(Program Counter Register
)是一块较小的内存空间,它的做用能够看作是当前线程所执行的字节码行号指示器。多线程
PC
计数器。JVM
启动而生,JVM
关闭而死。Java
方法时,记录其正在执行的虚拟机字节码指令地址。Native
方法时,计数器记录为空(Undefined
)。Java
虚拟机规范中没有规定任何OutOfMemoryError
状况区域。线程私有内存空间,它的生命周期和线程相同。线程执行期间,每一个方法执行时都会建立一个栈帧(Stack Frame) ,用于存储 局部变量表、操做数栈 、动态连接 、方法出口 等信息。架构
每个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈和出栈的全过程。框架
下面依次解释栈帧里的四种组成元素的具体结构和功能:异步
局部变量表是一组变量值的存储空间,用于存储方法参数和局部变量。 在 Class
文件的方法表的 Code
属性的 max_locals
指定了该方法所需局部变量表的最大容量。分布式
局部变量表在编译期间分配内存空间,能够存放编译期的各类变量类型:
boolean
, byte
, char
, short
, int
, float
, long
, double
等8
种;reference
,指向对象起始地址的引用指针;returnAddress
,返回地址的类型。变量槽(Variable Slot
):
变量槽是局部变量表的最小单位,规定大小为
32
位。对于64
位的long
和double
变量而言,虚拟机会为其分配两个连续的Slot
空间。
操做数栈(Operand Stack
)也常称为操做栈,是一个后入先出栈。在 Class
文件的 Code
属性的 max_stacks
指定了执行过程当中最大的栈深度。Java
虚拟机的解释执行引擎被称为基于栈的执行引擎 ,其中所指的栈就是指-操做数栈。
32
字长为单位的数组。int
、long
、float
、double
、reference
和returnType
等类型 (对于byte
、short
以及char
类型的值在压入到操做数栈以前,也会被转换为int
)。虚拟机把操做数栈做为它的工做区——大多数指令都要从这里弹出数据,执行运算,而后把结果压回操做数栈。
1 |
begin |
在这个字节码序列里,前两个指令
iload_0
和iload_1
将存储在局部变量表中索引为0
和1
的整数压入操做数栈中,其后iadd
指令从操做数栈中弹出那两个整数相加,再将结果压入操做数栈。第四条指令istore_2
则从操做数栈中弹出结果,并把它存储到局部变量表索引为2
的位置。
下图详细表述了这个过程当中局部变量表和操做数栈的状态变化(图中没有使用的局部变量表和操做数栈区域以空白表示)。
每一个栈帧都包含一个指向运行时常量池中所属的方法引用,持有这个引用是为了支持方法调用过程当中的动态连接。
Class
文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用:
final
、static
域等),称为静态解析,当一个方法开始执行之后,只有两种方法能够退出当前方法:
Normal Method Invocation Completion
),通常来讲,调用者的PC
计数器能够做为返回地址。Abrupt Method Invocation Completion
),返回地址要经过异常处理器表来肯定。当一个方法返回时,可能依次进行如下3
个操做:
PC
计数器的值指向下一条方法指令位置。小结:
注意:在Java虚拟机规范中,对这个区域规定了两种异常。
其一:若是当前线程请求的栈深度大于虚拟机栈所容许的深度,将会抛出StackOverflowError
异常(在虚拟机栈不容许动态扩展的状况下);其二:若是扩展时没法申请到足够的内存空间,就会抛出OutOfMemoryError
异常。
本地方法栈和Java
虚拟机栈发挥的做用很是类似,主要区别是Java
虚拟机栈执行的是Java
方法服务,而本地方法栈执行Native
方法服务(一般用C编写)。
有些虚拟机发行版本(譬如
Sun HotSpot
虚拟机)直接将本地方法栈和Java
虚拟机栈合二为一。与虚拟机栈同样,本地方法栈也会抛出StackOverflowError
和OutOfMemoryError
异常。
Java
堆是被全部线程共享的最大的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。
在Java
中,堆被划分红两个不一样的区域:新生代 (Young Generation
) 、老年代 (Old Generation
) 。新生代 (Young
) 又被划分为三个区域:一个Eden
区和两个Survivor
区 - From Survivor
区和To Survivor
区。
简要概括:新的对象分配是首先放在年轻代 (
Young Generation
) 的Eden
区,Survivor
区做为Eden
区和Old
区的缓冲,在Survivor
区的对象经历若干次收集仍然存活的,就会被转移到老年代Old
中。
这样划分的目的是为了使JVM
可以更好的管理堆内存中的对象,包括内存的分配以及回收。
方法区和Java
堆同样,为多个线程共享,它用于存储类信息、常量、静态常量和即时编译后的代码等数据。
运行时常量池是方法区的一部分,Class
文件中除了有类的版本、字段、方法和接口等描述信息外,
还有一类信息是常量池,用于存储编译期间生成的各类字面量和符号引用。
直接内存不属于虚拟机运行时数据区的一部分,也不是Java
虚拟机规范中定义的内存区域。Java NIO
容许Java
程序直接访问直接内存,一般直接内存的速度会优于Java堆内存。所以,对于读写频繁、性能要求高的场景,能够考虑使用直接内存。
除了程序计数器外,Java
虚拟机的其余运行时区域都有可能发生OutOfMemoryError
的异常,下面分别给出验证:
Java
堆可以存储对象实例。经过不断地建立对象,并保证GC Roots
到对象有可达路径来避免垃圾回收机制清除这些对象。
当对象数量到达最大堆的容量限制时就会产生OutOfMemoryError
异常。
设置JVM
启动参数:-Xms20M
设置堆的最小内存为20M
,-Xmx20M
设置堆的最大内存和最小内存同样,这样能够防止Java
堆在内存不足时自动扩容。-XX:+HeapDumpOnOutOfMemoryError
参数可让虚拟机在出现内存溢出异常时Dump
出内存堆运行时快照。
HeapOOM.java
1 |
/** |
打开Java VisualVM
导出Heap
内存运行时的dump
文件。
HeapOOM
对象不停地被建立,堆内存使用达到99%
。垃圾回收器不断地尝试回收但都以失败了结。
分析:遇到这种状况,一般要考虑内存泄露和内存溢出两种可能性。
若是是内存泄露:
进一步使用
Java VisualVM
工具进行分析,查看泄露对象是经过怎样的路径
与GC Roots
关联而致使垃圾回收器没法回收的。
若是是内存溢出:
经过
Java VisualVM
工具分析,不存在泄露对象,也就是说堆内存中的对象必须得存活着。就要考虑以下措施:
- 从代码上检查是否存在某些对象生命周期过长、持续状态时间过长的状况,尝试减小程序运行期的内存。
- 检查虚拟机的堆参数(
-Xmx
与-Xms
),对比机器的物理内存看是否还能够调大。
关于虚拟机栈和本地方法栈,分析内存异常类型可能存在如下两种:
StackOverflowError
异常。OutOfMemoryError
异常。能够划分为两类问题,当栈空间没法分配时,到底时栈内存过小,仍是已使用的栈内存过大。
测试方案一:
-Xss
参数减小栈内存的容量,异常发生时打印栈的深度。设置JVM
启动参数:-Xss128k
设置栈内存的大小为128k
。
JavaVMStackSOF.java
1 |
/** |
测试结果:
分析:在单个线程下,不管是栈帧太大仍是虚拟机栈容量过小,当没法分配内存的时候,虚拟机抛出的都是
StackOverflowError
异常。
测试方案二:
JavaVMStackOOM.java
1 |
/** |
测试结果:
1 |
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread |
上述测试代码运行时存在较大的风险,可能会致使操做系统假死,这里就不亲自测试了,引用做者的测试结果。
运行时常量和字面量都存放于运行时常量池中,常量池又是方法区的一部分,所以两个区域的测试是同样的。
这里采用String.intern()
进行测试:
String.intern()是一个native方法,它的做用是:若是字符串常量池中存在一个String对象的字符串,那么直接返回常量池中的这个String对象;
不然,将此String对象包含的字符串放入常量池中,而且返回这个String对象的引用。
设置JVM
启动参数:经过-XX:PermSize=10M
和-XX:MaxPermSize=10M
限制方法区的大小为10M
,从而间接的限制其中常量池的容量。
RuntimeConstantPoolOOM.java
1 |
/** |
测试结果分析:
JDK1.6
版本运行结果:
1 |
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space |
JDK1.6
版本运行结果显示常量池会溢出并抛出永久带的OutOfMemoryError
异常。
而JDK1.7
及以上的版本则不会获得相同的结果,它会一直循环下去。
方法区存放Class
相关的信息,好比类名、访问修饰符、常量池、字段描述、方法描述等。
对于方法区的内存溢出的测试,基本思路是在运行时产生大量类字节码区填充方法区。
这里引入Spring
框架的CGLib
动态代理的字节码技术,经过循环不断生成新的代理类,达到方法区内存溢出的效果。
JavaMethodAreaOOM.java
1 |
/** |
JDK1.6
版本运行结果:
1 |
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space |
测试结果分析:
JDK1.6
版本运行结果显示常量池会溢出并抛出永久带的OutOfMemoryError
异常。
而JDK1.7
及以上的版本则不会获得相同的结果,它会一直循环下去。
本机直接内存的容量可经过-XX:MaxDirectMemorySize
指定,若是不指定,则默认与Java
堆最大值(-Xmx指定)同样。
测试场景:
直接经过反射获取Unsafe
实例,经过反射向操做系统申请分配内存:
设置JVM
启动参数:-Xmx20M
指定Java
堆的最大内存,-XX:MaxDirectMemorySize=10M
指定直接内存的大小。
DirectMemoryOOM.java
1 |
/** |
测试结果:
测试结果分析:
由DirectMemory
致使的内存溢出,一个明显的特征是Heap Dump
文件中不会看到明显的异常信息。
若是OOM
发生后Dump
文件很小,而且程序中直接或者间接地使用了NIO
,那么就能够考虑一下这方面的问题。
欢迎关注技术公众号: 零壹技术栈
本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。