《深入理解Java虚拟机》读书笔记(2)——Java内存区域与OOM异常

JVM内存的划分,以及介绍OOM异常~

本章的内容总览,个人认为比较重要的是明白JVM的分区机制,也就是有哪些分区,各个分区是什么作用

image-20200523223628059

运行时数据区域

JVM在运行时,会把管理的内存划分成几个区域,各自有各自的用途,创建与销毁时间也各异。总体上分成5个区,架构图如下(图片源自《深入理解Java虚拟机》,以下简称《JVM》)

  1. 方法区
  2. 虚拟机栈
  3. 本地方法栈
  4. 程序计数器
image-20200523211357650

JVM内存主要被分为五大区域,但是从思维导图上可见本书介绍一共有七个部分,运行时常量池和直接内存未被提及,是因为有一些区域存在包含关系,而《JVM》这本书中画出的模型抽象级别比较高,下面是源自一个博客的插图(侵删),细度稍微高一些,能更全面地展示这七个部分。

img

从第一张图可见,区域是分是否线程共享的,所以以这个标准,来分开介绍这几大区域的作用。

线程私有

1. 程序计数器

  • 功能:一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,是程序控制流程的指示器。
  • 线程:每条线程都有一个独立的程序计数器,互不影响,独立存储。
  • 异常:唯一一个没有规定任何OOM情况的区域。
  • 备注:如果正在执行本地方法,则计数器值置空

2. 虚拟机栈

  • 功能:每个方法被执行的时候,JVM都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口信息等。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在JVM栈中从入栈到出栈的过程。JVM谈论“栈”,通常就是指虚拟机栈,或更多情况下指的是虚拟机栈中的局部变量表部分。
    • 局部变量表:基本数据类型、对象引用和returnAddress(指向了一条字节码指令的地址)
  • 线程:生命周期与线程相同
  • 异常:SOF(线程请求的栈深度大于虚拟机所允许的深度)和OOM(栈无法申请到足够的内存)

3. 本地方法栈

  • 功能:作用与虚拟机栈类似,区别在于本地方法栈是为虚拟机使用的本地方法服务
  • 备注:HotSpot将这两者栈合二为一

线程共享

4. 堆

  • 功能:唯一目的就是存放对象实例,几乎所有对象实例都在此分配内存。是GC管理的内存区域,该区域又可以细分成“新生代”“老年代”等,是为了分代收集。堆可以处于物理上不连续的内存空间,但逻辑上应被视为连续的。
  • 线程:虚拟机启动时创建
  • 异常:堆中没有内存完成实例分配,且堆也无法再扩展时,会抛出OOM异常

5. 方法区

  • 功能:存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • 线程:同堆
  • 异常:同堆

6. 运行时常量池

  • 功能:是方法区的一部分,用于存放编译期间生成的各种字面量符号引用
  • 异常:常量池无法再申请到内存时会抛出OOM异常

7. 直接内存

该区域不是虚拟机运行时数据区的一部分,但是这部分内存被频繁使用,也可能导致OOM异常

对象初始化

Java程序中new一个对象时,Hotspot虚拟机创建对象包括如下5个步骤:

image-20200523223519831

类型检查

检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被夹在、解析和初始化锅。如果没有,则必须先执行相应的类加载过程。

对象分配内存

对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。

分配方法:

  • 指针碰撞
    假设Java堆中内存是绝对规整的,所有被使用过的内存都在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存仅仅是把那个指针向空闲方向移动一段与对象大小相等的距离。

  • 空闲列表

    如果Java堆中的内存不是规整的,已被使用的内存和空闲的内存相互交错在一起,指针碰撞失效,虚拟机必须维护一个列表,记录上哪些内存块可以使用。分配的时候从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录。

选择哪种方式取决于所采用的GC是否带有空间压缩整理的能力

  • Serial、ParNew等带压缩整理过程的GC,采用的分配算法时指针碰撞,简单高效
  • CMS这种基于清除算法的GC,理论上采用空闲列表算法分配内存

线程安全问题:

并发情况下,仅仅修改一个指针的指向,也不是线程安全的。解决方法有如下两种。

  1. 对分配内存空间的动作进行同步处理。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
  2. 把内存分配动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,称作本地线程分配缓冲(TLAB),哪个线程要分配内存,就在该线程TLAB中分配,只有TLAB用完时,分配新的缓存区时才需要同步锁定。是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定,JVM默认开启。

初始化为零值

将分配到的内存空间初始化为零值,如果使用了TLAB,则可提前至TLAB分配时进行。保证了对象的实力字段在Java代码中可以不显示赋初始值就可使用,是程序能访问到这些字段的数据类型所对应的零值。

对象头设置

设置这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。

执行<init>

把对象按照程序中设定的初始赋值进行初始化

0%