《深入理解Java虚拟机》1 —— 第二章 Java内存区域与内存溢出异常

《深入理解Java虚拟机》第二章 —— Java内存区域与内存溢出异常

一、运行时数据区域

Java虚拟机在执行Java程序时会把它所管理的内存划分为若干个不同的数据区域,这些区域各有各的用途、以及创建和销毁的时间

img

接下来介绍Java虚拟机所管理的内存将会包括一下几个运行时数据内存

1.程序计数器

Program Counter Register:

  • 一块较小的内存空间,可以看作时当前线程所执行的字节码的行号指示器
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

在任意一个确定的时间,一个处理器都会只执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程要有一个独立的程序计数器,各条线程之前计数器互不影响、独立存储,我们称这类内存区域为“线程私有”的内存

2.Java虚拟机栈

Java Virtual Machine Stack:

  • 虚拟机栈描述的是Java方法执行的线程内存模型:
    • 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息
    • 每一个方法被调用直至执行完毕的过程,就对应一个战阵在虚拟机栈中从入栈到出栈的过程
  • 也是线程私有的,生命周期与线程相同

局部变量表:存放编译期可知的各种基本数据类型、对象引用。局部变量表的大小(变量槽的数量)在编译期已经确定,不会改变。

3.本地方法栈

Native Method Stacks 与虚拟机栈发挥的作用很类似,区别:

  • 虚拟机栈为虚拟机执行Java方法(字节码)服务
  • 本地方法栈为虚拟机使用到的本地(native)方法服务

4.Java堆

Java Heap:

  • Java堆是虚拟机所管理的内存最大的一块,几乎所有的对象实例都在这里分配内存
  • Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一目的是存放对象实例
  • 也是垃圾收集器管理的主要区域

5.方法区

Method Area(别名:非堆 Non-Heap):

  • 同样是各个线程共享的区域
  • 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • 永久代的概念再jdk8被完全废弃,改用了与JRockit、J9一样的再本地内存中实现的元数据(Meta-space)来代替

6.运行时常量池

Runtime Constant Pool:

  • 方法区的一部分
  • class文件除了类的版本、字段、方法等信息外,还有一项信息是常量池表(Constant Pool Table)用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池
  • 运行期间也可以将新的变量放入池中,如:String类的intern()方法

7.直接内存

Direct Memory:并不是虚拟机运行时数据区的一部分,由于这部分内存被频繁调用且可能导致Out Of Memory Error,放到这里一起讲解

  • jdk1.4中增加了NIO,可以分配堆外内存(系统内存替代用户内存),提高了性能。

二、HotSpot虚拟机对象探秘

深入探讨一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程

1.对象的创建

image-20220720092537649

对象所需的内存大小在类加载之后即可以完全确定。

分配内存方法:

  • 指针碰撞(Bump The Pointer):如果java堆带有空间压缩整理(Compact)能力,那么java堆就是规整的,即被使用过的内存放在一边,未使用的内存放在另一边,中间放着一个指针作为分界点的指示器,内存分配就是向空闲空间的方向移动一段与对象相等的距离。简单、高效。
  • 空闲列表(Free List):如果java堆不带有空间压缩整理(Compact)能力,Java堆中的空闲内存和已使用的内存交织在一起。虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配时找到一块可用的足够大的内存分配,并更新列表上的记录。

线程安全问题:

对象在虚拟机中创建时很频繁的行为,仅仅修改一个指针指向的位置,在并发情况下也不是线程安全的。

可能在给A分配内存时,指针还没来得及修改,对象B就使用了原来的指针来分配内存。

两种解决方法:

  • 对分配内存空间的动作进行同步处理,采用CAS+失败重试的方法保证更新操作的原子性
  • 每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)。只有当本地缓冲区用完了,分配新的缓冲区才需要同步锁定。

虚拟机是否使用TLAB,可以通过 -XX:+/-UseTLAB 参数来设定

2.对象的内存布局

在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头:

  • 自身运行时数据:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁等。这部分数据被官方称为“Mark Word”。Mark Word 被设计成一个有着动态定义的数据结构,以便在极小的空间内存存储尽量多的数据,根据对象的状态复用自己的存储空间。image-20220720100212616
  • 类型指针:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个实例。

**实例数据:**相同宽度的字段会分配到一起存放,父类定义的变量会出现在子类之前

**对齐填充:**不是必然存在的,仅仅起占位符的作用:对于对象实例数据部分没有对齐的部分,用对齐填充来补齐

3.对象的访问定位

Java程序会通过栈上的reference数据来操作堆上的具体对象。reference类型被定义是一个指向对象的引用,但这个引用时通过什么方式、访问到堆中对象的具体位置呢?

对象访问方式也是由虚拟机实现而定的,主流的访问方式有 使用句柄 和 直接指针:

  • 使用句柄访问:Java堆可能划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自具体的地址信息。img
  • 直接指针访问:reference中存储的直接就是对象地址。img

各有优势。使用句柄最大好处就是在对象被移动的时候,改变的句柄中的示例数据指针,而reference不需要改变。直接指针的最大好处就是速度更快,节省了一次指针定位的时间开销,由于对象访问极为频繁,这类开销积少成多也是一项极为可观的执行成本。

HotSpot 主要使用的是直接指针访问方式。

三、实战:Out Of Memory异常

以下称作OOM。除了程序计数器外,其他的几个运行时区域包括直接内存都有OOM的可能。

1.Java堆溢出

Java堆用于存储对象实例,随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

虚拟机启动参数配置:

image-20220720104154709

image-20220720104208032

2.虚拟机栈和本地方法栈溢出

由于HotSpot并不区分虚拟机栈和本地方法栈,因此-Xoss(设置本地方法栈大小)虽然存在但不起作用,只能由-Xss来设定栈容量。

关于虚拟机栈和本地方法栈有两种异常:

  • 如果线程请求的栈的深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常

HotSpot不支持栈的动态扩展,所以除非在创建线程申请内存时就因为无法获得足够的内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError。

限制栈内存容量:

image-20220720110845870

image-20220720110957809

还有一种测试方法是多占局部变量表空间,由于太过麻烦在这里不再示范。

当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。但如果在允许动态扩展栈容量大小的虚拟机上,抛出的异常就会是OutOfMemoryError异常了。

在不断建立线程的情况下,HotSpot也是可以产生OutOfMemoryError异常的,,在这种情况下,给每个线程分配的内存越大,越容易产生内存溢出异常。

3.方法区和运行时常量池异常

运行时常量池实时方法区的一部分,一起测试。

永久代在JDK8中已经完全被元空间代替,这里在JDK8的环境下通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,可以间接限制其中常量池的容量。

提示已经被移除了。

image-20220720112719815

如果在JDK6中:

img

img

img

4.本机直接内存溢出

直接内存可以由-XX:MaxDirectMemorySize来设定,如果不指定,默认和Java堆最大值一致。

image-20220720114141851

image-20220720114151612

由直接内存导致的内存溢出,明显特征是Heap Dump文件不会看见有什么明显的异常情况,如果Dump文件很小,而程序中又间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方法的原因了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

HotRabbit.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值