当前位置: 首页 > news >正文

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

 
概述

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


一、内存区域划分总览

      JVM内存分为线程私有线程共享区域:

  • 线程私有: 程序技术器、虚拟机栈、本地方法栈
  • 线程共享:堆、方法区            
二、运行时数据区详解
1. 程序计数器(Program Counter Register)
  • 作用:记录当前线程执行的字节码指令地址(分支、循环、跳转等依赖此区域),是一块很小的内存空间。如果正在执行的是本地方法,则程序技术器的值为空。

  • 特性

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

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

2. Java虚拟机栈(Java Virtual Machine Stack)
  • 核心功能:线程私有,用来存储栈帧(Frame),每个方法调用对应一个栈帧的入栈与出栈(方法开始-进栈,方法结束-出栈)。

  • 栈帧结构

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

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

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

    • 方法返回地址:方法正常退出或异常退出的地址,方法返回的是void类型,则不存储返回值。

  • 异常场景

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

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

🔍 参数调优

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

  • 特点:本地方法栈也是线程私有的,其生命周期与线程的生命周期同步。本地方法栈的实现方式可以由虚拟机实现者自行决定,有些虚拟机直接将本地方法栈和虚拟机栈合二为一。

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

4. Java堆(Java Heap)
  • 核心角色:所有对象实例和数组的存储区域,GC主战场,同时也是Java虚拟机管理的内存区域中最大的一块,是被所有线程共享的内存区域。

  • 分代设计

    • 新生代(Young Generation):Eden区 + 2个Survivor区(默认比例8:1:1),Minor GC触发条件:Eden区满。

    • 老年代(Old Generation):长期存活对象晋升至此,Major GC/Full GC触发条件:老年代空间不足。

  • 内存分配策略:

         对象优先分配在Eden区;

         大对象直接进入老年代(避免复制开销);

         动态对象年龄判定,Survivor区中相同年领对象总大小超过50%,晋升到老年代。

  • 异常场景

    • 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有哪些

        1)可达性分析(Reachability Analysis):如果一个对象无法通过任何引用链连接到 GC Roots,则判定为不可达,可以被回收;GC Roots 是垃圾回收的起点,包括虚拟机栈中引用的对象(局部变量、方法参数等)、方法区中静态变量引用的对象、方法区中常量引用的对象(如字符串常量池)、JNI(Native方法)引用的对象。      

// 示例:对象不可达的情况
public class Example {
    public static void main(String[] args) {
        Object obj = new Object();  // obj 是 GC Root(栈中局部变量)
        obj = null;                 // obj 断开引用,对象变为不可达
        // 此时 new Object() 可以被回收
    }
}

         2)引用类型的影响:

        强引用(Strong Reference):默认引用类型,只要存在强引用,对象不会被回收。

        软引用(Soft Reference):内存不足时可能被回收(适合缓存)。        

        弱引用(Weak Reference):无论内存是否充足,GC 运行时都可能被回收。

        虚引用(Phantom Reference):无法通过虚引用访问对象,仅用于跟踪对象被回收的状态。

// 弱引用示例:对象可能被快速回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc();  // 触发 GC
if (weakRef.get() == null) {
    System.out.println("对象已被回收");
}

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

  1. 内存位置:元空间在本地内存中分配;永久代在堆内存中分配。
  2. 内存管理:元空间动态扩展,按需分配,减少内存溢出风险,自动调整大小,仅需设置上限(可选);永久代固定大小,容易导致 OutOfMemoryError: PermGen space,需要手动调优 PermSize。
  3. 自动扩容和垃圾回收:
    特性永久代元空间
    扩容机制固定大小,需手动调整动态扩展(默认无上限)
    垃圾回收Full GC 时触发,效率低由元空间自身管理,与堆 GC 解耦
    内存释放难以释放已加载的类元数据类加载器死亡时,元数据可被回收
  4. 配置参数:
    永久代元空间
    -XX:PermSize-XX:MetaspaceSize(初始大小)
    -XX:MaxPermSize-XX:MaxMetaspaceSize(最大大小)
    (Java 8 已移除)-XX:MinMetaspaceFreeRatio 等
  5. 元空间的优点:1)避免内存溢出:本地内存通常比堆内存大得多,显著减少了 OutOfMemoryError 风险。2)性能提升:元空间的内存分配和回收效率更高,减少 Full GC 频率;3)简化调优:开发者不再需要关注 PermSize 的设置,除非需要限制元空间的最大大小。
  6. 总结
维度永久代(PermGen)元空间(Metaspace)
内存位置堆内存本地内存
内存管理固定大小,易溢出动态扩展,按需分配
垃圾回收依赖 Full GC,效率低独立回收,高效
调优复杂度需手动设置 PermSize默认无需调优
适用版本Java 7 及之前Java 8 及之后

