戴老师的博客 - JVM
type: channel
link: https://www.tushu.info/category/JVM/
description: JVM
items: Java GC 调试手记
items: Java 编程的动态性，第 1 部分: 类和类装入
items: 深度剖析Java内存管理：泄漏、溢出与优化
items: Java并发之synchronized关键字深度解析
items: 使用MAT时的Shallow Size和 Retained Size的区别
items: 双亲委派
items: JAVA虚拟机体系结构
items: JVM调优实战：解决CMS concurrent-abortable-preclean LongGC的问题
items: java内存区域

Java GC 调试手记
type: item
date: 2026-07-09T04:39:43+00:00
description: 编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。摘要本文记录了一次 GC 调试的实验过程与结果分析。GC 知识要点回顾问题 1：为什么要调试 GC 参数？在 32 核处理器的系统上，10% 的 GC 时间导致 75% 的吞吐量损失。所以在大型系统上，调试 GC 是以小博大的不错选择。"Small improvements in reducing such a bottleneck can produce large gains in performance."问题 2：怎么样调试 GC？调试 GC，主要有 三个主要的参数：选择合适的 GC Collector整个 JVM Heap 堆的大小Young Generation 的大小（-Xmn 或 -XX:NewRatio=?）问题 3：有哪些不同的 GC Collector？Tony Printezis (JVM 大牛.html) 在 Garbage Collection in the Java HotSpot Virtual Machine 中有图为证，还有一篇更早的 Sun 开发人员介绍 GC 调试 也是有图为证。Neo4j 总结如下：GC shortnameGenerationCommand line parameterCommentCopyYoung-XX:+UseSerialGCThe Copying collectorMarkSweepCompactTenured-XX:+UseSerialGCThe Mark and Sweep CompactorConcurrentMarkSweepTenured-XX:+UseConcMarkSweepGCThe Concurrent Mark and Sweep CompactorParNewYoung-XX:+UseParNewGCThe parallel Young Generation Collector — can only be used with the Concurrent mark and sweep compactor.PS ScavengeYoung-XX:+UseParallelGCThe parallel object scavengerPS MarkSweepTenured-XX:+UseParallelGCThe parallel mark and sweep collector简而言之，Young 和 Tenured 代各有三种 Collector，分别是：Serial：单线程。Parallel：多线程并行，GC 线程和 App 线程取一运行，即 GC 要 Stop the (app.html) World。Concurrent：多线程并发，GC 线程和 App 线程可同时运行。（注：Young generation 没有 CMS，取而代之的是可和 CMS (Old.html) 一起运行的 ParNew）问题 4：如何选择 Collector？Serial 可以直接排除掉，现在最普通的服务器也有双核 64 位/8G 内存，默认的 Collector 是 PS Scavenge 和 PS MarkSweep。所以 Collector 主要在并行 (Parallel.html) 和并发 (Concurrent.html) 两者之间选择。问题 5：选择的标准 (参数指标.html) 是什么？如何得到这些参数值 (How to measure it.html)？指标主要是 Throughput（吞吐量） 和 Latency（延迟）。garbage-collection-in-java-part-3 从 GC 的耗时给出了吞吐量和响应速度的公式：Total Execution Time = Useful Time + Paused Time throughput = Useful Time / Total Execution Time latency = average paused time如何得到 Useful time 和 Paused Time？即如何得到 JVM 的 GC 时间，有以下几种方式：1. GC Log打印 GC log，在 Java 启动参数中加入下面的语句（本文为 Tomcat 应用）。GC Log 记录每次 GC 时间，可根据 GC Log 计算平均 GC 时间和累积 GC 时间。CATALINA_OPTS="$CATALINA_OPTS -verbose:gc -Xloggc:/usr/local/tomcat/gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"2.…

