学习笔记01——《深入理解Java虚拟机(第四版)》第二章

发布于:2025-02-23 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、概述

理解JVM内存管理的核心设计思想,掌握内存区域的划分原理、对象生命周期与内存溢出(OOM)的根本原因及排查方法。第二章主要是围绕Java虚拟机的运行时数据区展开,详细介绍了Java虚拟机在运行Java程序时,如何分配和管理内存空间。


二、运行时数据区详解
1. 程序计数器(Program Counter Register)
  • 作用:记录当前线程执行的字节码指令地址(分支、循环、跳转等依赖此区域),是一块很小的内存空间。

  • 特性

    • 线程私有,生命周期与线程绑定。

    • 唯一无OOM的区域(无垃圾回收,无内存溢出)。

2. Java虚拟机栈(Java Virtual Machine Stack)
  • 核心功能:存储栈帧(Frame),每个方法调用对应一个栈帧的入栈与出栈。

  • 栈帧结构

    • 局部变量表:存放方法参数和局部变量(基本类型、对象引用)。

    • 操作数栈:执行字节码指令的工作区(如加减乘除、方法调用)。

    • 动态链接:指向运行时常量池的方法引用。

    • 方法返回地址:方法正常退出或异常退出的地址。

  • 异常场景

    • StackOverflowError:线程请求栈深度超过虚拟机限制(如无限递归)。

    • OutOfMemoryError:虚拟机栈动态扩展时无法申请足够内存(如大量线程并发)。

🔍 参数调优

-Xss1m  # 设置线程栈大小为1MB(默认值依赖操作系统,Linux通常为1MB)  
3. 本地方法栈(Native Method Stack)
  • 功能:为Native方法(如C/C++实现的方法)服务。

  • 异常:与Java虚拟机栈类似,可能抛出StackOverflowError和OOM。

4. Java堆(Java Heap)
  • 核心角色:所有对象实例和数组的存储区域,GC主战场。

  • 分代设计

    • 新生代(Young Generation):Eden区 + 2个Survivor区(默认比例8:1:1)。

    • 老年代(Old Generation):长期存活对象晋升至此。

  • 异常场景

    • OutOfMemoryError: Java heap space:堆内存不足(内存泄漏或堆容量不足)。

🔍 参数调优

-Xms4g -Xmx4g  # 初始堆=最大堆(避免动态扩容引发性能波动)  
-XX:NewRatio=2  # 老年代与新生代比例(2表示老年代:新生代=2:1)  
5. 方法区(Method Area)
  • 存储内容:类元信息(类名、字段、方法)、运行时常量池、静态变量、JIT编译后的代码。

  • 演进历史

    • JDK7及之前:永久代(PermGen),易引发OOM。

    • JDK8+:元空间(Metaspace),使用本地内存,动态扩展。

  • 异常场景

    • OutOfMemoryError: Metaspace:类加载过多(如动态生成类、反射滥用)。

🔍 参数调优

-XX:MetaspaceSize=256m      # 初始元空间大小  
-XX:MaxMetaspaceSize=512m    # 最大元空间大小(默认无限制,依赖系统内存)  
6. 运行时常量池(Runtime Constant Pool)
  • 功能:存放编译期生成的字面量与符号引用(如字符串常量)。

  • 与字符串常量池关系:JDK7+将字符串常量池移至堆中,避免永久代OOM。

7. 直接内存(Direct Memory)
  • 定义:通过ByteBuffer.allocateDirect()分配的堆外内存,不受JVM堆限制。

  • 异常场景

    • OutOfMemoryError:直接内存超过-XX:MaxDirectMemorySize限制。


三、对象生命周期与内存溢出实战
1. 对象创建流程
  1. 类加载检查:检查类是否已被加载、解析和初始化。

  2. 分配内存

    • 指针碰撞(堆内存规整时使用,如Serial、ParNew)。

    • 空闲列表(堆内存不规整时使用,如CMS)。

  3. 初始化零值:对象字段赋默认值(如int=0,boolean=false)。

  4. 设置对象头:存储对象哈希码、GC分代年龄、锁状态等元数据。

  5. 执行<init>方法:构造函数初始化(Java代码层面)。

2. 内存溢出(OOM)场景与排查
OOM类型 原因分析 排查工具
Java heap space 对象数量超过堆容量或内存泄漏 MAT、JProfile分析堆转储文件
Metaspace 动态生成类过多(如CGLib代理) JVM参数限制,检查类加载器
Unable to create thread 线程数过多,栈总内存超过系统限制 jstack分析线程栈,减少线程数
Direct buffer memory 直接内存分配过多(NIO使用不当) -XX:MaxDirectMemorySize调整

🔧 实战案例:模拟堆内存溢出

public class HeapOOM {  
    public static void main(String[] args) {  
        List<byte[]> list = new ArrayList<>();  
        while (true) {  
            list.add(new byte[1024 * 1024]);  // 持续分配1MB数组  
        }  
    }  
}  

输出

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  

排查步骤

  1. 添加JVM参数:-Xmx20m -XX:+HeapDumpOnOutOfMemoryError(生成堆转储文件)。

  2. 使用MAT(Memory Analyzer Tool)分析java_pid<pid>.hprof文件,查找大对象或GC Roots引用链。


四、本章核心总结
  1. 内存区域划分:线程私有(栈、程序计数器)与线程共享(堆、方法区)。

  2. 对象生命周期:从分配到回收,依赖GC算法与内存区域特性。

  3. OOM本质:内存区域容量不足或对象无法回收(内存泄漏)。

  4. 调优核心:根据应用类型(高吞吐、低延迟)选择GC算法,合理设置内存参数。


五、高频面试题
  1. JVM哪些区域是线程共享的?哪些是线程私有的?

    • 共享:堆、方法区。

    • 私有:虚拟机栈、本地方法栈、程序计数器。

  2. 如何判断一个对象是否可以回收?可达性分析中的GC Roots有哪些?

    • GC Roots包括:虚拟机栈中的局部变量、静态变量、JNI引用等。

  3. 元空间(Metaspace)与永久代(PermGen)的区别是什么?

    • 元空间使用本地内存,动态扩展;永久代受JVM堆大小限制。

  4. StackOverflowError和OutOfMemoryError在栈内存中的区别?

    • StackOverflowError:栈深度超过限制(代码问题)。

    • OutOfMemoryError:栈扩展时内存不足(系统资源问题)。