元空间的设计解决了永久代的痛点,使得 JVM 在处理类元数据时更灵活、高效,尤其是在需要动态生成大量类(如 Spring、Hibernate 等框架)的场景下表现更优。

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

首先我们来分析下,这两个错误触发的条件:

错误类型触发条件
StackOverflowError单个线程的调用栈深度超过栈容量(例如无限递归、过深的嵌套方法调用)。
OutOfMemoryError创建线程时,线程的栈内存总需求超过 JVM 可分配的栈内存总量(例如创建大量线程)。

在来看下栈内存:

  • 栈内存(Stack Memory)

    • 每个线程独占一个栈,用于存储方法调用的栈帧(局部变量、操作数栈、方法返回地址等)。

    • 栈大小由 -Xss 参数指定(如 -Xss1m 表示每个线程栈大小为 1MB)。

    • 栈内存不足的两种场景

      1. 单线程栈溢出:单个线程的调用链过深(如递归未终止)。

      2. 多线程栈总内存耗尽:创建大量线程,每个线程的栈内存总和超过 JVM 可分配的总内存。

具体区别有:

维度StackOverflowErrorOutOfMemoryError(栈内存场景)
错误根源单个线程的调用栈过深线程数量过多,总栈内存不足
错误类型单线程内部错误多线程资源耗尽错误
触发代码示例无限递归:
void foo() { foo(); }
循环创建线程:
while(true) new Thread(...).start();
错误信息java.lang.StackOverflowErrorjava.lang.OutOfMemoryError: unable to create native thread
解决方案修复代码逻辑(如终止递归条件)减少线程数、调整 -Xss 或增加系统内存
是否可恢复不可恢复(需代码修复)

不可恢复(需调整资源或代码逻辑)

通过代码辅助理解:

StackOverflowError

public class StackOverflowExample {
    public static void main(String[] args) {
        recursiveCall(); // 无限递归
    }

    static void recursiveCall() {
        recursiveCall(); // 触发 StackOverflowError
    }
}
OutOfMemoryError(栈内存耗尽)
public class ThreadOOMExample {
    public static void main(String[] args) {
        while (true) {
            new Thread(() -> {
                try {
                    Thread.sleep(Long.MAX_VALUE); // 保持线程不退出
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

 最后我们可以通过参数调优来避免该错误发生

错误类型调优参数说明
StackOverflowError-Xss(如 -Xss2m增大单个线程的栈大小(需权衡内存开销)。
OutOfMemoryError-Xss(减小单个线程栈大小)
减少线程数
降低单个线程的栈内存占用,或限制线程总数。

总结

场景StackOverflowErrorOutOfMemoryError(栈内存)
根本原因单线程调用链过深多线程耗尽总栈内存
修复方向代码逻辑优化资源分配优化(线程数、栈大小)
JVM参数调整增大 -Xss减小 -Xss 或增加物理内存
  • StackOverflowError 是单线程的栈深度问题,直接由代码逻辑引发。

  • OutOfMemoryError(栈内存场景)是多线程资源竞争问题,需调整线程数量或栈内存配置。

相关文章:

  • Android输入事件传递流程系统源码级解析
  • DeepSeek基础之机器学习
  • Spring AutoWired与Resource区别?
  • OpenHarmony构建系统-GN与子系统、部件、模块理论与实践
  • 新学一个JavaScript 的 classList API
  • 如何使用 JavaScript 模拟 Docker 中的 UnionFS 技术:从容器到文件系统的映射
  • 小波变换分解低频和高频
  • 从零开始学 Rust:基本概念——变量、数据类型、函数、控制流
  • RT-Thread+STM32L475VET6实现定时器定时功能
  • Transformer LLaMA
  • 基于SpringBoot的建筑工程项目管理系统
  • element ui的select选择框
  • 简单易懂,解析Go语言中的Slice切片
  • 【JavaEE进阶】数据库连接池
  • BFS算法解决最短路径问题(典型算法思想)—— OJ例题算法解析思路
  • Opengl常用缓冲对象功能介绍及使用示例(C++实现)
  • Qt中QRadioButton的使用
  • 钉钉快捷免登录 通过浏览器打开第三方系统,
  • element ui 组件el-autocomplete的使用方法(输入建议,利用filter和include)
  • 碳基生物的悲歌-DeepSeek思考实现Linux动态库递归收集工具
  • 上财发布“AI+课程体系”,人工智能如何赋能财经教育?
  • 国家主席习近平在莫斯科出席红场阅兵式
  • 海关总署统计分析司司长:4月进出口增速较一季度加快4.3个百分点
  • 江苏省人社厅党组书记、厅长王斌接受审查调查
  • 第1现场 | 50多年来首次!印度举行大规模民防演习
  • 法治日报:商品明细是隐私,外卖员快递员不应知晓