Skip to content

Java 内存区域与内存溢出异常

因为 Java 程序员把控制内存的权力交给了 JVM,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,将会难以排查错误,修正问题

[!hint] Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来

  • 对于 C++ 的开发者来说,C++ 需要开发者手动地进行内存的分配和释放操作,如果操作不当可能会导致内存泄漏 …… ,而 Java 有自动的垃圾收集机制,开发者不需要过多操心内存管理的细节
  • Java 由于有垃圾收集器,所以也有一些局限性【在某些对性能要求极高,需要更精细控制内存的场景下,Java 的垃圾收集机制可能会带来一些不可预测的暂停,也无法做到极致的内存控制

运行时的数据区

350

程序计数器

[!quote] 程序计数器 当前线程所执行的字节码的行号指示器,每个线程有一个独立的程序计数器,这些程序计数器互不影响,独立存储,所以是线程私有内存

  • 字节码解释器工作时,就是通过改变程序计数器的值来选取下一条需要执行的字节码指令
  • 分支、循环、跳转、异常处理、线程恢复 ……基础功能都需要依赖程序计数器来完成

虚拟机栈

[!quote] 虚拟机栈 一个线程对应一个虚拟机栈,在虚拟机栈中有很多个栈帧,一个方法对应一个栈帧【当方法被执行时,JVM 都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口】,所以虚拟机栈也是线程私有内存

[!quote] 局部变量表 存放了 Java 中的基本数据类型对象引用returnAddress【指向了一条字节码指令的地址】

  • 局部变量表中,存储空间由局部变量槽来表示。每个变量槽可以容纳一个数据项,对于 64 位长度的 longdouble,占用两个变量槽,其余的数据类型只占用一个
  • 局部变量表的内存空间【变量槽的数量】在编译期间就已经确定,并且不会再方法运行期间改变,但是每个变量槽的大小由 JVM 判断架构和性能需求确定
  • StackOverflowError 线程请求的栈深度大于虚拟机所允许的深度
  • OutOfMemoryError 线程申请栈空间失败了

本地方法栈

[!quote] 本地方法栈 与虚拟机栈服务于 Java 方法不同,本地方法栈服务于本地方法

[!quote] 堆 堆 的唯一目的就是存放对象实例但是随着各种优化技术日渐发展,对象实例都分配在堆上也不是那么绝对】,在 JVM 启动时创建,被所有线程共享

  • 堆可以处于物理上不连续,逻辑上连续的内存空间中,但对于大对象【数组对象 ……】,出于实现简单、存储高效的考虑,很可能会要求连续的内存空间

方法区

[!quote] 方法区 方法区 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存 ……,也被所有线程共享

  • 方法区不需要连续的内存,还可扩展,甚至还可以选择不实现垃圾收集【这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,但一般来说回收效果比较难令人满意

[!quote] 类型信息 类型信息 是指已加载的类、接口、枚举和注解的结构信息类的字段、方法、父类、接口 ……】,它提供了对类的结构和行为的描述,JVM 通过类型信息来实例化对象、执行方法调用 ……


[!quote] 运行时常量池 运行时常量池 是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

JVM 对于 Class 文件每一部分的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java 虚拟机规范》并没有做任何细节的要求,不过一般来说,除了保存 Class 文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

  • OutOfMemoryError 方法区无法满足新的内存分配需求

垃圾收集器与内存分配策略

只要有强引用指向对象,该对象就不会被垃圾回收。只有当所有指向该对象的强引用都消失后,垃圾回收器才有可能回收它

即使使用了 System.gc(); 手动调用垃圾回收器,JVM 也可能决定不执行垃圾回收


当程序运行超过变量的作用域后,Java 会自动释放掉该变量的内存空间

在堆中分配的内存,由 JVM 自动垃圾回收器来管理


在 Java 中,有四种引用类型 :

  • 强引用 Strong Reference :new 创建出的对象就是强引用的,只要强引用还存在,GC 就不会回收这个对象,即使内存不足,JVM 也不会主动回收,而是抛出内存溢出异常
java
String str = new String("Hello");
  • 软引用 Soft Reference :软引用允许垃圾回收器在内存不足时回收对象,当 JVM 检测到内存不足时,首先会尝试回收软引用指向的对象,如果回收后仍然不足,才会抛出内存溢出异常
java
SoftReference<String> softRef = new SoftReference<>(new String("Hello"));
  • 弱引用 WeakReference :在下一个 GC 回收执行时,弱引用指向的对象就会被回收掉
java
WeakReference<String> weakRef = new WeakReference<>(new String("Hello"));
  • 虚引用 Phantom Reference :被虚引用引用的对象不能被直接访问,当其指向的对象被 GC 回收后,虚引用会被加入到引用队列,用于跟踪对象被垃圾回收的状态
java
PhantomReference<String> phantomRef = new PhantomReference<>(new String("Hello"), referenceQueue);

[!quote] 引用队列 ReferenceQueue : 对象被垃圾回收之后,关联的引用对象(例如软引用、弱引用、虚引用)会被放入到 ReferenceQueue 中

  • 追踪引用的状态:检测是否有引用进入队列,进而实现更灵活的资源管理,比如关闭文件流、清理缓存 ……

  • Java 8 - Parallel GC
  • Java 9-22 - G1 GC
  • Java 23-24 - ZGC

虚拟机性能监控

故障处理工具

调优案例分析