Java内存区域与内存溢出异常
标签(空格分隔): Java
写在前面:
系统存在一个主内存,Java所有变量都存在在主存中,对于所有线程是共享的;
每个线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝。线程对所有变量的操作都是在工作内存中进行,线程之间无法互相直接访问,变量传递需要通过主存完成。
运行时数据区域
JVM在执行Java程序的过程中会把它所管理的内存划分若干个不同的数据区域。每个区域都有各自的用途,以及创建和销毁的时间,有的随着JVM进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
如图所示:
程序计数器(Program Counter Register)
程序计数器是线程私有的,可以看做是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要程序计数器来完成。
Java虚拟机栈(Java Virtul Machine Stacks)
Java虚拟机栈就是我们常说的栈。它也是线程私有的,它的生命周期与线程相同。用栈帧(Stack Frame)用于存放局部变量表、操作数栈、动态链接、方法出口等信息。
主要存放:基本数据类型、对象引用(reference)、returnAddress类型(指向了一条字节码指令的地址)。
在这个区域可能会出现两种异常:
- StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出;
- OutOfMemoryError:如果虚拟机栈可以动态扩展,如果扩展是无法申请到足够的内存,酒鬼抛出该异常。
本地方法栈(Native Method Stack)
与虚拟机栈的区别是,虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。
也会抛出StackOverflowError和OutOfMemoryError异常.
堆(Heap)
Java堆是JVM所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。主要存放对象实例。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
会抛出OutOfMemoryError异常.
方法区(Method Area)
是各个线程共享的内存区域,用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
会抛出OutOfMemoryError异常
HotSpot虚拟机对象
对象的创建
在Java中,对象一般由new指令创建出来,那么,但JVM遇到new指令时会怎么做呢?
- 检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析或初始化过。如果没有,就必须先进行类加载过程。
- JVM为新生对象分配内存。(对象所需的内存大小在类加载完成后就可以完全确定)
- JVM需要将分配到的内存空间全部初始化为零;
- JVM对对象进行必要的设置,信息存放入对象头(Object Header)中。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data),对齐填充(Padding).
对象的访问定位
在Java中,每个线程拥有自己的虚拟机栈,在栈中保存有对所需对象的reference。
stack(reference) ---> heap(object)
所以,Java中栈中的引用是如何定位、访问了堆中的对象呢?
句柄
如果是用句柄方式的话,Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据(Heap)和类型数据(Method Area)各自的具体地址信息。
直接指针
如果是使用直接指针方式的话,reference中存放的是对象实例数据(Heap)和对象类型数据(Method Area)的指针。
OutOfMemoryError异常
堆溢出
java.lang.OutOfMemoryError: Java heap space
出现了这类异常,首先要分清是内存泄露(Memory Leak)还是内存溢出(Memory Overflow).
Memory Leak
通过一些内存映像分析工具查看泄露对象到GC Roots的引用链。这样就可以找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息以及GC Roots引用链的信息,就可以比较精准的定位出泄露代码的位置
以上也可以回答面试题:如何定位内存泄露/出现了内存泄露怎么办?
Memory Overflow
如果不存在内存泄露,也就是对象确实还存活,就可以检查JVM的堆参数(-Xms与-Xmx),看与物理内存相比是否还可以调大一些。
虚拟机栈和本地方法栈溢出
在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,对于他们,会存在两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,则抛出StackOverflowError。(When the depth of stack is over the largets depth which JVM defines, then it'll throw this kind of exception.)
- 如果虚拟机在扩展栈时无法申请到足够的空间,则会抛出OutOfMemoryError.
如下实例:
public class JVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
JVMStackSOF sof = new JVMStackSOF();
try {
sof.stackLeak();
} catch(Throwable e) {
System.out.println("stack length: " + sof.stackLength);
throw e;
}
}
}
方法区和运行时常量池溢出
Java中,例如字符串的创建:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中的这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
总结,本篇博客主要分析了Java中内存区域的分配(3个线程私有的内存区:虚拟机栈,程序计数器,本地方法区;2个线程共享的内存区:堆,方法区),对象的创建过程(检查,分配,初始化,设置)以及一些常见的异常。
Reference
[1] 周志明. 深入理解Java虚拟机[M]. 北京:机械工业出版社, 2013: 61-100.