前言
那些使用过 C 或者 C++ 的读者一定会发现这两门语言的内存管理机制与 Java 的不同。在使用 C 或者 C++ 编程时,程序员需要手动的去管理和维护内存,就是说需要手动的清除那些不需要的对象,否则就会出现内存泄漏与内存溢出的问题。
如果你使用 Java 语言去开发,你就会发现大多数情况下你不用去关心无用对象的回收与内存的管理,因为这一切 JVM 虚拟机已经帮我们做好了。了解 JVM 内存的各个区域将有助于我们深入了解它的管理机制,避免出现内存相关的问题和高效的解决问题。下面来讲讲面试中也是Java学习进阶中必备的JVM知识,后续还会更新完JVM系列,观看的朋友可以转发关注下!
引出问题
在 Java 编程时我们会用到许多不同类型的数据,比如临时变量、静态变量、对象、方法、类等等。 那么他们的存储方式有什么不同吗?或者说他们存在哪?
运行时数据区域
Java 虚拟机在执行 Java 程序过程中会把它所管理的内存分为若干个不同的数据区域,各自有各自的用途。
1.程序计数器
线程私有的,可以看作是当前线程所执行字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
这时唯一一个没有规定任何 OOM 异常的区域。
2.虚拟机栈
虚拟机栈也是线程私有的,生命周期与线程相同。栈里面存储的是方法的局部变量、对象的引用等等。
在这片区域中,规定了两种异常情况,当线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。当虚拟机栈动态扩展无法申请到足够的内存时会抛出 OOM 异常。
3.本地方法栈
和虚拟机栈的作用相同,只不过它是为 Native 方法服务。HotSpot 虚拟机直接将虚拟机栈和本地方法栈合二为一了。
4.堆
堆是 Java 虚拟机所管理内存中最大的一块。是所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域唯一的作用就是存放对象实例,也就是 NEW 出来的对象。这个区域也是 Java 垃圾收集器的主要作用区域。
当堆的大小再也无法扩展时,将会抛出 OOM 异常。
5.方法区
方法区也是线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量等等。当方法区无法满足内存分配需求时,会抛出 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 等修改。
例子:在集合中无限加入对象,效果受到机器配置影响,可以主动更改堆大小方便演示。
java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class 或者 JSP 页面,或者采用 CGLIB 等反射机制的情况,因为上述情况会产生大量的 Class 信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似 -XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出,因为常量池也是方法区的一部分。
例子:无限加载 Class,需要在 JDK 1.8 之前的版本运行,因为1.8将方法区改成了元空间,利用了机器的内存,最好手动设置 -XX:MaxPermSize,将值调小一点。
java.lang.StackOverflowError ------> 不会抛 OOM error,但也是比较常见的 Java 内存溢出。Java 虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小就会出现此种溢出。可以通过虚拟机参数 -Xss 来设置栈的大小。
例子:无法快速收敛的递归。
总结
JVM内存区域划分,便于它能够更加高效的管理自身的内存。当程序中出现这种由于JVM造成的内存溢出的情况的时候,需要根据不同的情况做不同的分析与处理。
最后
读到这的朋友可以转发关注下,后续还会更新JVM及性能调优系列的精选文章,谢谢您的支持!