对于一名高级 Java 工程师来说,JVM 可以说是面试必问的一个知识点,而大多数人可能没有对 JVM 的实际开发和使用经验,接下来这一系列文章将带你深入了解 JVM 需要掌握的各个知识点。这也将帮助你完成从初级程序员到高级程序员的转变。
本 Chat 的内容如下:
- JVM 内存划分与内存溢出异常
- 垃圾回收算法与收集器
- 虚拟机的类加载机制
- 内存模型与线程
- 虚拟机性能监控工具介绍
带你全面了解高级 Java 面试中需要掌握的 JVM 知识点。 ——当年明月
目录:
JVM 内存划分与内存溢出异常
概述
如果在大学里学过或者在工作中使用过 C 或者 C++ 的读者一定会发现这两门语言的内存管理机制与 Java 的不同。在使用 C 或者 C++ 编程时,程序员需要手动的去管理和维护内存,就是说需要手动的清除那些不需要的对象,否则就会出现内存泄漏与内存溢出的问题。
如果你使用 Java 语言去开发,你就会发现大多数情况下你不用去关心无用对象的回收与内存的管理,因为这一切 JVM 虚拟机已经帮我们做好了。了解 JVM 内存的各个区域将有助于我们深入了解它的管理机制,避免出现内存相关的问题和高效的解决问题。
引出问题
在 Java 编程时我们会用到许多不同类型的数据,比如临时变量、静态变量、对象、方法、类等等。 那么他们的存储方式有什么不同吗?或者说他们存在哪?
运行时数据区域
Java 虚拟机在执行 Java 程序过程中会把它所管理的内存分为若干个不同的数据区域,各自有各自的用途。
(图片来源于网络)
程序计数器
线程私有的,可以看作是当前线程所执行字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
这时唯一一个没有规定任何 OOM 异常的区域。
虚拟机栈
虚拟机栈也是线程私有的,生命周期与线程相同。栈里面存储的是方法的
局部变量
、对象的引用
等等。在这片区域中,规定了两种异常情况,当线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。当虚拟机栈动态扩展无法申请到足够的内存时会抛出 OOM 异常。
本地方法栈
和虚拟机栈的作用相同,只不过它是为 Native 方法服务。HotSpot 虚拟机直接将虚拟机栈和本地方法栈合二为一了。
堆
堆是 Java 虚拟机所管理内存中最大的一块。是所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域唯一的作用就是存放
对象实例
,也就是 NEW 出来的对象。这个区域也是 Java 垃圾收集器的主要作用区域。当堆的大小再也无法扩展时,将会抛出 OOM 异常。
方法区
方法区也是线程共享的内存区域,用于存储已经被虚拟机加载的
类信息
、常量
、静态变量
等等。当方法区无法满足内存分配需求时,会抛出 OOM 异常。这个区域也被称为永久代。
补充
虽然上面的图里没有运行时常量池和直接内存,但是这两部分也是我们开发时经常接触的。所以给大家补充出来。
运行时常量池
运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种
字面量
和符号引用
,这部分内容将在类加载后存放到方法区的运行时常量池中。也会抛出 OOM 异常。直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是却是
NIO
操作时会直接使用的一块内存,虽然不受虚拟机参数限制,但是还是会受到本机总内存的限制,会抛出 OOM 异常。
JAVA8 的改变
对于方法区,它是线程共享的,主要用于存储类的信息,常量池,方法数据,方法代码等。我们称这个区域为永久代
。
大部分程序员应该都见过 java.lang.OutOfMemoryError:PermGen space 异常,这里的 PermGen space
其实指的就是方法区。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代
的内存溢出,典型的场景是在 JSP 页面比较多的情况,容易出现永久代内存溢出。
在JDK 1.8中,HotSpot 虚拟机已经没有 PermGen space 这个区域了,取而代之的是一个叫做Metaspace
(元空间)的东西。
(图片来源于网络)
变化就是移除了方法区,增加了元空间,与方法区最大的区别是:元空间不再虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制。
这样更改的好处:
- 字符串常量存在方法区中,容易出现性能问题和内存溢出。
- 类和方法的信息等比较难确定大小,因此对于方法区大小的指定比较困难,太小容易出现方法区溢出,太大容易导致堆的空间不足。
- 方法区的垃圾回收会带来不必要的复杂度,并且回收效率偏低(垃圾回收会在下一章给大家介绍)。
内存溢出
虽然有 JVM 帮我们管理内存,但是在实际开发过程中一定还会遇到内存溢出的问题。堆,栈,方法区都有可能出现内存溢出问题。下面我们就结合几个实际的小例子来给大家展示一下,方便大家以后根据不同的情况对内存溢出问题进行快速准确的定位。
java.lang.OutOfMemoryError: Java heap space ———>java 堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数 -Xms、 -Xmx 等修改。
例子:在集合中无限加入对象,效果受到机器配置影响,可以主动更改堆大小方便演示。
public class HeapOOM { public static void main(String[] args){ long i= 0; try { List<Object> objects = new ArrayList<Object>(); while (true) { i++; objects.add(new Object()); System.out.println(i); } } catch(Throwable ex) { System.out.println(i); ex.printStackTrace(); } }}
70091068700910697009107070091071java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:265) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231) at java.util.ArrayList.add(ArrayList.java:462) at HeapOOM.main(HeapOOM.java:14)
java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class 或者 JSP 页面,或者采用 CGLIB 等反射机制的情况,因为上述情况会产生大量的 Class 信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似 -XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出,因为常量池也是方法区的一部分。
例子:无限加载 Class,需要在 JDK 1.8 之前的版本运行,因为1.8将方法区改成了元空间,利用了机器的内存,最好手动设置 -XX:MaxPermSize,将值调小一点。
public class HeapOOM { public static void main(String[] args) throws Exception { for (int i = 0; i < 100_000_000; i++) { generate("cn.paul.test" + i); } } public static Class generate(String name) throws Exception { ClassPool