Java 面试复习_5
2019-5-28
作者:水不要鱼
(注:能力有限,如有说错,请指正!)
Java 规范中将内存大致分为了以下几个区:
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
- 堆
- 方法区
其中,程序计数器
,Java 虚拟机栈
,本地方法栈
是属于线程私有的,也就是每个线程会有自己的一份,而
方法区
和堆
是线程共享的,也就是只会有一份,所有线程都访问这一份。
扩展:JVM 内存每一个区域的作用
-
程序计数器:线程私有,也就是每个线程内部都会有一份,原因就在于,它记录的是当前字节码执行的指令的地址。
举个例子,A 线程和 B 线程,当 A 线程在执行字节码第 35 行的时候,时间片用完了,于是系统将运行 B 线程,
而 A 线程就需要将这个 35 记录下来,只有这样,当系统将时间片分配到 A 线程上,也就是 A 线程又继续恢复
运行的时候,才知道要从哪一行字节码开始运行。如果线程执行的是 Java 方法,这个计数器记录的就是字节码指令的地址;
如果线程执行的是本地方法(使用 native 关键字修饰的方法),那这个计数器就是空。 -
Java 虚拟机栈:线程私有,而且生命周期和线程一样,也就是线程存活的时候存活,线程销毁的时候销毁。
我们可以注意到这是个栈,也就是先进后出的模型,具体入栈的是每一个方法调用的内存模型,说白了就是,每一个
Java 方法在调用的时候会有属于这个方法的数据,比如局部变量表,操作数栈,方法的出口等,而这些数据就会存在
这个 Java 虚拟机栈中。举个例子,一个线程调用了 A 方法,这时候会将 A 方法的数据包装成一个栈帧,这个
栈帧包含了局部变量表(比如方法里面写了int a = 0;
,这个 a 就是局部变量,就会保存在局部变量表中),
操作数栈还有方法出口等,包装成一个栈帧之后就会将它 push 进 Java 虚拟机栈,如果 A 方法内部调用了 B 方法,也会有同样的
过程,也就是将 B 方法的数据包装成栈帧,然后 push 进 Java 虚拟机栈,这时候栈顶就是 B 方法的栈帧了。
当 B 方法执行完了,就会将 B 方法的栈帧从 Java 虚拟机栈中 pop 出来,这时候栈顶又变回 A 方法的栈帧了。
所以,很明显这个栈帧必须是线程私有的,也就是每一个线程都独占一份的才能正常工作。另外,从上面可以看出,局部变量表中的数据在编译期
就会确定好,也就是说,调用一个方法的时候要分配多大的局部变量空间是完全确定的。如果这个方法要求的栈空间太大,
比如超过了虚拟机允许的线程栈大小,就会提示 StackOverflowError 异常,这在递归深度很大的时候就会看见。
部分虚拟机是允许这个虚拟机栈动态扩展的,如果扩展时内存空间不足,就会提示 OutOfMemoryError 异常。 -
本地方法栈:线程私有,与 Java 虚拟机栈类似,只不过 Java 虚拟机栈针对的是 Java 方法,而本地方法栈针对的是本地方法,
也就是使用 native 关键字修饰的方法。除此之外,一些虚拟机上直接把本地方法栈和 Java 虚拟机栈合为一个,比如 HotSpot。。。 -
Java 堆:线程共享,这是 JVM 管理的内存中最大的一块,也是我们使用最多的一块。几乎所有的对象都在这个区域中分配内存,
为什么说是“几乎”呢?因为随着 JIT 编译器的优化和升级,栈上分配之类的变量逃逸优化会导致这个对象不一定分配在堆上,这些优化手段
在 Golang 中体现的很明显。另外,由于这个区域比较大,所以也是垃圾收集器管理的最主要区域,而很多垃圾收集器都是使用分代收集算法,
这就使得 Java 堆被进一步分成了多个区域,比如分为新生代和老年代,再细致的分还可以分成 Eden 区、From Survivor 区和 To Survivor 区。
当然,如果堆空间不足,就会抛出 OutOfMemoryError 异常。 -
方法区:线程共享,用于存放虚拟机加载的类信息、常量、静态变量以及编译之后的代码等。但是从 JDK7 之后,字符串常量池已经从这里面移出来了。
我们应该注意的是,方法区中有一个运行时常量池,专门用于存放类信息中的常量池,具体就是编译期生成的字面量和符号引用。这个区域和堆一样,
也被列入了垃圾收集的范围,所以,如果内存不足了,就会抛出 OutOfMemoryError 异常。
下面我们来看一个总结的例子:
class Test {
private int a = 0; // 几乎可以说保存在 堆 中,为啥是几乎,上面已经说过了
private static int b = 0; // 保存在 方法区 中
private final int c = 0; // 几乎可以说保存在 堆 中,为啥是几乎,上面已经说过了
private static final int d = 0; // 保存在 方法区 中
// 我们要知道的是,static 会影响变量的存储位置,静态变量就是保存在方法区中,
// 而 final 不会影响变量的存储位置,上面提到的方法区存放的常量并不是指这个 final 常量
private void test() {
int e = 0; // 存放在 Java 虚拟机栈 中
}
}
除了上面所提到的 JVM 管理的内存,在 JDK1.4 之后新加入的 NIO 使用到了堆外的内存。这块内存是使用本地方法分配的,
它避免了数据在本地内存和 JVM 内存之间来回复制,所以性能会更好。但是也会有一个坑,因为这块内存是直接分配的,
而不像 JVM 堆可以使用 -Xmx 设置大小,这就会导致如果你的 -Xmx 设置的太大,也就是堆内存分配的太大,而
你的程序又使用了大量的直接内存,就会导致内存不足。举个例子,一台服务器的内存是 32 GB,你的 JVM 堆使用了
16 GB 内存,而系统和其他应用占了 8 GB 内存,这时候的内存已经被分配走了 24 GB 内存,可用内存就只剩下 8 GB 了。
如果你的程序中还使用到了大量的直接内存,比如做 NIO 的时候,你使用到了 12 GB 内存,很明显,系统内存已经不足,
要么就会开始使用交换空间,要么就会内存不足导致程序崩溃甚至系统崩溃。