Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • ☕ Understanding the JVM
    • Part 2 - 自动内存管理

      • Chapter 2 - Java 内存区域与内存溢出异常
      • Chapter 3.1-3.2 - 概述 && 对象已死
      • Chapter 3.3-3.4 - 垃圾收集算法 && HotSpot 的算法实现细节
      • Chapter 3.5 - 经典垃圾收集器
      • Chapter 3.6 - 低延迟垃圾收集器
    • Part 3 - 虚拟机执行子系统

      • Chapter 6 - 类文件结构
      • Chapter 7 - 虚拟机类加载机制
      • Chapter 8.1-8.2 - 运行时栈帧结构
      • Chapter 8.3 - 函数调用
      • Chapter 8.5 - 基于栈的字节码解释执行引擎
      • Chapter 9.2 - Tomcat: 正统的类加载器架构
    • Part 4 - 程序编译与代码优化

      • Chapter 10 - 前端编译与优化
      • Chapter 11.2 - 即时编译器
      • Chapter 11.3-11.4 - 提前编译器 && 编译器优化技术
    • Part 5 - 高效并发

      • Chapter 12.3 - Java 内存模型
      • Chapter 12.4-12.5 - Java 线程与协程
      • Chapter 13 - 线程安全与锁优化

Chapter 2 - Java 内存区域与内存溢出异常

Created by : Mr Dk.

2020 / 01 / 24 20:26 🧨🧧

Ningbo, Zhejiang, China


2.1 概述

在 JVM 的自动内存管理机制的帮助下,不再需要为每一个 new 操作分配对应的 delete/free 代码。

2.2 运行时数据区域

JVM 管理的内存被划分为不同区域。

2.2.1 程序计数器 (Program Counter Register)

当前线程所执行的字节码的行号指示器。各条线程之间的计数器互不影响,独立存储,是线程私有的内存。

2.2.2 Java 虚拟机栈 (Java Virtual Machine Stack)

由线程私有,生命周期与线程相同,描述的是 Java 函数执行的线程内存模型。

  • 当一个 Java 函数被调用时,JVM 会创建栈帧存储局部变量表等信息
  • 调用完毕,栈帧出栈

局部变量表中存放编译期可知的数据:

  • JVM 基本数据类型
  • 对象引用 (指向对象起始地址的引用指针)
  • returnAddress 类型

这些数据在局部变量表中以 局部变量槽 (slot) 来表示,局部变量槽在编译期间完成分配 (栈帧中需要的局部变量大小是可以提前确定的)。

2.2.3 本地方法栈 (Native Method Stacks)

与虚拟机栈发挥的作用类似。

  • 虚拟机栈为执行 Java 函数 (字节码) 服务
  • 本地方法栈为虚拟机用到的本地函数服务 (意思是非 Java 函数)

2.2.4 Java 堆 (Java Heap)

Java Heap 是 JVM 所管理的内存中最大的一块,是 所有线程共享 的一块内存区域,作用是存放所有的对象实例。Java Heap 是垃圾收集器管理的内存区域,因此也被称为 GC Heap。所有线程共享的 Java Heap 上可以划分出各线程私有的 分配缓冲区 (Thread Local Allocation Buffer, TLAB):

  • 提升对象分配时的效率
  • 将 Java Heap 细分的目的只是为了更好地回收内存,或更快地分配内存

Java Heap 在物理上可以是不连续的,但在逻辑上是连续的。Java Heap 可以被实现为是固定的,也可以是可扩展的。

2.2.5 方法区 (Method Area)

也是各个线程共享的内存区域,存储 JVM 加载的类型信息、常量、静态变量、JIT 编译器编译后的代码缓存。在实现上,不需要连续内存,大小可扩展,甚至还可以不实现垃圾收集。

2.2.6 运行时常量池 (Runtime Constant Pool)

是 方法区 的一部分。JVM 严格规定了 Class 文件每一部分的格式,其中。常量池表 (Constant Pool Table) 用于存放编译期生成的常量与符号引用。

2.2.7 直接内存

绕过 JVM 分配的内存,不位于 Java Heap 中,不属于运行时数据区。

2.3 HotSpot 虚拟机对象探秘

2.3.1 对象的创建

JVM 遇到 new 指令时,首先去检查指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、初始化。如果没有,则需要执行类加载。接下来,为新生对象分配内存:

  • 将一块固定大小的内存从堆上划分出来
  • 类加载后,对象所需要的内存大小可以确定

另外,考虑到分配内存时的线程安全问题,解决方案有:

  1. 对分配内存空间进行同步:JVM 使用 CAS + 失败重试保证更新的原子性
  2. 为每个线程在 JVM 中划分私有区域 (TLAB):只有本地缓冲用完,分配新的内存时才需要同步锁定

内存分配完毕后,JVM 将分配的内存空间初始化为 0,保证了对象的字段在 Java 代码中不需要初始化就能直接使用。JVM 对对象进行设置,将信息存放在对象的 Object Header 中,从 JVM 的视角来看,新的对象已经分配完成。但从 Java 代码的视角看,还需要执行对象的构造函数,这样才构造出了一个真正可用的对象。

2.3.2 对象的内存布局

对象在堆内存中的存储布局可以划分为三部分:

  • 对象头 (Header)
  • 实例数据 (Instant Data)
  • 对齐补充 (Padding)

对象头包含两部分信息:

  1. 存储对象自身的运行时数据 (Mark Word, 32-bit 或 64-bit)
    • HashCode
    • GC 年龄
    • 锁状态
    • 持有的锁等
  2. 类型指针,指向对象类型 metadata 的指针
    • JVM 通过这个类来确定该对象是哪个类的实例
    • 如果是数组,还额外需要记录数组的长度

实例数据部分存储了对象真正存储的有效信息,包括 Java 代码中定义的各种字段。HotSpot 虚拟机会有一个默认的字段分配顺序,使相同宽度的字段被存放到一起;满足这一条件的前提下,父类中定义的变量会出现在子类之前。由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址对齐 8B,由于对象头已被设计为固定长度,并对齐 8B,若实例数据不足 8B,将会被填充以对齐 8B。

2.3.3 对象的访问定位

Java 程序通过栈上的对象引用来操作堆上的具体对象。JVM 规范没有定义对象的具体访问方式,由 JVM 的实现决定:

  • 句柄访问
    • Java Heap 中划出一块内存作为句柄池
    • 对象引用存储的是句柄地址
    • 句柄中包含对象实例数据与类型数据各自的地址
  • 直接指针访问
    • 对象引用直接存储对象的地址

句柄访问的优势:在对象被移动 (GC 时很普遍) 时只会改变句柄中的实例数据地址,对象引用不会被修改。

直接指针访问的优势:

  • 速度更快,节省了一次指针定位的开销
  • HotSpot 使用这种方式进行对象访问
Edit this page on GitHub
Next
Chapter 3.1-3.2 - 概述 && 对象已死