Java 编程的动态性，第 1 部分: 类和类装入
type: item
date: 2026-07-09T04:39:37+00:00
description: Java 编程的动态性，第 1 部分：类和类装入系列内容第 1 部分：类和类装入第 3 部分：应用反射第 5 部分：动态转换类第 6 部分：利用 Javassist 进行面向方面的更改第 7 部分：用 BCEL 设计字节码第 8 部分：用代码生成取代反射引言本文是这个新系列文章的第一篇，该系列将讨论我称之为 Java 编程的动态性 的一系列主题。这些主题的范围从 Java 二进制类文件格式的基本结构，以及使用反射进行运行时元数据访问，一直到在运行时修改和构造新类。贯穿整篇文章的公共线索是这样一种思想：在 Java 平台上编程要比使用直接编译成本机代码的语言更具动态性。如果您理解了这些动态方面，就可以使用 Java 编程完成那些在任何其它主流编程语言中不能完成的事情。本文中，我将讨论一些基本概念，它们是这些 Java 平台动态特性的基础。这些概念的核心是用于表示 Java 类的二进制格式，包括这些类装入到 JVM 时所发生的情况。本文不仅是本系列后续文章的基础，而且还演示了开发人员在使用 Java 平台时碰到的一些非常实际的问题。用二进制表示的类使用 Java 语言的开发人员在用编译器编译他们的源代码时，通常不必关心对这些源代码做了些什么样的细节处理。但是在本系列文章中，我将讨论从源代码到执行程序所涉及的许多幕后细节，因此我将首先探讨由编译器生成的二进制类。二进制类格式实际上是由 JVM 规范定义的。通常这些类表示是由编译器从 Java 语言源代码生成的，而且它们通常存储在扩展名为 .class 的文件中。但是，这些特性都无关紧要。已经开发了可以使用 Java 二进制类格式的其它一些编程语言，而且出于某些目的，还构建了新的类表示，并被立即装入到运行中的 JVM。就 JVM 而言，重要的部分不是源代码以及如何存储源代码，而是格式本身。那么这个类格式实际看上去是什么样呢？清单 1 提供了一个（非常）简短的类的源代码，还附带了由编译器输出的类文件的部分十六进制显示：清单 1. Hello.java 的源代码和（部分）二进制类文件public class Hello { public static void main(String[] args.html) { System.out.println("Hello, World!".html); } }0000: cafe babe 0000 002e 001a 0a00 0600 0c09 ................ 0010: 000d 000e 0800 0f0a 0010 0011 0700 1207 ................ 0020: 0013 0100 063c 696e 6974 3e01 0003 2829 .....<init>...() 0030: 5601 0004 436f 6465 0100 046d 6169 6e01 V...Code...main. 0040: 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 ..([Ljava/lang/S 0050: 7472 696e 673b 2956 0c00 0700 0807 0014 tring;)V........ 0060: 0c00 1500 1601 000d 4865 6c6c 6f2c 2057 ........Hello, W 0070: 6f72 6c64 2107 0017 0c00 1800 1901 0005 orld!........... 0080: 4865 6c6c 6f01 0010 6a61 7661 2f6c 616e Hello...java/lan 0090: 672f 4f62 6a65 6374 0100 106a 6176 612f g/Object...java/ 00a0: 6c61 6e67 2f53 7973 7465 6d01 0003 6f75 lang/System...ou ...二进制类文件的内幕清单 1 显示的二进制类表示中首先是"cafe babe"特征符，它标识 Java 二进制类格式（并顺便作为一个永久的 — 但在很大程度上未被认识到的 — 礼物送给努力工作的 barista，他们本着开发人员所具备的精神构建 Java 平台）。这个特征符恰好是一种验证一个数据块 确实 声明成 Java 类格式的一个实例的简单方法。任何 Java 二进制类（甚至是文件系统中没有出现的类）都需要以这四个字节作为开始。该数据的其余部分不太吸引人。该特征符之后是一对类格式版本号（本例中，是由 1.4.1 javac 生成的次版本 0 和主版本 46 — 用十六进制表示就是 0x2e），接着是常量池中项的总数。项总数（…

深度剖析Java内存管理：泄漏、溢出与优化
type: item
date: 2026-07-09T04:39:36+00:00
description: 一、引言在 Java 编程领域，内存管理犹如大厦的基石，直接关系到应用程序的稳定性、性能和可扩展性。尽管 Java 拥有自动内存管理机制（垃圾回收器，Garbage Collector），但内存泄漏（Memory Leak）和内存溢出（OutOfMemoryError）问题仍然如影随形，困扰着许多开发者，给应用程序带来诸多隐患。内存泄漏就像一个隐藏在程序深处的“黑洞”，悄无声息地吞噬着内存资源。它指的是那些不再被应用程序使用的对象，却因为某些原因未能被垃圾回收器识别并回收，导致内存空间被白白占用。随着时间的推移，这些泄漏的内存逐渐积累，最终可能引发内存溢出，使程序陷入崩溃的边缘。内存溢出则是内存泄漏的“恶果”，当应用程序所需的内存超出了 JVM 所能提供的最大内存时，就会出现这种情况，导致程序无法正常运行，给用户带来极差的体验。二、Java 内存泄漏的深度解析（一）内存泄漏的概念内存泄漏的核心在于对象的生命周期管理失控。在 Java 中，对象通过引用被访问和操作。当一个对象不再被程序逻辑所需要，但仍然存在引用指向它时，垃圾回收器就无法将其回收，从而造成内存泄漏。这就好比一个房间里堆满了无用的杂物，但由于某些原因，这些杂物始终无法被清理出去，最终导致房间越来越拥挤，可用空间越来越小。（二）引发内存泄漏的常见场景1. 静态集合类的陷阱静态集合类（如 HashMap、ArrayList 等）在应用程序中广泛使用。如果将对象添加到静态集合中后，没有在合适的时机将其移除，那么即使该对象不再被使用，它也会一直存在于集合中，无法被回收。例如，在一个 Web 应用中，如果将用户会话信息存储在静态的 HashMap 中，而在用户注销时没有正确删除对应的会话对象，那么随着用户的不断登录和注销，HashMap 会不断积累无用的会话对象，最终导致内存泄漏。以下是一个简单的示例代码，展示了静态集合类可能导致的内存泄漏问题：import java.util.ArrayList; import java.util.List; public class StaticCollectionMemoryLeak { private static List<Object> staticList = new ArrayList<>(); public static void addObject(Object obj.html) { staticList.add(obj.html); } public static void main(String[] args.html) { for (int i = 0; i < 100000; i++.html) { addObject(new Object(.html)); } // 这里模拟程序运行一段时间后，忘记移除部分对象 staticList.remove(0.html); // 即使调用了 remove 方法，由于静态集合的生命周期与应用程序相同， // 之前添加的大量对象仍然可能无法被垃圾回收，导致内存泄漏 } }2. 监听器和回调的疏忽在 Java 的事件驱动编程中，监听器和回调机制十分常见。如果注册了监听器或回调，但在不再需要时没有及时注销，就会导致相关对象无法被回收。例如，在一个图形用户界面（GUI）应用中，为按钮添加了点击事件监听器。当界面关闭时，如果没有正确注销该监听器，那么即使界面已经不存在，监听器对象及其引用的相关资源仍然会留在内存中。以下是一个简单的 Swing 应用示例，展示了监听器未注销可能导致的内存泄漏问题：import javax.swing.JButton; import javax.swing.JFrame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class ListenerMemoryLeak { public static void main(String[] args.html) { JFrame frame = new JFrame("Listener Memory Leak Example".html); JButton button = new JButton("Click Me".html); button.addActionListener(new ActionListener(.html) { @Override public void actionPerformed(ActionEvent e.html) { System.out.println("Button clicked!".html); } }); frame.add(button.html…

Java并发之synchronized关键字深度解析
type: item
date: 2026-07-09T04:39:12+00:00
description: 前言本文承接【Java 并发之 synchronized 关键字深度解析（一）】，着重介绍 synchronized 几种锁的特性及其底层实现原理。一、对象头结构及锁状态标识synchronized 关键字是如何实现给对象加锁的？首先我们需要了解 Java 中对象的组成。Java 对象在内存中主要由三部分组成：对象头（Object Header）、实例数据（Instance Data）和对齐填充（Padding）。对齐填充：JVM 规定对象的起始内存地址必须是 8 字节的整数倍，如果不够则用占位符填充，此部分即为对齐填充。实例数据：对象存储的真正有效信息，即对象的成员变量信息（包括继承自父类的）。对象头：由两部分组成。第一部分是对象的运行时数据（Mark Word），包括哈希码、锁偏向标识、锁类型、GC 分代年龄、偏向线程 ID 等；第二部分是对象的类型指针（Klass Word），用于在堆中定位对象的实例数据和方法区中的类型数据。Java 对象的公共特性都存放在对象头中。对象头存储内容如下所示（以 64 位操作系统为例）：|--------------------------------------------------------------------------------------------------------------| | Object Header (128 bits.html) | |--------------------------------------------------------------------------------------------------------------| | Mark Word (64 bits.html) | Klass Word (64 bits.html) | |--------------------------------------------------------------------------------------------------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁 |----------------------------------------------------------------------|--------|------------------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁 |----------------------------------------------------------------------|--------|------------------------------| | ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁 |----------------------------------------------------------------------|--------|------------------------------| | ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁 |----------------------------------------------------------------------|--------|------------------------------| | | lock:2 | OOP to metadata object | GC |--------------------------------------------------------------------------------------------------------------|其中 lock:2 表示有 2 bit 控制锁类型，biased_lock:1 表示 1 bit 控制偏向锁状态，对应关系如下所示：01：无锁（前面偏向锁状态为 0 时表示未锁定）01：可偏向（前面偏向锁状态为 1 时表示可偏向）00：轻量级锁10：重量级锁11：GC 标记看到前两种状态时读者可能会有些疑惑…

使用MAT时的Shallow Size和 Retained Size的区别
type: item
date: 2026-07-09T04:38:54+00:00
description: 使用 MAT 时的 Shallow Size 和 Retained Size 的区别编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。所有具备 Heap Profiling（堆分析）功能的工具（如 MAT、Yourkit、JProfiler、TPTP 等）都会涉及两个核心概念：Shallow Size 和 Retained Size。这两个概念在日常开发中并不常见，但它们对于理解内存占用至关重要。本文将对这两个名词进行详细解释。Shallow SizeShallow Size 指的是对象自身占用的内存大小，不包括它引用的其他对象。非数组类型对象：大小等于对象头与它所有成员变量大小的总和。这其中还包括一些 Java 语言特性的数据存储单元（如对象头信息）。数组类型对象：大小是数组元素对象的大小总和。Retained SizeRetained Size 的计算公式如下：Retained Size = 当前对象大小 + 当前对象可直接或间接引用到的对象的大小总和间接引用的含义：若存在引用链 A -> B -> C，则 C 是 A 的间接引用对象。实际意义：Retained Size 代表当前对象被 GC（垃圾回收）后，从 Heap（堆）上总共能释放掉的内存。注意：在计算释放内存时，需要排除被 GC Roots 直接或间接引用的对象，因为这些对象暂时不会被当做 Garbage（垃圾）回收。图解 Retained Size通过下图可以更直观地理解 Retained Size 的计算逻辑。在上图中，GC Roots 直接引用了 A 和 B 两个对象：A 对象的 Retained Size = A 对象的 Shallow SizeB 对象的 Retained Size = B 对象的 Shallow Size + C 对象的 Shallow Size这里不包括 D 对象，因为 D 对象被 GC Roots 直接引用，即使 B 被回收，D 依然存在。如果 GC Roots 不引用 D 对象呢？此时，引用关系发生变化：B 对象的 Retained Size = B 对象的 Shallow Size + C 对象的 Shallow Size + D 对象的 Shallow Size参考资料转载自：http://kenwublog.com/understand-shallow-and-retained-size-in-hprofling

双亲委派
type: item
date: 2026-07-09T04:38:50+00:00
description: 背景知识在介绍双亲委派机制之前，首先需要了解 ClassLoader（类加载器） 的相关概念。Java 程序运行在 Java 虚拟机（JVM）之中。我们在 IDE 中编写的 Java 源代码被编译器编译成 .class 字节码文件，随后由 ClassLoader 负责将这些 class 文件加载到 JVM 中执行。JVM 中主要提供了三层 ClassLoader：Bootstrap ClassLoader：主要负责加载核心类库（如 java.lang.* 等），并负责构造 ExtClassLoader 和 AppClassLoader。ExtClassLoader：主要负责加载 jre/lib/ext 目录下的一些扩展 jar 包。AppClassLoader：主要负责加载应用程序的主函数类及classpath 下的类。那么，一个由 Hello.java 编译成的 Hello.class 文件，究竟是如何被加载到 JVM 中的呢？双亲委派机制详解查看 JDK 源码中 java.lang 包下的 ClassLoader 类，重点关注其 loadClass 方法，核心逻辑如下：public Class<?> loadClass(String name.html) throws ClassNotFoundException { return loadClass(name, false.html); } // ----- 核心逻辑 ----- protected Class<?> loadClass(String name, boolean resolve.html) throws ClassNotFoundException { // 首先，检查是否已经被类加载器加载过 Class<?> c = findLoadedClass(name.html); if (c == null.html) { try { // 存在父加载器，递归地交由父加载器处理 if (parent != null.html) { c = parent.loadClass(name, false.html); } else { // 直到最上面的 Bootstrap 类加载器 c = findBootstrapClassOrNull(name.html); } } catch (ClassNotFoundException e.html) { // 如果父加载器无法找到该类，则捕获异常 // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null.html) { // 如果仍未找到，则调用 findClass 方法尝试自己加载 c = findClass(name.html); } } return c; }这段代码清晰地解释了双亲委派机制。为了更直观地理解，我们可以通过下图来描述上述代码的执行流程：结合上图与源码，当一个类（如 Hello.class）需要被加载时，流程如下：检查是否已加载：首先会在当前的 ClassLoader（例如 AppClassLoader）中检查该类是否已经被加载过，如果有则无需再次加载。委托父加载器：如果没有加载过，则获取父加载器，调用父加载器的 loadClass 方法。递归向上：父加载器同理，先检查自己是否加载过，如果没有则继续向上委托。这是一个类似递归的过程，直到到达 Bootstrap ClassLoader。向下查找：在到达 Bootstrap ClassLoader 之前，各级加载器都只是检查是否加载过，并不会尝试自己去加载。直到 Bootstrap ClassLoader（它没有父加载器），如果它无法加载，则会下沉到子加载器去尝试加载（调用 findClass），一直到最底层。抛出异常：如果没有任何加载器能加载该类，最终将抛出 ClassNotFoundException。机制设计的意义双亲委派机制的设计主要有以下好处：防止核心类库被篡改。例如，如果有人试图替换系统级别的类（如 String.java）并篡改其实现，在这种机制下，由于系统类已经被 Bootstrap ClassLoader 加载过了（当一个类需要加载时，最先尝试加载的就是 Bootstrap ClassLoader），其他类加载器并没有机会再去加载这些类。这在一定程度上防止了危险代码的植入，保证了 Java 核心 API 的安全性。参考文章https://tushu.info/archives/java-de-classloader-yu-package-ji-zhi.html> 说明：本文基于 J…

JAVA虚拟机体系结构
type: item
date: 2026-07-09T04:38:40+00:00
description: JAVA虚拟机的生命周期 一个运行时的Java虚拟机实例的天职是：负责运行一个java程序。当启动一个Java程序时，一个虚拟机实例也就诞生了。当该程序关闭退出，这个虚拟机实例也就随之消亡。如果同一台计算机上同时运行三个Java程序，将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。 Java虚拟机实例通过调用某个初始类的main()方法来运行一个Java程序。而这个main()方法必须是共有的(public.html)、静态的(static.html)、返回值为void，并且接受一个字符串数组作为参数。任何拥有这样一个main()方法的类都可以作为Java程序运行的起点。public class Test { public static void main(String[] args.html) { // TODO Auto-generated method stub System.out.println("Hello World".html); } } 在上面的例子中，Java程序初始类中的main()方法，将作为该程序初始线程的起点，任何其他的线程都是由这个初始线程启动的。 在Java虚拟机内部有两种线程：守护线程和非守护线程。守护线程通常是由虚拟机自己使用的，比如执行垃圾收集任务的线程。但是，Java程序也可以把它创建的任何线程标记为守护线程。而Java程序中的初始线程——就是开始于main()的那个，是非守护线程。 只要还有任何非守护线程在运行，那么这个Java程序也在继续运行。当该程序中所有的非守护线程都终止时，虚拟机实例将自动退出。假若安全管理器允许，程序本身也能够通过调用Runtime类或者System类的exit()方法来退出。JAVA虚拟机的体系结构 下图是JAVA虚拟机的结构图，每个Java虚拟机都有一个类装载子系统，它根据给定的全限定名来装入类型（类或接口）。同样，每个Java虚拟机都有一个执行引擎，它负责执行那些包含在被装载类的方法中的指令。 当JAVA虚拟机运行一个程序时，它需要内存来存储许多东西，例如：字节码、从已装载的class文件中得到的其他信息、程序创建的对象、传递给方法的参数，返回值、局部变量等等。Java虚拟机把这些东西都组织到几个“运行时数据区”中，以便于管理。 某些运行时数据区是由程序中所有线程共享的，还有一些则只能由一个线程拥有。每个Java虚拟机实例都有一个方法区以及一个堆，它们是由该虚拟机实例中所有的线程共享的。当虚拟机装载一个class文件时，它会从这个class文件包含的二进制数据中解析类型信息。然后把这些类型信息放到方法区中。当程序运行时，虚拟机会把所有该程序在运行时创建的对象都放到堆中。 当每一个新线程被创建时，它都将得到它自己的PC寄存器（程序计数器）以及一个Java栈，如果线程正在执行的是一个Java方法（非本地方法），那么PC寄存器的值将总是指向下一条将被执行的指令，而它的Java栈则总是存储该线程中Java方法调用的状态——包括它的局部变量，被调用时传进来的参数、返回值，以及运算的中间结果等等。而本地方法调用的状态，则是以某种依赖于具体实现的方法存储在本地方法栈中，也可能是在寄存器或者其他某些与特定实现相关的内存区中。 Java栈是由许多栈帧（stack frame）组成的，一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时，虚拟机压入一个新的栈帧到该线程的Java栈中，当该方法返回时，这个栈帧被从Java栈中弹出并抛弃。 Java虚拟机没有寄存器，其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑、同时也便于Java虚拟机在那些只有很少通用寄存器的平台上实现。另外，Java虚拟机这种基于栈的体系结构，也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。 下图描绘了Java虚拟机为每一个线程创建的内存区，这些内存区域是私有的，任何线程都不能访问另一个线程的PC寄存器或者Java栈。 上图展示了一个虚拟机实例的快照，它有三个线程正在执行。线程1和线程2都正在执行Java方法，而线程3则正在执行一个本地方法。 Java栈都是向下生长的，而栈顶都显示在图的底部。当前正在执行的方法的栈帧则以浅色表示，对于一个正在运行Java方法的线程而言，它的PC寄存器总是指向下一条将被执行的指令。比如线程1和线程2都是以浅色显示的，由于线程3当前正在执行一个本地方法，因此，它的PC寄存器——以深色显示的那个，其值是不确定的。 数据类型 Java虚拟机是通过某些数据类型来执行计算的，数据类型可以分为两种：基本类型和引用类型，基本类型的变量持有原始值，而引用类型的变量持有引用值。 J…

JVM调优实战：解决CMS concurrent-abortable-preclean LongGC的问题
type: item
date: 2026-07-09T04:38:23+00:00
description: 1. 背景1.1 多个业务线的应用出现 Long GC 告警最近一段时间，频繁收到监控系统 CAT 上报的 Long GC 告警（配置阈值为大于 3 秒）。2. 分析前的一些 JVM 背景知识回顾2.1 JVM 堆内存划分新生代（Young Generation）新生代内被划分为三个区：Eden、From Survivor、To Survivor。大多数对象在新生代被创建，Minor GC 针对的是新生代的垃圾回收。老年代（Old Generation）在新生代中经历了几次 Minor GC 仍然存活的对象，会被晋升到老年代。Major GC 针对的是老年代的垃圾回收。本文重点分析的 CMS 就是一种针对老年代的垃圾回收算法。另外，Full GC 是针对整堆（包括新生代和老年代）做的垃圾回收。永久代（Perm）主要存放已被虚拟机加载的类信息、常量、静态变量等数据。该区域对垃圾回收的影响不大，本文不会过多涉及。2.2 CMS 垃圾回收的 6 个重要阶段initial-mark（初始标记）：CMS 的第一个 STW（Stop-The-World）阶段。标记 GC Root 直接引用的对象，由于 GC Root 直接引用的对象不多，所以速度很快。concurrent-mark（并发标记）：由第一阶段标记过的对象出发，所有可达的对象都在本阶段标记。concurrent-preclean（并发预清理）：也是一个并发执行的阶段。在本阶段，会查找前一阶段执行过程中从新生代晋升、新分配或被更新的对象。通过并发地重新扫描这些对象，预清理阶段可以减少下一个 STW 重新标记阶段的工作量。concurrent-abortable-preclean（并发可中止的预清理）：这个阶段其实跟上一个阶段做的事情一样，也是为了减少下一个 STW 重新标记阶段的工作量。增加这一阶段是为了让我们可以控制这个阶段的结束时机，比如扫描多长时间（默认 5 秒）或者 Eden 区使用占比达到期望比例（默认 50%）就结束本阶段。remark（重标记）：CMS 的第二个 STW 阶段。暂停所有用户线程，从 GC Root 开始重新扫描整堆，标记存活的对象。需要注意的是，虽然 CMS 只回收老年代的垃圾对象，但是这个阶段依然需要扫描新生代，因为很多 GC Root 都在新生代，而这些 GC Root 指向的对象又在老年代，这称为“跨代引用”。concurrent-sweep（并发清理）。3. 分析下面先看看出现 Long GC 时发生了什么。选取其中一个应用分析其 GC 日志，发现 Long GC 发生在 CMS 的收集阶段。箭头 1 显示 abortable-preclean 阶段耗时 4.04 秒。箭头 2 显示的是 remark 阶段，耗时 0.11 秒。虽然 abortable-preclean 阶段是 concurrent 的，不会暂停其他的用户线程，就算不优化可能影响也不大。但是频繁收到各个业务线的 GC 报警，长久来说也不是理想的状态。在调优之前先看下该应用的 GC 统计数据，包括 GC 次数、耗时：统计期间内（18 天）发生 CMS GC 69 次，其中 abortable preclean 阶段平均耗时 2.45 秒，final remark 阶段平均 112ms，最大耗时 170ms。4. 优化目标降低 abortable preclean 时间，而且不增加 final remark 的时间（因为 remark 是 STW 的）。5. JVM 参数调优5.1 第一次调优先尝试调低 abortable preclean 阶段的时间，看看效果。有两个参数可以控制这个阶段何时结束：-XX:CMSMaxAbortablePrecleanTime=5000：默认值 5s，代表该阶段最大的持续时间。-XX:CMSScheduleRemarkEdenPenetration=50：默认值 50%，代表 Eden 区使用比例超过 50% 就结束该阶段进入 remark。调整为最大持续时间为 1s，Eden 区使用占比 10%，如下：-XX:CMSMaxAbortablePrecleanTime=1000 -XX:CMSScheduleRemarkEdenPenetration=10调整思路：首先，每次 CMS 都发生在老年代使用占比达到 80% 时，这是由下面两个参数决定的：-XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly老年代的增长是由于部分对象在 Minor GC 后仍然存活，被晋升到老年代，导致老年代使用占比增长。也就是在每次 CMS GC 发生之前刚刚发生过一次 Mino…

java内存区域
type: item
date: 2026-07-09T04:37:07+00:00
description: Java 内存区域编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。Java 虚拟机（JVM）管理的内存区域主要分为以下五部分：线程共享区域：方法区（Method Area）、堆（Heap）。线程私有区域：程序计数器（Program Counter Register）、本地方法栈（Native Method Stack）、虚拟机栈（VM Stack）。程序计数器（Program Counter Register）程序计数器是一块较小的内存空间，它可以看作是当前线程所执行的字节码的行号指示器。每一条 JVM 线程都有自己的程序计数器，各条线程之间互不影响、独立存储，这类内存区域被称为“线程私有”内存。在任意时刻，一条 JVM 线程只会执行一个方法的代码，该方法称为该线程的当前方法（Current Method）。如果该方法是 Java 方法，程序计数器保存的是 JVM 正在执行的字节码指令的地址。如果该方法是 native 方法，程序计数器的值则为 undefined（未定义）。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。说明：Java 虚拟机的多线程是通过线程轮流切换并分配处理器时间来实现的。Java 虚拟机栈（Java Virtual Machine Stack）与程序计数器一样，Java 虚拟机栈也是线程私有的。每一个 JVM 线程都有自己的虚拟机栈，栈与线程同时创建，其生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型：每个方法被执行的时候都会同时创建一个栈帧（Stack Frame），用于存储局部变量表、操作数栈（如表达式 a+b 的语法树计算、进栈出栈操作）、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程，就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。JVM 栈可以被实现成固定大小，也可以根据计算动态扩展。如果采用固定大小的 JVM 栈设计，那么每一条线程的 JVM 栈容量应该在线程创建时独立地选定。JVM 实现应该提供调节 JVM 栈初始容量的手段。如果采用动态扩展和收缩的 JVM 栈方式，应该提供调节最大、最小容量的手段。如果线程请求的栈深度大于虚拟机所允许的深度，将抛出 StackOverflowError。如果 JVM 栈可以动态扩展，但在尝试扩展时无法申请到足够的内存，则抛出 OutOfMemoryError。本地方法栈（Native Method Stack）本地方法栈与虚拟机栈作用相似，后者为虚拟机执行 Java 方法服务，而前者为虚拟机用到的 Native 方法服务。虚拟机规范对于本地方法栈中方法使用的语言、使用方式和数据结构没有强制规定。有的虚拟机（比如 HotSpot）直接把本地方法栈和虚拟机栈合二为一。该区域抛出的异常类型与虚拟机栈相同（StackOverflowError 或 OutOfMemoryError）。Java 堆（Java Heap）Java 堆是虚拟机管理的内存中最大的一块，同时也是被所有线程所共享的。它在虚拟机启动时创建，其主要作用是存放对象实例，几乎所有的对象实例以及数组都要在这里分配内存。堆中的对象被自动管理，也就是俗称的 GC（Garbage Collector，垃圾收集器） 所管理。开发者无需操心销毁回收的事儿。Java 堆的容量可以是固定大小，也可以随着需求动态扩展（通过 -Xms 和 -Xmx 参数），并在不需要过多空间时自动收缩。Java 堆所使用的内存不需要保证是物理连续的，只要逻辑上是连续的即可。JVM 实现应当提供给程序员调节 Java 堆初始容量的手段；对于可动态扩展和收缩的堆，则应当提供调节其最大和最小容量的手段。如果堆中没有内存完成实例分配并且堆也无法扩展，就会抛出 OutOfMemoryError。方法区（Method Area）方法区跟堆一样是被各个线程共享的内存区域，用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然这个区域被虚拟机规范描述为堆的一个逻辑部分，但是它的别名叫“非堆”，用来与堆做区别。方法区在虚拟机启动的时候创建。方法区的容量可以是固定大小的，也可以随着程序执行的需求动态扩展，并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段；对于可以动态扩展和收缩的方法区，则应当提供调节其最大、最小容量的手段。当方法区无法满足内存分配需求时，就会抛出 OutOfMemoryError。示例：堆、栈和方法区之间的关系以下代码示例说明了对象实例、类信息与引用变量在不同…

JAVA虚拟机体系结构
type: item
date: 2026-07-09T04:36:25+00:00
description: Java 虚拟机的生命周期一个运行时的 Java 虚拟机（Java Virtual Machine, JVM）实例的职责是运行一个 Java 程序。当启动一个 Java 程序时，一个虚拟机实例随之诞生；当该程序关闭退出时，这个虚拟机实例也随之消亡。如果在同一台计算机上同时运行三个 Java 程序，将得到三个 Java 虚拟机实例。每个 Java 程序都运行于它自己的 Java 虚拟机实例中。Java 虚拟机实例通过调用某个初始类的 main() 方法来运行一个 Java 程序。这个 main() 方法必须是公有的（public）、静态的（static）、返回值为 void，并且接受一个字符串数组作为参数。任何拥有这样一个 main() 方法的类都可以作为 Java 程序运行的起点。public class Test { public static void main(String[] args.html) { // TODO Auto-generated method stub System.out.println("Hello World".html); } }在上面的例子中，Java 程序初始类中的 main() 方法将作为该程序初始线程的起点，任何其他的线程都是由这个初始线程启动的。在 Java 虚拟机内部有两种线程：守护线程（Daemon Thread）和非守护线程（Non-Daemon Thread）。守护线程通常是由虚拟机自己使用的，比如执行垃圾收集任务的线程。但是，Java 程序也可以把它创建的任何线程标记为守护线程。而 Java 程序中的初始线程——即开始于 main() 的那个线程，是非守护线程。只要还有任何非守护线程在运行，那么这个 Java 程序也在继续运行。当该程序中所有的非守护线程都终止时，虚拟机实例将自动退出。假若安全管理器允许，程序本身也能够通过调用 Runtime 类或者 System 类的 exit() 方法来退出。Java 虚拟机的体系结构下图是 Java 虚拟机的结构图。每个 Java 虚拟机都有一个类装载子系统（Class Loading Subsystem），它根据给定的全限定名来装入类型（类或接口）。同样，每个 Java 虚拟机都有一个执行引擎（Execution Engine），它负责执行那些包含在被装载类的方法中的指令。![]([binary]