Java虚拟机在执行java程序的过程中会它所管理的内存划分为若干个不同的数据区域。主要有程序计数器、java虚拟机栈、本地方法栈、java堆和方法区五个区域。
1、程序计数器
一块较小的内存空间,属于线程私有的内存,他的作用可以看作是当前线程作执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
因为线程之间经常存在切换,所以每个线程都需要一个独立的程序计数器。如果正在执行一个java方法,它记录的是在正在执行的虚拟机字节码指令的地址;如果正在执行Native方法,则计数器的值为空(undefined)。
在Java虚拟机规范中,这个区域是唯一一个不会抛出 OutOffMemoryError 的内存区域。
2、java虚拟机栈
它也是线程私有的,生命周期与线程相同。描述的是java方法执行的内存模型,每个方法被执行的时候都会创建一个栈帧(Stack frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法从被调用到执行完成,对应着一个栈帧在虚拟机中的从入栈到出栈的过程。栈帧是方法执行期的基础数据结构。
在Java虚拟机规范中,这个内存区域可能会抛出 StackOverflowError 和 OutOffMemoryError 两种异常。当线程请求的栈深度大于虚拟机所允许的深度抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存会抛出 OutOffMemoryError 异常。
虚拟机栈StackOverflowError案例:
Eclipse设置虚拟机参数:-Xss256k 虚拟机栈内存容量。
程序代码:
public class MainTest {
private static int stackLen = 1;
public static void main(String[] args) {
doStackSOF();
}
//虚拟机参数 -Xss256k
private static void doStackSOF() {
try {
stackLeak();
}catch(Throwable e){
System.out.println("stackLen:" + stackLen);
throw e;
}
}
public static void stackLeak() {
stackLen ++;
stackLeak();
}
}
输出:
stackLen:1843
Exception in thread "main"
java.lang.StackOverflowError
at cn.psq.javavm.MainTest.stackLeak(MainTest.java:26)
at cn.psq.javavm.MainTest.stackLeak(MainTest.java:27)
......
操作系统的剩余内=总内存-最大堆内存容量(Xmx)-最大方法区容量(MaxPermSize)-程序计数器消耗的内存(很小)-虚拟机本身消耗的内存
剩余内存被虚拟机栈和本地方法瓜分;所以每个线程分配到的栈容量越大,可以建立的线程数量越小,建立线程就越容易把剩下的内存耗尽。如果在建立过多线程导致内存溢出,再不能减少线程数量和更换64位虚拟机时,就只能通过减少最大堆和减少栈容量来换取更多线程。
3、本地方法栈
与虚拟机栈发挥的作用非常相似,为虚拟机执行的本地方法服务。在Java虚拟机规范中,这个内存区域也可能会抛出 StackOverflowError 和 OutOffMemoryError 两种异常。
4、java堆
对于大多数应用,java堆是java虚拟机所管理的内存中最大的一块区域。是被所有线程共享的一片内存区域,虚拟机启动时就创建好了,唯一的目的就是存放对象实例。它也是垃圾搜集器管理的主要区域,有些时候被成为“GC堆”。
在Java虚拟机规范中,java堆可以物理上不连续,但是逻辑上要求连续。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,会抛出 OutOffMemoryError 异常。堆内存 OutOffMemoryError 是实际应用中最常见的内存溢出异常。
堆OutOffMemoryError案例:
Eclipse设置虚拟机参数:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
参数设置说明:
(1)-Xms堆最小值和-Xmx堆最大值都设置成20M是为了避免堆自动扩展。
(2)-XX:+HeapDumpOnOutOfMemoryError 在虚拟机出现内存溢出异常时,dump出当前内存堆转储快照,方便事后分析。
程序代码:
public class MainTest {
public static void main(String[] args) {
doOOMObject();
}
private static void doOOMObject() {
List<MainTest> list = new ArrayList<MainTest>();
while(true) {
list.add(new MainTest());
}
}
}
输出:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid27142.hprof ...
Heap dump file created [29651203 bytes in 0.071 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3720)
at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
at java.base/java.util.ArrayList.grow(ArrayList.java:238)
at java.base/java.util.ArrayList.grow(ArrayList.java:243)
at java.base/java.util.ArrayList.add(ArrayList.java:486)
at java.base/java.util.ArrayList.add(ArrayList.java:499)
at cn.psq.javavm.MainTest.doOOMObject(MainTest.java:16)
at cn.psq.javavm.MainTest.main(MainTest.java:9)
解决这个区域的异常,一般手段是首先通过内存映像分析工具(如:Eclipse Memory Analyzer)对dunp出来的堆转储快照进行分析,重点确认内存中的对象是否是有必要,也就是要确认的到底是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,需要通过工具进一步查看泄漏对象到GC Roots的引用链,这样就可以比较准确的定位出内存泄漏代码的位置。如果不存在泄漏,内存中的对象确实必须活着,那就应该检查虚拟机的堆参数(-Xms和-Xmx),与物理内存对比看看是否可以调大。从代码层面检查某些对象生命周期和持有状态是否过长,尽量降低程序运行时期的内存西消耗。
5、方法区
它也是被所有线程共享的一片内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器便以后的代码等数据。
在Java虚拟机规范中,这个区域不需要连续的内存,且可选择固定或者可扩展大小,还可以选择不实现立即搜集。当方法区无法满足内存分配需求时,会抛出 OutOffMemoryError 异常。