第二章 Java 内存区域与内存溢出

前言

文中内容多数出自《Java虚拟机规范 SE 7/8/9》中文版 和《深入理解Java虚拟机-JVM高级特性与最佳实践第2版》周志明 著, 本人英语差劲,看不懂专业的英文文档,只能学习别人翻译和著作好的书籍,徒呼奈何(我也很无奈)。

《Java虚拟机规范》并非某一款虚拟机实现的说明书,它是一份保证各个公司的Java虚拟机实现具备统一外部接口的契约文档,书中的概念和细节描述曾经与Sun的早期虚拟机的实现高度吻合,但是随着技术的发展,高性能虚拟机真正的细节实现方式已经渐渐与虚拟机规范所描述的内容产生了越来越大的差距。原作者也在书中不同地方反复强调过:虚拟机规范中所提及的“Java虚拟机”皆为虚拟机的概念模型而非具体实现。实现只要保证与概念模型最终等效即可,而具体实现的方式无需受概念模型束缚。因此通过虚拟机规范去分析程序的执行语义问题(虚拟机会做什么)时,但分析程序的执行行为问题(虚拟机是怎样做的、性能如何)则意义不大,如需对具体虚拟机实现进行调优、性能分析等,我推荐在本书基础上继续阅读《Java Performance》和《Oracle JRockit The Definitive Guide》等书。

概述

对于从事C,C++程序开发的人员来说,在内存管理方面可以通过malloc,free来对内存进行分配和回收。

对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每个对象new操作去写分配内存空间的代码,不容易出现内存泄露和内存溢出的问题。但是当你的程序出现一些因为内存溢出和内存泄露的问题时,如果你不了解JVM是怎么管理分配,回收内存的,那么排查错误将是一项艰难的工作。

运行时数据区域

