在我们学习JVM内存区域划分之前,先来看一下这几个名词:
- JDK(Java Development Kit):程序开发者用来编译、调试Java程序所使用的开发工具包。
- JRE(Java Runtime Environment):Java运行环境
- JVM(Java Virtual Machine):Java虚拟机,是JRE的一部分
1.Java内存区域划分:
- 线程私有内存:
即每个线程都有,彼此之间完全隔离。
- PC计数器:简单理解,就是记录当前线程运行到哪一步。
a. 若当前线程执行的是Java方法,则记录的是正在执行的JVM字节码指令地址。
b. 若当前线程执行的是native方法,则计数器值为0.
PC计数器是唯一一块不会发生OOM异常的区域。 - 虚拟机栈:描述Java方法执行的内存模型。
在创建一个线程的同时会创建它的虚拟机栈,线程执行结束,虚拟机栈与线程一同被回收。在这个区域中会出现两种异常:
a. 单线程情况下,若请求的栈深度大于虚拟机栈的深度,会有栈溢出异常StackOverFlowError。
b. 多线程情况下,若无法申请到足够的内存,则会抛出OOM(OutOfMemoryError)。 - 本地方法栈:描述本地方法执行的内存模型
这个与虚拟机栈类似,只不过是用来描述native方法的。但要注意的是,在HotSpot虚拟机中,本地方法栈与虚拟机栈是同一块内存区域。
- 线程共享内存:
即所有线程共享此内存空间,此空间对所有线程可见。
- 堆:用来存放类的实例化对象以及数组
堆是JVM管理的最大内存区域,也是垃圾回收管理的最主要内存区域。
若在堆中无法申请到足够的内存完成对象的实例化分配或不能再扩容时,抛出OOM。 - 方法区:存储已被JVM加载的类信息、常量、静态变量等。
JDK8以前,也叫永久代;在JDK1.7时,将静态变量转移到了堆上。
JDK8之后,叫元空间。
方法区无法满足内存分配需求时,就会抛出OOM。 - 运行时常量池:存放字面量与符号引用
运行时常量池其实也是方法区的一部分。
字面量:字符串常量、final常量、基本数据类型的值等。在JDK1.7时,将字符串常量转移到了堆上。
符号引用:编译时,class文件是通过符号引用来实现的。运行时会将符号引用解析为指向内存地址的直接引用。
2.StackOverFlowError:
当我们请求的栈深度大于JVM限定的栈深度,会抛出此异常,来看代码:
public static void main(String[] args) {
System.out.println(add(1));
}
public static int add(int a){
a++;
return add(a);
}
结果如下:
其实递归就是不断压栈,直到最后返回再一步步出栈的过程。如果我们在递归中没有设置出口,那么当不断压栈直到超出本来的栈深度后,就会抛出栈溢出异常。
3.OutOfMemoryError:
在多线程情况下,如果无法申请到足够的内存,则会抛出OOM异常。
public class ErrorTest{
static class OOMError{
}
public static void main(String[] args) {
List<OOMError> list = new ArrayList<>();
while(true){
list.add(new OOMError());
}
}
}
其实,OOM可以分为以下两种情况:
- 内存溢出:即内存中的对象确实应该存活,但是堆内存不够而引起的异常
- 内存泄漏:即无用的对象无法被回收