类加载时机与初始化时机的6种场景

引言 为什么要学习这个主题? 在前面的文章中,我们学习了类加载的完整流程和自定义类加载器。但你是否想过: 类什么时候被加载?什么时候被初始化? 为什么有时候静态代码块会执行,有时候不会? ClassName.class会触发类初始化吗? 理解类的加载和初始化时机,能帮助我们: 优化程序启动速度(延迟加载) 避免循环依赖问题 正确使用单例模式 理解静态代码块的执行时机 你将学到什么? ✅ 类加载的时机(何时加载) ✅ 类初始化的6种主动引用场景 ✅ 被动引用不会触发初始化 ✅ 接口的初始化规则 ✅ 类初始化的常见陷阱 ✅ 如何控制类的加载时机 一、类加载的时机 1.1 何时加载类? JVM规范没有强制规定类的加载时机,由JVM实现自行决定。 通常在以下情况加载: 第一次主动使用类时 预加载(可选,JVM优化) 1.2 何时必须初始化? JVM规范严格规定了6种必须初始化类的场景(主动引用)。 二、6种主动引用场景(必须初始化) 场景1:使用new创建对象 代码: public class NewDemo { public static void main(String[] args) { User user = new User(); // ← 触发User类初始化 } } class User { static { System.out.println("User类初始化"); } } 输出: User类初始化 说明: new关键字会触发类的初始化 执行静态代码块 执行静态变量赋值 场景2:访问类的静态字段(除final常量) 代码: public class StaticFieldDemo { public static void main(String[] args) { System.out.println(Config.name); // ← 触发Config类初始化 } } class Config { static { System.out.println("Config类初始化"); } public static String name = "Alice"; } 输出: ...

2025-11-20 · maneng

JVM内存结构全景:5大区域详解

引言:为什么要理解JVM内存结构? 当你遇到以下问题时,是否感到困惑: StackOverflowError 和 OutOfMemoryError 有什么区别? 为什么有的对象存储在堆中,有的却在栈中? 静态变量存储在哪里?方法代码又存储在哪里? 为什么多线程之间可以共享对象,却不能共享局部变量? 这些问题的答案,都藏在 JVM内存结构 中。 理解JVM内存结构是深入学习JVM的基础: 性能调优:知道内存如何分配,才能优化内存参数 问题排查:90%的内存问题与内存区域的使用不当相关 代码优化:理解对象存储位置,才能写出高效代码 本文将带你建立JVM内存结构的全景认知,为后续深入学习每个区域打下基础。 JVM内存结构全景图 运行时数据区概览 当Java程序运行时,JVM会将内存划分为不同的数据区域,这些区域统称为 运行时数据区(Runtime Data Area)。 根据《Java虚拟机规范》,JVM运行时数据区包括以下5大区域: ┌─────────────────────────────────────────────────────────┐ │ JVM 运行时数据区 │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ 程序计数器 │ │ 程序计数器 │ 线程私有 │ │ │ (PC Register) │ │ (PC Register) │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ 虚拟机栈 │ │ 虚拟机栈 │ 线程私有 │ │ │ (VM Stack) │ │ (VM Stack) │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ 本地方法栈 │ │ 本地方法栈 │ 线程私有 │ │ │ (Native Stack) │ │ (Native Stack) │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────┐ │ │ │ 堆内存 │ 线程共享 │ │ │ (Heap) │ │ │ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ 新生代 (Young) │ │ │ │ │ │ Eden + S0 + S1 │ │ │ │ │ └─────────────────┘ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ 老年代 (Old) │ │ │ │ │ └─────────────────┘ │ │ │ └───────────────────────┘ │ │ │ │ ┌───────────────────────┐ │ │ │ 方法区 │ 线程共享 │ │ │ (Method Area) │ │ │ │ │ │ │ │ · 类信息 │ │ │ │ · 常量池 │ │ │ │ · 静态变量 │ │ │ │ · JIT编译后的代码 │ │ │ └───────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ ┌───────────────────────┐ │ 直接内存 │ (堆外内存) │ (Direct Memory) │ │ │ │ · NIO Buffer │ │ · Netty Zero-Copy │ └───────────────────────┘ 关键特征: ...

2025-11-20 · maneng

程序计数器:最小的内存区域

引言:一个被忽视的关键角色 在JVM的5大内存区域中,程序计数器(Program Counter Register) 是最小、最简单的一个,但却承担着至关重要的使命: 它记录着当前线程正在执行的字节码指令地址 它保证了多线程环境下每个线程能够独立执行 它是JVM中 唯一不会发生OutOfMemoryError 的内存区域 如果没有程序计数器,多线程程序将无法正常运行。本文将深入理解这个小而美的内存区域。 什么是程序计数器? 核心定义 程序计数器(PC Register) 是一块较小的内存空间,可以看作是 当前线程所执行字节码的行号指示器。 在JVM的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 第一性原理:为什么需要程序计数器? 从计算机组成原理的角度看: CPU执行指令需要知道 “下一条指令在哪里” 传统CPU使用 PC寄存器(Program Counter) 存储下一条指令地址 JVM作为虚拟机,也需要类似的机制来 跟踪字节码执行位置 核心作用: 记录执行位置:存储当前线程正在执行的字节码指令地址 支持线程切换:多线程环境下,每个线程有独立的程序计数器,保证线程切换后能恢复到正确位置 支持分支跳转:循环、异常处理、方法返回等都依赖程序计数器 程序计数器的工作原理 字节码执行示例 让我们通过一个简单的例子理解程序计数器的工作: public class PCDemo { public static void main(String[] args) { int a = 1; int b = 2; int c = a + b; System.out.println(c); } } 编译后的字节码(使用 javap -c PCDemo.class 查看): public static void main(java.lang.String[]); Code: 0: iconst_1 // 将整数1压入操作数栈 1: istore_1 // 将栈顶值存储到局部变量表slot 1(变量a) 2: iconst_2 // 将整数2压入操作数栈 3: istore_2 // 将栈顶值存储到局部变量表slot 2(变量b) 4: iload_1 // 从局部变量表slot 1加载变量a 5: iload_2 // 从局部变量表slot 2加载变量b 6: iadd // 将栈顶两个int值相加 7: istore_3 // 将结果存储到局部变量表slot 3(变量c) 8: getstatic #2 // 获取静态字段 System.out 11: iload_3 // 加载变量c 12: invokevirtual #3 // 调用println方法 15: return // 方法返回 程序计数器的变化过程: ...

2025-11-20 · maneng

虚拟机栈:方法执行的内存模型

引言:方法调用的幕后英雄 当你调用一个Java方法时,JVM在背后做了什么? public class Demo { public static void main(String[] args) { int result = add(1, 2); System.out.println(result); } public static int add(int a, int b) { return a + b; } } 这段简单的代码背后,涉及到: 方法参数如何传递? 局部变量存储在哪里? 方法调用时发生了什么? 方法返回后内存如何释放? 这些问题的答案,都藏在 虚拟机栈(Java Virtual Machine Stack) 中。 理解虚拟机栈是理解Java方法调用机制的基础,也是排查 StackOverflowError 的关键。 什么是虚拟机栈? 核心定义 虚拟机栈(VM Stack) 是描述Java方法执行的内存模型: 每个方法执行时,JVM会创建一个 栈帧(Stack Frame) 栈帧用于存储 局部变量、操作数、方法返回地址 等信息 方法调用时入栈,方法返回时出栈 第一性原理:为什么需要栈结构? 栈(Stack) 是一种 后进先出(LIFO,Last In First Out) 的数据结构,天然适合处理 方法调用链: public class StackDemo { public static void main(String[] args) { method1(); } public static void method1() { method2(); } public static void method2() { method3(); } public static void method3() { System.out.println("Hello"); } } 方法调用链: ...

2025-11-20 · maneng

本地方法栈:Native方法的秘密

引言:Java与本地代码的桥梁 当你调用以下代码时,是否想过它们在JVM背后如何工作? // 获取当前时间戳 long time = System.currentTimeMillis(); // 计算对象哈希码 int hash = obj.hashCode(); // 加载本地库 System.loadLibrary("nativeLib"); 这些方法的共同特点是:它们都是 Native方法,不是用Java实现的,而是用C/C++等本地语言编写的。 Java如何调用这些本地代码?答案就是 本地方法栈(Native Method Stack) 和 JNI(Java Native Interface)。 什么是Native方法? 核心定义 Native方法 是用 native 关键字声明的方法,没有方法体,由 本地语言(C/C++)实现。 public class Object { // hashCode是Native方法 public native int hashCode(); // clone也是Native方法 protected native Object clone() throws CloneNotSupportedException; } public final class System { // currentTimeMillis是Native方法 public static native long currentTimeMillis(); // arraycopy也是Native方法 public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); } 特点: 使用 native 关键字修饰 没有方法体(没有{}) 实现代码在JVM外部(通常是C/C++) 通过JNI(Java Native Interface)调用 为什么需要Native方法? 1. 与操作系统交互 ...

2025-11-20 · maneng

堆内存:对象的诞生地与分代设计

引言:对象的家园 当你写下这行代码时: User user = new User(); 这个User对象存储在哪里?答案是:堆(Heap)。 堆是JVM管理的 最大的内存区域,也是 垃圾收集器的主战场。理解堆的结构和工作原理,是掌握Java内存管理和性能调优的基础。 为什么堆如此重要? 90%以上的对象实例存储在堆中 堆内存不足是最常见的OOM原因 垃圾回收主要发生在堆中 堆的大小直接影响应用性能 本文将深入理解堆的分代设计、分代假说、以及对象的分配策略。 什么是堆? 核心定义 堆(Heap) 是JVM管理的最大内存区域,用于存储 几乎所有的对象实例和数组。 关键特点: 线程共享:所有线程共享同一个堆 动态分配:对象的创建和销毁是动态的 GC管理:堆是垃圾收集器的主要工作区域 可调整大小:通过JVM参数调整堆的大小 堆的基本参数 通过JVM参数控制堆的大小: # -Xms: 初始堆大小(起始大小) # -Xmx: 最大堆大小 # 建议: 生产环境中将两者设置为相同值,避免堆动态扩展的开销 # 示例1:设置初始堆512MB,最大堆2GB java -Xms512m -Xmx2g MyApp # 示例2:设置固定堆大小为1GB java -Xms1g -Xmx1g MyApp # 示例3:查看堆信息 java -XX:+PrintFlagsFinal -version | grep HeapSize 常见配置: 应用类型 推荐堆大小 说明 小型应用 512MB - 1GB 适合个人项目、小型Web应用 中型应用 2GB - 4GB 适合中等规模的企业应用 大型应用 8GB - 16GB 适合高并发、大数据应用 超大型应用 32GB+ 适合超大规模、内存密集型应用 堆的分代设计 为什么要分代? 在深入分代结构之前,先理解 为什么要分代。 ...

2025-11-20 · maneng

方法区:类元数据的存储演变

引言:类信息存储在哪里? 当你写下一个类定义时: public class User { private static int count = 0; private static final String TYPE = "USER"; private String name; private int age; public void sayHello() { System.out.println("Hello"); } } 这个类的信息(字段、方法、常量)存储在哪里?答案是:方法区(Method Area)。 方法区是JVM规范中定义的一个 逻辑概念,用于存储 类信息、常量、静态变量、即时编译器编译后的代码 等数据。 为什么方法区很重要? 存储类的元数据,是类加载的基础 运行时常量池是方法调用的关键 JDK 8的元空间改造影响深远 方法区溢出是常见的OOM原因之一 什么是方法区? 核心定义 方法区(Method Area) 是《Java虚拟机规范》中定义的一个 逻辑概念,用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码 等数据。 关键特点: 线程共享:所有线程共享同一个方法区 逻辑概念:规范定义,具体实现由JVM决定 也称"非堆":与堆分开管理,但实际仍属于内存区域 可回收:方法区也会进行垃圾回收,但条件苛刻 方法区 vs 堆 对比维度 方法区 堆 存储内容 类信息、常量、静态变量 对象实例、数组 线程 线程共享 线程共享 GC 条件苛刻,效率低 频繁,效率高 溢出异常 OutOfMemoryError: Metaspace/PermGen OutOfMemoryError: Java heap space 大小 较小(通常几十MB到几百MB) 较大(通常几百MB到几GB) 方法区存储的内容 方法区主要存储以下4类数据: ...

2025-11-20 · maneng

直接内存:堆外内存与NIO

引言:突破JVM堆的限制 当你使用NIO进行文件操作或网络通信时: // NIO的文件读取 FileChannel channel = FileChannel.open(Paths.get("file.txt")); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 直接内存 channel.read(buffer); 这里的 ByteBuffer.allocateDirect() 分配的内存并不在JVM堆中,而是在 堆外内存(Direct Memory),也称为 直接内存。 为什么需要直接内存? 避免Java堆与本地内存之间的数据复制 提升IO性能(零拷贝) 不受GC管理,减少GC压力 适合大数据量、高吞吐量的场景 理解直接内存是掌握高性能IO编程的关键。 什么是直接内存? 核心定义 直接内存(Direct Memory) 是JVM堆之外的内存区域,位于 本地内存(Native Memory),由操作系统管理。 关键特点: 不属于JVM规范定义的内存区域(但实际广泛使用) 不受JVM堆大小限制(受限于物理内存) 不受GC管理(手动释放或通过Cleaner机制) 读写性能高(避免Java堆与本地内存之间的复制) 直接内存 vs 堆内存 对比维度 直接内存 堆内存 位置 本地内存(Native Memory) JVM堆内存 分配方式 ByteBuffer.allocateDirect() new byte[] 或 ByteBuffer.allocate() GC管理 不受GC管理 受GC管理 读写性能 高(零拷贝) 较低(需复制) 分配速度 较慢 较快 释放方式 手动或Cleaner机制 GC自动回收 大小限制 -XX:MaxDirectMemorySize -Xmx 溢出异常 OutOfMemoryError: Direct buffer memory OutOfMemoryError: Java heap space 为什么需要直接内存? 传统IO的性能瓶颈 传统IO流程(使用堆内存): ...

2025-11-20 · maneng

对象的内存布局:对象头、实例数据、对齐填充

引言:对象在内存中的真实面貌 当你创建一个对象时: User user = new User("张三", 25); 这个对象在JVM堆中究竟占用多少字节?内存是如何布局的? 答案可能出乎意料:即使 User 类只有两个字段(name和age),这个对象也可能占用 32字节甚至更多。 为什么? 对象不仅包含实例数据(字段值) 还包含 对象头(存储对象元信息) 还可能有 对齐填充(保证内存对齐) 理解对象的内存布局是掌握JVM内存优化、锁机制、GC原理的基础。 对象的内存布局结构 三大组成部分 在HotSpot虚拟机中,对象在内存中分为3个部分: ┌─────────────────────────────────────────────────┐ │ 对象内存布局 │ ├─────────────────────────────────────────────────┤ │ 1. 对象头 (Object Header) │ │ ├─ Mark Word(8字节,64位JVM) │ │ └─ 类型指针(4字节,开启指针压缩) │ │ │ │ 2. 实例数据 (Instance Data) │ │ · 字段1: String name │ │ · 字段2: int age │ │ │ │ 3. 对齐填充 (Padding) │ │ · 确保对象大小是8字节的倍数 │ └─────────────────────────────────────────────────┘ 对象头(Object Header) 对象头的组成 对象头包含两部分: ...

2025-11-20 · maneng

什么是垃圾?如何判断对象已死?

引言:垃圾回收的第一步 当你创建对象后不再使用它们时: public void createObjects() { User user1 = new User("张三", 25); User user2 = new User("李四", 30); // 方法结束后,user1和user2还能被访问吗? } 这些对象会发生什么?它们如何被回收?在回收之前,JVM如何判断它们是否还有用? 这些问题的答案,都从理解 “什么是垃圾” 开始。 垃圾回收的核心问题: 哪些内存需要回收?(什么是垃圾) 什么时候回收?(GC触发时机) 如何回收?(GC算法和收集器) 本文将深入第一个问题:如何判断对象已死。 什么是垃圾? 定义 垃圾(Garbage) = 不再被使用的对象 更准确地说:垃圾是指无法再被程序访问到的对象。 为什么需要垃圾回收? 内存资源是有限的: 堆内存大小有限(如2GB、4GB) 不断创建对象,内存终将耗尽 需要回收不再使用的对象,释放内存 手动管理的问题(如C/C++): 忘记释放内存 → 内存泄漏 释放后继续使用 → 悬空指针 重复释放 → 程序崩溃 Java的自动垃圾回收: 程序员无需手动释放内存 JVM自动识别垃圾并回收 大幅减少内存管理错误 判断对象是否存活的两种算法 判断对象是否是垃圾,有两种主流算法: 引用计数算法(Reference Counting) 可达性分析算法(Reachability Analysis) 方法1:引用计数算法 核心思想 为每个对象添加一个引用计数器: 有引用指向对象时,计数器+1 引用失效时,计数器-1 计数器为0时,对象可被回收 示例 public class ReferenceCountingDemo { public static void main(String[] args) { Object obj = new Object(); // 计数器 = 1 Object obj2 = obj; // 计数器 = 2 obj = null; // 计数器 = 1 obj2 = null; // 计数器 = 0,可被回收 } } 引用计数变化: ...

2025-11-20 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...