根据Java虚拟机规范的规定,Java虚拟机所管理的内存将会包括以下几个运行时的数据区域
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z41YZaZL-1683874266335)(https://note.youdao.com/yws/res/1784/CF79346432654C8B94F1D41DF3D87318)]

PS: 《Java SE 7 / 8 / 9 虚拟机规范》内容基本一致,变化最大的就是方法区(永久代:HotSpot将分代垃圾收集算法扩展至方法区,对于其他虚拟机(BEA JRockit, IBM J9)来说不存在永久代的概念)。

程序计数器(PC(Program Counter)寄存器)

程序计数器是一块较小的内存空间,可以将它看做是当前线程所执行的字节码的行号指示器。

Java虚拟机可以支持多条线程同时执行,每一条Java虚拟机线程都有自己的程序计数器。在任意时刻,一条Java虚拟机线程只会执行一个方法的代码(为了多个线程在切换时能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程互不影响,独立存储,我们称这类内存区域为“”线程私有“的内存。),这个正在被线程执行的方法称为该线程的当前方法。如果这个方法不是native的,那PC寄存器就保存Java虚拟机正在执行的字节码指令的地址,如果该方法是native的,那PC寄存器的值是undefined。PC寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值。

程序计数器的内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。(估计是因为所需要的内存实在太小,虚拟机出现的时候操作系统的内存技术就已经以MB计算了,所以以当时的情况来说,程序计数器不可能出现内存溢出的情况,一家之言,猜的,哈哈!)

Java虚拟机栈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMEyVwFT-1683874266336)(https://note.youdao.com/yws/res/1788/E2F29CE1A26C45BBB5FACC91A42DC243)]

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,生命周期与线程同生同灭。

Java虚拟机栈可能发生如下异常情况:

如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。

如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

① 在Java虚拟机规范第一版之中,Java虚拟机栈也被称为“Java栈”。

② 译者注:请读者注意避免混淆Stack、Heap和Java (VM)Stack、Java Heap的概念,Java虚拟机的实现本身是由其他语言编写的应用程序,在Java语言程序的角度上看分配在Java Stack中的数据,而在实现虚拟机的程序角度上看则可以是分配在Heap之中。

本地方法栈

本地方法栈为虚拟机使用到的Native方法(通常称之为“C Stacks”,例如调用操心系统的函数)服务。虚拟机并没有强制规定对本地方法栈中方法使用的语言、使用方式、数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

本地方法栈可能发生如下异常情况:

如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。

如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

Java堆

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存(Java虚拟机规范:在Java虚拟机中,堆(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域)。

Java堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存不需要保证是连续的。

Java堆可能发生如下异常情况:

如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

“永久代”: 很多人把方法区叫作永久代的是因为HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说是用永久代来实现防区而已。而对于其他虚拟机(BEA JRockit , IBM J9等)来说是不存在永久代的概念的。

如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,并不是一个好主意,因为这样更容易遇到内存溢出问题(永久代有-XX:MaxPermSize的上限,JRockit ,和J9只要没有碰触到进程可用的内存上限就不会出问题)。在Java SE 8 / 1.8 Editition 中已经将永久代改为元空间(Metaspace)已经和JRockit ,J9基本一样。只要没有碰触到进程可用的内存上限就不会出问题。

Java堆可能发生如下异常情况:

如果实际所需的堆超过了自动内存管理系统能提供的最大容量(Java SE 7 / 1.7 Edition),那Java虚拟机将会抛出一个OutOfMemoryError异常。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。

运行时常量池是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表(Symbol Table)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。

每一个运行时常量池都分配在Java虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。

在创建类和接口的运行时常量池时,可能会发生如下异常情况:

当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值(Java SE 7 / 1.7 Edition),那Java虚拟机将会抛出一个OutOfMemoryError异常。

直接内存

  • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分区域也经常频繁的使用,而且也可能导致OutOfMemoryError异常出现。

  • 直接内存是通过存储在Java堆中的DirectByteBuffer对象作为引用,来操作因为调用Native函数库的而直接分配的堆外内存。这样能在一些场景显著提高性能,因为避免了Java堆和Native堆中来回复制数据。

OutOfMemoryError异常实战

Java堆溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Root到对象都可达来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。

为了尽快得出结果,设置运行时参数堆大小为20MB,修改配置,给参数VM options配置如下:

-Xms20m-Xms20m-XX:+HeapDumpOnOutOfMemoryError

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52AHvcCB-1683874266337)(https://note.youdao.com/yws/res/1807/1EF9D3981F4E43C49E58FF40AAAC3D9D)]

最后Apply->OK

代码:

public class Animal{
private String name;
private Integer age;
/**
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

*/
public static void main(String[] args) {
    List<Animal> list = new ArrayList<>();
    while (true){
        list.add(new Animal());
    }
}

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SajREdul-1683874266338)(https://note.youdao.com/yws/res/1811/F50DED1E97FF45A98DB5F2A8F7AA75F8)]

由于Hotspot虚拟机中并不区分虚拟机栈和本地栈,因此,对于Hotspot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。

关于虚拟机栈和本地方法栈,Java虚拟机规范中描述了的两种异常:

  1. 如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。

  2. 如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那Java虚拟机将会抛出一个OutOfMemoryError异常

实验一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s2K2ygMm-1683874266339)(https://note.youdao.com/yws/res/1819/59766AD378904F0F9BF2906B57D5F473)]

实验二

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MTyXRvzm-1683874266341)(https://note.youdao.com/yws/res/1821/96DDF9CF0B614C59BD1D4B0F9DD7AA95)]

栈容量从小到大设置,堆栈深度相应变多

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uwEHhQup-1683874266342)(https://note.youdao.com/yws/res/1823/A9C21B96D2294E999EA86C48806A8852)]
抛出异常java.lang.StackOverflowError ,异常出现时输出的堆栈深度相应缩小。

方法区和运行时常量池溢出

JDK1.7版本永久代内存大小设置

-XX:PermSize

-XX:MaxPermSize

PS: JDK1.7请按照1.8的实验方法自行实验

JDK1.8元空间内存大小设置

-XX:MetaspaceSize

-XX:MaxMetaspaceSize

设置最大元空间大小为 10M

实验一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h84cKllI-1683874266343)(https://note.youdao.com/yws/res/1825/166D2944A02644F6B42BB7EFF12D20EC)]

实验二

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MSZvWXFb-1683874266344)(https://note.youdao.com/yws/res/1827/52181B283AE54C4DA3D14F276A34F132)]

以上两个实验对比发现,当使用CFLIB加载很多类的时候,元空间会因为超出设置的大小而报错:Metaspace。当只加载大量String类型的常量时,却不会报错Metaspace,而是报错Java heap space,由此可见,在JDK1.8中,将常量池已经不在方法区(元空间)了。

本机直接内存溢出

DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不设置,则默认与Java堆对大值(-Xmx指定)一样。

实验一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AVvDPBnh-1683874266345)(https://note.youdao.com/yws/res/1831/4FDEE4350C654345A637B488A8AB8770)]

DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看到明显的异常。如果发现OOM之后Dump文件很小,而程序中又直接或者间接的使用了NIO,那就很有可能是这方面的原因。

PS:

零拷贝的原理 https://www.jianshu.com/p/497e7640b57c

java虚拟机规范文档https://www.jianshu.com/p/79d58629e03f

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唐·王惜之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值