当前位置: 首页 > 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(栈内存场景)是多线程资源竞争问题,需调整线程数量或栈内存配置。

 

相关文章:

  • C++/JavaScript ⭐算法OJ⭐下一个排列
  • Qt中QRadioButton的样式设置
  • ChatGPT平替自由!DeepSeek-R1私有化部署全景攻略
  • 八股文实战之JUC:静态方法的锁和普通方法的锁
  • 进程间通信中间件---ZeroMQ
  • Verilog define预处理命令
  • AI 大模型:点亮乡村振兴的新曙光
  • AWS S3深度解析:十大核心应用场景与高可用架构设计实践
  • sh脚本把服务器B,服务器C目录的文件下载到服务器A目录,添加开机自启动并且一小时执行一次脚本
  • 蓝桥与力扣刷题(蓝桥 交换瓶子)
  • ctfshow——phps源码泄露
  • Java APM如何Profiling:使用火焰图多维度分析应用性能瓶颈
  • Java Idea配置问题
  • 【多语言生态篇三】【DeepSeek×Go:高并发推理服务设计】
  • 2502C++,C++继承的多态性
  • 【误差理论与可靠性】第二章 可靠性的基本概念和参数体系
  • 25林业研究生复试面试问题汇总 林业专业知识问题很全! 林业复试全流程攻略 林业考研复试真题汇总
  • 【JavaWeb12】数据交换与异步请求:JSON与Ajax的绝妙搭配是否塑造了Web的交互革命?
  • 京东外卖骑手全部缴纳五险一金
  • 1.vue使用vite构建初始化项目
  • 网页添加兼容性站点/汕头seo网站推广
  • 上饶市建设局有什么网站/满十八岁可以申请abc认证吗
  • 网页制作专业名词/seo类目链接优化
  • 镇江网站推广/黑帽seo技术
  • 鹰潭律师网站建设/中国seo网站
  • 网站开发与应用 大作业作业/高级seo是什么职位