Linux/Unix 中的标准输出与错误输出在 Linux/Unix 系统中，屏幕上显示的信息通常来源于 stdout（Standard Output，标准输出）或者 stderr（Standard Error，标准错误输出）。许多人会问：output 就是 output，直接送到屏幕上不就行了，为什么要分成 stdout 和 stderr 呢？这是因为在服务器工作环境下，几乎所有的程序都是运行在后台（background）的。为了方便调试（debug），一般在设计程序时，会将正常的 stdout 送到一个文件，而将错误的信息 stderr 存到不同的文件中。正常的 output：例如程序开始运行的时间、当前在线人数等。错误的 output：例如无法找到用户想要访问的 URL，或者信用卡认证失败等。什么是 /dev/null有了上述认知后，我们再来看什么是 > /dev/null。这条命令的意思是将 stdout 送到 /dev/null 里面。那么，什么是 /dev/null 呢？/dev/null 是 Unix/Linux 里的「无底洞」。任何 output 送去了「无底洞」就再也无法找回了。相信我，数据真的消失了！那么，在什么情况下要把 output 送去这个无底洞呢？这里没有标准答案，但通常来说，如果你不想看到 output，或者 output 太多太大，有可能把硬盘占满的时候，程序设计就会考虑将 output 送到 /dev/null。重定向语法详解：>/dev/null 2>&1在 Shell 脚本中，经常能看到 >/dev/null 2>&1 这样的写法。这条命令的意思是将标准输出和错误输出全部重定向到 /dev/null 中，也就是将产生的所有信息丢弃。命令的结果可以通过 %> 的形式来定义输出。我们可以将这个组合 >/dev/null 2>&1 分解为五个部分来理解：>：代表重定向到哪里。例如：echo "123" > /home/123.txt。/dev/null：代表空设备文件。2>：表示 stderr（标准错误）。&：表示等同于的意思。2>&1 表示 2 的输出重定向等同于 1。1：表示 stdout（标准输出）。系统默认值是 1，所以 >/dev/null 等同于 1>/dev/null。因此，>/dev/null 2>&1 也可以写成 1>/dev/null 2>&1。执行过程解析上述语句的执行过程如下：1>/dev/null：首先表示标准输出重定向到空设备文件，也就是不输出任何信息到终端，说白了就是不显示任何信息。2>&1：接着，标准错误输出重定向到标准输出。因为之前标准输出已经重定向到了空设备文件，所以标准错误输出也随之重定向到空设备文件。command > file 2>file 与 command > file 2>&1 的区别下面就为大家说明一下，command > file 2>file 与 command > file 2>&1 有什么不同的地方。command > file 2>file意思是将命令所产生的标准输出信息，和错误的输出信息送到 file 中。这样的写法，stdout 和 stderr 都直接送到 file 中，file 会被打开两次。这样 stdout 和 stderr 会互相覆盖，相当使用了 FD1 和 FD2 两个文件描述符同时去抢占 file 的管道。command > file 2>&1这条命令将 stdout 直接送向 file，stderr 继承了 FD1 管道后，再被送往 file。此时，file 只被打开了一次，也只使用了一个管道 FD1，它包括了 stdout 和 stderr 的内容。从 I/O 效率上来看，前一条命令的效率要比后一条命令的效率低。所以在编写 Shell 脚本的时候，较多的时候我们会用 command > file 2>&1 这样的写法。说明：本文所述重定向语法适用于大多数 Bourne Again Shell (Bash) 及兼容的 POSIX Shell 环境。
Java源码分析：深入探讨Iterator模式——迭代器模式
引言java.util 包中包含了一系列重要的集合类。本文将从源码分析入手，深入研究集合类的内部结构，以及遍历集合的迭代器模式（Iterator Pattern）的源码实现内幕。我们将先讨论根接口 Collection，然后分析抽象类 AbstractList 及其对应的 Iterator 接口，仔细研究迭代器模式的实现原理。说明：本文讨论的源代码版本基于 JDK 1.4.2。JDK 1.5 及后续版本在 java.util 中引入了泛型（Generics），为了简化问题并聚焦核心原理，本文主要讨论 1.4 版本的代码逻辑，但核心设计思想在后续版本中依然适用。集合类的根接口 CollectionCollection 接口是所有集合类的根类型。它的一个主要接口方法如下：boolean add(Object o)add() 方法用于添加一个新元素。注意该方法返回一个 boolean 值，但返回值不是表示添加成功与否。仔细阅读文档可以看到，Collection 规定：如果一个集合拒绝添加这个元素（无论任何原因），都必须抛出异常。返回值表示的意义是：add() 方法执行后，集合的内容是否改变了（即元素有无数量、位置等变化），具体行为由实现类决定。即：如果方法出错，总会抛出异常；返回值仅仅表示该方法执行后这个 Collection 的内容有无变化。类似的方法还有：boolean addAll(Collection c); boolean remove(Object o); boolean removeAll(Collection c); boolean retainAll(Collection c);Object[] toArray() 方法很简单，把集合转换成数组返回。Object[] toArray(Object[] a) 方法则稍微复杂一些：返回的 Object[] 仍然是把集合的所有元素变成的数组，但是类型和参数 a 的类型是相同的。例如执行：String[] o = (String[])c.toArray(new String[0]);得到的 o 实际类型是 String[]。如果参数 a 的大小装不下集合的所有元素，返回的将是一个新的数组。如果参数 a 的大小能装下集合的所有元素，则返回的还是 a，但 a 的内容用集合的元素来填充。尤其要注意的是，如果 a 的大小比集合元素的个数还多，a 后面的部分全部被置为 null。最后一个最重要的方法是 iterator()，返回一个 Iterator（迭代器），用于遍历集合的所有元素。使用 Iterator 模式遍历集合Iterator 模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出来，从而避免向客户端暴露集合的内部结构。例如，如果没有使用 Iterator，遍历一个数组的方法是使用索引：for(int i = 0; i < array.size(); i++) { // TODO: get(i) ... }而访问一个链表（LinkedList）又必须使用 while 循环：while((e = e.next()) != null) { // TODO: e.data() ... }以上两种方法客户端都必须事先知道集合的内部结构，访问代码和集合本身是紧耦合，无法将访问逻辑从集合类和客户端代码中分离出来。每一种集合对应一种遍历方法，客户端代码无法复用。更严重的是，如果以后需要把 ArrayList 更换为 LinkedList，则原来的客户端代码必须全部重写。为解决以上问题，Iterator 模式总是用同一种逻辑来遍历集合：for(Iterator it = c.iterator(); it.hasNext(); ) { // ... }奥秘在于客户端自身不维护遍历集合的“指针”，所有的内部状态（如当前元素位置，是否有下一个元素）都由 Iterator 来维护。而这个 Iterator 由集合类通过工厂方法生成，因此，它知道如何遍历整个集合。客户端从不直接和集合类打交道，它总是控制 Iterator，向它发送“向前”、“向后”、“取当前元素”的命令，就可以间接遍历整个集合。首先看看 java.util.Iterator 接口的定义：public interface Iterator { boolean hasNext(); Object next(); void remove(); }依赖前两个方法就能完成遍历，典型的代码如下：for(Iterator it = c.iterator(); it.hasNext(); ) { Object o = it.next(); // 对 o 的操作... }在 JDK 1.5 中，还对上面的代码在语法上作了简化（增强型 for 循环）：// Type 是具体的类型，如 String for(Type t : c) { // 对 t 的操作... }每一种集合类返回的 Iterator 具体类型可能不同，Array 可能返回 ArrayIterator，Set 可能返回 SetIterator，Tree 可能返回 TreeIterator。但是它们都实现了 Iterator 接口，因此，客户端不关心到底是哪种 Iterator，它只需要获得这个 Iterator 接口即可，这就是面向对象的威力。Iterator 源码剖析让我们来看看 AbstractList 如何创建 Iterator。首先 AbstractList 定义了一个内部类（inner class）：private class Itr implements Iterator { ... }而 iterator() 方法的定义是：public Iterator iterator() { return new Itr(); }因此客户端不知道它通过 Iterator it = a.iterator() 所获得的 Iterator 的真正类型。现在我们关心的是这个声明为 private 的 Itr 类是如何实现遍历 AbstractList 的：private class Itr implements Iterator { int cursor = 0; int lastRet = -1; int expectedModCount = modCount; }Itr 类依靠 3 个 int 变量（还有一个隐含的 AbstractList 的引用）来实现遍历：cursor：是下一次 next() 调用时元素的位置，第一次调用 next() 将返回索引为 0 的元素。lastRet：记录上一次游标所在位置，因此它总是比 cursor 少 1。expectedModCount：表示期待的 modCount 值，用来判断在遍历过程中集合是否被修改过。变量 cursor 和集合的元素个数决定 hasNext()：public boolean hasNext() { return cursor != size(); }方法 next() 返回的是索引为 cursor 的元素，然后修改 cursor 和 lastRet 的值：public Object next() { checkForComodification(); try { Object next = get(cursor); lastRet = cursor++; return next; } catch(IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } }AbstractList 包含一个 modCount 变量，它的初始值是 0，当集合每被修改一次时（调用 add，remove 等方法），modCount 加 1。因此，modCount 如果不变，表示集合内容未被修改。Itr 初始化时用 expectedModCount 记录集合的 modCount 变量，此后在必要的地方它会检测 modCount 的值：final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }如果 modCount 与一开始记录在 expectedModCount 中的值不等，说明集合内容被修改过，此时会抛出 ConcurrentModificationException。这个 ConcurrentModificationException 是 RuntimeException，不要在客户端捕获它。如果发生此异常，说明程序代码的编写有问题，应该仔细检查代码而不是在 catch 中忽略它。但是调用 Iterator 自身的 remove() 方法删除当前元素是完全没有问题的，因为在这个方法中会自动同步 expectedModCount 和 modCount 的值：public void remove() { ... AbstractList.this.remove(lastRet); ... // 在调用了集合的 remove() 方法之后重新设置了 expectedModCount expectedModCount = modCount; ... }要确保遍历过程顺利完成，必须保证遍历过程中不更改集合的内容（Iterator 的 remove() 方法除外）。因此，确保遍历可靠的原则是只在一个线程中使用这个集合，或者在多线程中对遍历代码进行同步。示例与总结最后给个完整的示例：Collection c = new ArrayList(); c.add("abc"); c.add("xyz"); for(Iterator it = c.iterator(); it.hasNext(); ) { String s = (String)it.next(); System.out.println(s); }如果你把第一行代码的 ArrayList 换成 LinkedList 或 Vector，剩下的代码不用改动一行就能编译，而且功能不变。这就是针对抽象编程的原则：对具体类的依赖性最小。版本说明：本文基于 JDK 1.4.2 源码分析，未涉及泛型特性。现代 Java 开发中建议使用泛型集合（如 List<String>）及增强型 for 循环，但迭代器底层原理（包括 fail-fast 机制）依然保持一致。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。1. 概述在缺乏必要同步的情况下，编译器、运行时或处理器可能会应用各种优化措施。尽管这些优化在大多数情况下是有益的，但有时它们也可能引发一些细微的问题。缓存（Caching）和指令重排序（Reordering）是在并发上下文中可能使我们感到惊讶的优化措施之一。Java 和 JVM 提供了多种方法来控制内存顺序，而 volatile 关键字就是其中一种。在本文中，我们将重点介绍 Java 语言中这个基本但经常被误解的概念——volatile 关键字。首先，我们将从底层计算机体系结构的工作背景入手，然后再熟悉 Java 中的内存顺序。2. 共享多处理器架构处理器负责执行程序指令。因此，它们需要从 RAM 中检索程序指令和所需的数据。由于 CPU 每秒能够执行大量指令，因此直接从 RAM 中获取数据并不是理想的选择。为了改善这种情况，处理器使用了一些技巧，例如乱序执行、分支预测、推测执行，当然还有缓存。以下是内存层次结构的作用示意图：当不同的内核执行更多的指令并处理更多的数据时，它们会用更多相关的数据和指令填充其缓存。这将以提高缓存一致性挑战为代价，换取整体性能的提升。简而言之，我们需要慎重考虑当一个线程更新缓存的值时会发生什么。3. 何时使用 volatile为了进一步扩展缓存一致性的概念，让我们从《Java 并发实践》（Java Concurrency in Practice）一书中借用一个示例：public class TaskRunner { private static int number; private static boolean ready; private static class Reader extends Thread { @Override public void run() { while (!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new Reader().start(); number = 42; ready = true; } }该 TaskRunner 类维护两个简单的变量。在它的 main 方法中，它创建另一个线程，只要 ready 变量为 false，就会在该变量上自旋（spin）。当变量变为 true 时，线程将打印 number 变量。许多人可能希望此程序在短暂的延迟后仅打印 42。但是，实际上，延迟可能会更长。它甚至可能永远挂起，甚至打印为零！这些异常的原因是缺乏适当的内存可见性和重排序。让我们对其进行更详细的评估。3.1 内存可见性 (Memory Visibility)在这个简单的示例中，我们有两个应用程序线程：主线程和阅读器线程。让我们想象一个场景，其中 OS 在两个不同的 CPU 内核上调度这些线程，其中：主线程在其核心高速缓存中具有 ready 和 number 变量的副本。阅读器线程也以其副本结尾。主线程更新缓存的值。在大多数现代处理器上，写请求在发出后不会立即应用。实际上，处理器倾向于将这些写操作在特殊的写缓冲区中排队。一段时间后，它们会将这些写入一次全部应用到主存储器。综上所述，当主线程更新 number 和 ready 变量时，无法保证阅读器线程会看到什么。换句话说，读取器线程可能会立即看到更新的值，或者有些延迟，或者根本看不到！这种内存可见性问题可能会在依赖可见性的程序中引起活跃性问题（Liveness Problem）。3.2 指令重排序 (Reordering)更糟的是，读取器线程可能会以实际程序顺序以外的任何顺序查看这些写入。例如，由于我们首先更新了 number 变量：public static void main(String[] args) { new Reader().start(); number = 42; ready = true; }我们可能希望读取器线程打印 42。但是，实际上有可能看到零作为打印值！重排序是一种用于提高性能的优化技术。有趣的是，不同的组件可能会应用此优化：处理器可以按程序顺序以外的任何顺序刷新其写缓冲区。处理器可能会应用乱序执行技术。JIT 编译器可以通过重排序进行优化。3.3 volatile 内存顺序为了确保变量的更新可预测地传播到其他线程，我们应该对这些变量应用 volatile 修饰符：public class TaskRunner { private volatile static int number; private volatile static boolean ready; // same as before }这样，我们与运行时和处理器进行通信，以不对任何涉及 volatile 变量的指令重新排序。而且，处理器知道应该立即刷新这些变量的所有更新。4. volatile 与线程同步对于多线程应用程序，我们需要确保一些规则以实现一致的行为：互斥（Mutual Exclusion）：一次只有一个线程执行关键部分。可见性（Visibility）：一个线程对共享数据所做的更改对其他线程可见，以保持数据一致性。同步的方法和块以应用程序性能为代价提供上述两个属性。volatile 是一个非常有用的关键字，因为它可以帮助确保数据更改的可见性，而无需提供互斥。因此，它在可以并行执行多个代码块的多个线程可以使用的地方很有用，但我们需要确保可见性属性。5. Happens-Before 顺序volatile 变量的内存可见性影响超出了 volatile 变量本身。更具体地说，假设线程 A 写入一个 volatile 变量，然后线程 B 读取相同的 volatile 变量。在这种情况下，写入 volatile 变量之前 A 可见的值，将在读取 volatile 变量之后对 B 可见：从技术上讲，对 volatile 字段的任何写入都发生在随后每次对该字段的读取之前。这是 Java 内存模型（JMM）的 volatile 变量规则。5.1 依赖传递 (Piggybacking)由于 Happens-Before 关系的强度（在进行内存排序之前），有时我们可以依赖另一个 volatile 变量的可见性属性。例如，在我们的特定示例中，我们只需要将 ready 变量标记为 volatile：public class TaskRunner { private static int number; // not volatile private volatile static boolean ready; // same as before }写入 ready 变量之前，任何真正的准备可变读取之后是什么可见的准备变量。因此，number 变量搭载在 ready 变量强制执行的内存可见性上。简而言之，即使它不是一个 volatile 变量，它也表现出一个 volatile 行为。通过使用这些语义，我们只能将类中的几个变量定义为 volatile 并优化可见性保证。6. 结论在本教程中，我们探索了关于 volatile 关键字及其功能的更多信息，以及从 Java 5 开始对其进行的改进。与往常一样，可以在 Github 上找到代码示例。说明：本文基于 Java 5 及以上版本的内存模型特性编写，具体行为请以当前 JDK 官方文档为准。
编者注：本文为历史博文归档；涉及 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 大牛) 在 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) World。Concurrent：多线程并发，GC 线程和 App 线程可同时运行。（注：Young generation 没有 CMS，取而代之的是可和 CMS (Old) 一起运行的 ParNew）问题 4：如何选择 Collector？Serial 可以直接排除掉，现在最普通的服务器也有双核 64 位/8G 内存，默认的 Collector 是 PS Scavenge 和 PS MarkSweep。所以 Collector 主要在并行 (Parallel) 和并发 (Concurrent) 两者之间选择。问题 5：选择的标准 (参数指标) 是什么？如何得到这些参数值 (How to measure it)？指标主要是 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. JconsoleJDK 自带工具。在 Java 启动参数中加入下面的语句（本文为 Tomcat 应用），然后在监控端可以远程连接 1090 端口。在内存一项，有累积 GC 时间和次数。注意在以 min 为单位显示时，只显示整数部分，如 1min20s 显示为 1min。CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.port=1090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"3. VisualGCJVM 监控工具，未同 JDK 一起发布，可在 JVisualvm (JDK 自带) 中以插件的方式使用，本文为独立使用。有累积 GC 时间和次数，并有曲线图直观显示。首先在 Server 端启动 jstatd：vi jstatd.all.policy grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; }; jstatd -J-Djava.security.policy=jstatd.all.policy然后在监控端启动 VisualGC，远程连接服务端进程 id：visualgc 102592@remote.domain问题 6：应用请求的吞吐量和响应是否可以反映 JVM 的性能？这正是我们调优的目标。本文使用 Jmeter 来做压力测试，并给出吞吐量和响应 report。测试硬件环境项目值项目值操作系统Linux 2.6.18-53.el5物理内存总量8,175,632 Kb体系结构amd64可用物理内存1,140,520 Kb处理器的数目2交换空间总量8,594,764 Kb分配的虚拟内存2,680,408 Kb可用交换空间8,594,680 KbTest case使用 Jmeter 压力测试。共 6 个 client，每个 client 启动 30 个线程发送请求。每个请求从 16 种测试样例中随机挑选一个，发送到 server 端。测试持续 10min。参数值server 使用默认 GC (PS Scavenge 和 PS MarkSweep)。server 使用 CMS (-XX:+UseConcMarkSweepGC -XX:+UseParNewGC)。server 使用 CMS，设置 Young generation 的大小为 200m (-Xmn200m)。server 使用 CMS，设置 Young generation 的大小为 600m (-Xmn600m)。观察值Jmeter 请求的 summary report。server 端累积 GC 时间和次数。测试结果1) CMS 和 Parallel 比较1.1) 吞吐量和响应 (PS Scavenge 和 PS MarkSweep) (ParNew 和 CMS)从 Jmeter 的 report 中可以看出，使用 CMS 后吞吐量 (对应总的请求数) 下降 18%，而最大响应时间 (包括最小响应时间) 有近 30% 的提升 (变小)。这验证了 Tony Printezis 在 Step-by-Step: Garbage Collection Tuning in the Java HotSpot™ Virtual Machine 中说的：使用 CMS 应用的吞吐量会相对下降，但有更好的最差响应时间。Expect longer young GC timesDue to slower allocations into the old genExpect better worst-case latenciesCMS does its work mostly-concurrentlyShorter worst-case pausesExpect lower throughputCMS does more work在官方的 JVM 性能调优中给出的建议也是：如果你的应用对峰值处理有要求，而对一两秒的停顿可以接受，则使用 -XX:+UseParallelGC；如果应用对响应有更高的要求，停顿最好小于一秒，则使用 -XX:+UseConcMarkSweepGC。1.2) GC 累积时间和次数 (PS Scavenge 和 PS MarkSweep) (ParNew 和 CMS)PS 累积 GC 时间 (visualgc) 为 1min25s，其中 Eden 189 次，共 52s；Old 13 次，共 33s。CMS 累积 GC (visualgc) 为 2min2s，其中 Eden 2333 次，共 1min46s；Old 55 次，共 16s。(Jconsole 和 GC log 却显示没有 Full GC，从 understanding cms gc logs 和 jstat 显示的 full GC 次数与 CMS 周期的关系 中我推测 visualgc 与 jstat 显示一致，都是统计 Old 的回收次数；而 Full GC 则是 Young 和 Old 一起回收，在其他类型的 GC 里，Old 只有 Full GC 时才触发)。可以看到 PS 的 GC 频率相对低，但每次 GC 时间长，每次 Full 在 3s 左右徘徊，Young 在 0.3s 左右；CMS 则是短频快，频繁快速回收，Young 在 0.03s (<0.1s) 左右，Old <0.5s。从 JMeter 上，使用 PS GC，Request Report 会有间歇性的停顿，即 server 没有任何响应；CMS 则相对较少，停顿不那么明显。2) CMS 下不同 Xmn 的比较由于 CMS Young 太多频繁，又测试了分别调整 Xmn 为 200m 和 600m 之后的结果。200m 是仿照 cassandra 中 100m * cpu # 来设置 Young gen 的大小；600m 则是与 PS 下的 Young gen 一致。 200m 600m随着 Young gen 的增大 (40m -> 200m -> 600m)，Young 的回收次数减少，Old 的回收次数增加，总体 GC 累积时间下降，应用吞吐量上升，最差响应时间变慢 (即便和 PS 比较也更差，是我的测试有问题？)。结论App 停顿 3s 是不可接受的，因此倾向于使用 CMS；CMS 的 default young gen 相当小，于是设置 Xmn。对于更加 Prefer 响应的应用，下面配置是否是黄金标配：JVM_OPTS="$JVM_OPTS -XX:+UseParNewGC" JVM_OPTS="$JVM_OPTS -XX:+UseConcMarkSweepGC" JVM_OPTS="$JVM_OPTS -XX:+CMSParallelRemarkEnabled" JVM_OPTS="$JVM_OPTS -XX:SurvivorRatio=8" JVM_OPTS="$JVM_OPTS -XX:MaxTenuringThreshold=1" JVM_OPTS="$JVM_OPTS -XX:CMSInitiatingOccupancyFraction=75" JVM_OPTS="$JVM_OPTS -XX:+UseCMSInitiatingOccupancyOnly"说明：本文涉及的技术内容基于较早期的 JDK 版本（如 JDK 6/7/8）。请注意，CMS 收集器在 JDK 9 中被标记为废弃，并在 JDK 14 中正式移除。现代 JDK 版本推荐使用 G1、ZGC 或 Shenandoah 等垃圾收集器。文中提到的部分工具（如 VisualGC 插件）在新版 JDK 或 IDE 中可能需要额外配置或已不再适用，请以官方最新文档为准。
面向对象编程：这里我说了算！自 9 岁拥有第一台 Commodore 64 家用电脑起，我就开始编程了。然而，面对“如何写出好代码”这个问题，我仍然感觉自己有很多需要学习的地方。在探索如何提高编程水平的过程中，我学习了很多种语言，其中大多数是以面向对象（Object-Oriented, OO）为主的。然而，令人惊讶的是，在我读过的大多数书本、杂志和网络文章中，存在着大量糟糕透顶的代码，但它们却被当作面向对象的范例。在这些代码中，我看到被违反得最频繁的原则是 “命令，不要去询问”（Tell, Don't Ask） 原则。这个原则的核心在于：一个对象应该命令其它对象该做什么，而不是去查询其它对象的状态来决定自己要做什么。查询其它对象的状态来决定做什么，也被称作 “功能嫉妒”（Feature Envy）。在面向对象编程中，一个对象被定义为由“对象状态”和“操作这个状态的方法”组成。在《Holub on Patterns: Learning Design Patterns By Looking At Code》这本书里，Allen Holub 在第一章中有一节的标题是“为什么 getter 和 setter 方法有害”。他在 JavaWorld 上的一篇文章里也谈论了这个问题。对所有的面向对象程序员来说，这应该是一篇“必读”文章。我有一些程序员同事，他们在为一个对象声明了属性后，第一步就是添加 getter 和 setter 方法。JavaBean 规范对于这种文化的推广负有很大责任。人们认为这是一种能让你写出可复用的模块化组件的好方法，但这已是很多年前的事了，时过境迁。编写带有 getter 和 setter 方法的类会导致过程式代码。通过 getter 和 setter 来获取数据进行操作的逻辑最终会遍布整个应用，进而经常导致应用内的重复（这违反了另外一个原则：DRY——不要自我重复 (Don't Repeat Yourself)）。这会致使产生很难维护的代码，当你对一个类做任何修改时，都会在整个应用内造成连锁式的牵连。用这种方式来暴露数据还会妨碍你重构你的类，因为对这样的属性的任何修改都意味着会影响到访问了这个属性的其它类。违反“命令，不要去询问”原则的另外一个副作用是，你的探询最终会变成严重依赖状态信息并带有很多前提条件。这会让人很难理解你究竟询问的是什么。你很可能会最终违反的第三个原则是，最少知识（Least Knowledge） 原则，也叫作 得墨忒耳定律（Law Of Demeter）。这个定律可以总结为下面一句话：一个类应该只跟它的直接朋友通话，不要跟陌生人说话。在类里面加入 getter 方法，你的代码最终会写成这样：if (person.getAddress().getCountry() == "Australia") {这违反了得墨忒耳定律，因为这个调用者跟 Person 过于亲密。它知道 Person 里有一个 Address，而 Address 里还有一个 country。它实际上应该写成这样：if (person.livesIn("Australia")) {这并不违反得墨忒耳定律，因为这个调用者只跟它的直接朋友 Person 通话，而且它不知道内部情况。从“命令，不要询问”的视角来看，这也是更好的做法，因为确定这个 person 是否居住在 Australia 的逻辑被隐藏到了 Person 里。如果我们改变 Person 里存储国家信息的方式，这将不会影响任何依赖这个信息的其它类。另外一个这样写代码的好处是，它把我们的意图显示得很清楚。这是我会在以后的时间里讨论的另外一个话题。为了避免违反“命令，不要去询问”原则，有几样事情你要记在心里：在 IDE 里敲击快捷生成键前，询问自己 ‘五个为什么’，问问自己为什么一上来就添加 getter 和 setter 方法。不要询问对象的状态。要做什么，告诉它们该怎么做。操作所属的对象拥有这些操作的数据。进一步阅读我的一个朋友是这个思想的大力倡导者，他用 East Oriented 方式精彩地解释了这个原则。《Tell Don't Ask and the effect on testing》，出自 Steve Freeman 和 Nat Pryce，他们也是《Growing Object-Oriented Software》、《Guided by Tests》两本书的作者。[英文原文：I give the orders around here! ]说明：本文原文发表于 2012 年左右，文中提到的部分观点（如 JavaBean 规范的影响）具有特定的时代背景，但核心设计原则（Tell, Don't Ask、得墨忒耳定律）在当今面向对象设计中依然适用。
Redis 系列教程导航如何在 Ubuntu 18.04 上安装和保护 Redis如何连接到 Redis 数据库如何管理 Redis 数据库和 Keys如何在 Redis 中管理副本和客户端如何在 Redis 中管理字符串如何在 Redis 中管理 List如何在 Redis 中管理 Hashes如何在 Redis 中管理 Sets如何在 Redis 中管理 Sorted Sets如何在 Redis 中运行事务如何使 Redis 中的 Key 失效如何解决 Redis 中的故障如何从命令行更改 Redis 的配置Redis 数据类型简介介绍Redis 是一个开源的内存键值存储系统。它最受欢迎的功能之一是对复制（Replication）的支持：任何 Redis 服务器都可以将其数据复制到任意数量的副本中，从而实现高读取可伸缩性和强大的数据冗余性。此外，Redis 设计允许许多客户端（默认情况下最多 10,000 个）连接数据并与之交互，因此对于许多用户需要访问同一数据集的情况而言，它是一个不错的选择。本教程介绍了用于管理 Redis 客户端和副本的命令。如何使用本指南本指南以备有完整示例的速查表（Cheatsheet）形式编写。我们鼓励您跳至与您要完成的任务相关的任何部分。本指南中显示的命令已在运行 Redis 版本 4.0.9 的 Ubuntu 18.04 服务器上进行了测试。要设置类似的环境，您可以按照我们的指南 如何在 Ubuntu 18.04 上安装和保护 Redis 的 步骤 1 进行操作。我们将通过使用 Redis 命令行界面 redis-cli 运行它们来演示这些命令的行为。请注意，如果您使用其他 Redis 界面（例如 Redli），则某些命令的确切输出可能会有所不同。另外，您可以提供一个托管的 Redis 数据库实例来测试这些命令，但是请注意，根据数据库提供者所允许的控制级别，本指南中的某些命令可能无法按所述方式工作。要配置 DigitalOcean 托管数据库，请遵循我们的 托管数据库产品文档。然后，您必须 安装 Redli 或 设置 TLS 隧道 才能通过 TLS 连接到托管数据库。注意： Redis 项目在其文档和各种命令中使用术语“主（Master）”和“从（Slave）”来标识复制中的不同角色，尽管该项目的参与者 正在采取措施 在不引起兼容性问题的情况下 更改此语言。DigitalOcean 通常更喜欢使用替代术语“主要（Primary）”和“副本（Replica）”。本指南将在可能的情况下默认设置为“主要”和“副本”，但请注意，在某些情况下不可避免地会出现“主”和“从”这两个术语。管理副本Redis 最具特色的功能之一是其 内置的复制功能。使用复制时，Redis 创建主实例的精确副本。这些辅助实例在其连接断开时会随时重新连接至主要实例，并且始终旨在保持主要实例的精确副本。如果不确定当前连接的 Redis 实例是主实例还是副本，则可以通过运行以下 role 命令进行检查：role如果您使用 Redis Sentinel，则此命令将返回 master 或 slave，或者可能返回 sentinel。要将 Redis 实例动态指定为另一个实例的副本，请运行 replicaof 命令。此命令将预期的主服务器的主机名或 IP 地址和端口作为参数：replicaof hostname_or_IP port如果服务器已经是另一台主服务器的副本，它将停止复制旧服务器，并立即开始与新服务器同步。它还将丢弃旧数据集。要将副本提升为主副本，请运行以下 replicaof 命令：replicaof no one这将阻止实例复制主服务器，但不会丢弃已复制的数据集。在原始主数据库失败的情况下，此语法很有用。在 replicaof no one 发生故障的主数据库的副本上运行之后，以前的副本可以用作新的主数据库，并拥有自己的副本作为故障保护。注意： 在 5.0.0 之前的版本中，Redis 包含了此命令的一个名为 slaveof 的版本。管理客户端一个 客户端) 是任何机器或软件连接到服务器才能访问服务。Redis 附带了一些命令，可帮助跟踪和管理客户端连接。该 client list 命令返回一组有关当前客户端连接的可读信息：client listOutput "id=18165 addr=[2001:db8:0:0::12]:47460 fd=7 name=jerry age=72756 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping id=18166 addr=[2001:db8:0:1::12]:47466 fd=8 name= age=72755 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=info id=19381 addr=[2001:db8:0:2::12]:54910 fd=9 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client "以下是每个字段的含义：字段含义id唯一的 64 位客户端 IDname客户端连接的名称，由先前的 client setname 命令定义addr客户端连接的地址和端口fd与客户端所连接的套接字相对应的 文件描述符age客户端连接的总时长，以秒为单位flags一组一个或多个单字符标志，可提供有关客户端的更详细的信息；有关更多详细信息，请参见 client list 命令文档db客户端连接到的当前数据库 ID 号（可以从 0 到 15）sub客户端订阅的频道数psub客户端的模式匹配订阅数multi客户端已在 事务中 排队的命令数（将显示 -1 客户端是否尚未开始事务或 0 是否仅已开始事务且未排队任何命令）qbuf客户端的查询缓冲区长度，0 这意味着它没有待处理的查询qbuf-free客户端查询缓冲区中的可用空间量，0 表示查询缓冲区已满obl客户端的输出缓冲区长度oll客户端的输出列表的长度，当客户端的缓冲区已满时，将在其中排队答复omem客户端的输出缓冲区使用的内存events客户端的文件描述符事件，可以是 r“可读”，w“可写”或两者兼有cmd客户端运行的最后一条命令设置客户端名称对于调试使用 Redis 的任何应用程序中的连接泄漏都很有用。每个新连接都将在开始时没有分配的名称，但 client setname 可用于为当前客户端连接创建一个新连接。尽管 Redis 通常将字符串长度限制为 512 MB，但是对客户端名称的长度没有限制。但是请注意，客户端名称不能包含空格：client setname elaine要检索客户端连接的名称，请使用以下 client getname 命令：client getnameOutput "elaine"要获取客户端的连接 ID，请使用以下 client id 命令：client id"19492"Redis 客户端 ID 永远不会重复，并且会单调递增。这意味着，如果一个客户端的 ID 大于另一个客户端，则它是在以后的时间建立的。阻止客户端并关闭客户端连接复制系统通常被描述为 同步 或 _异步_。同步复制：每当客户端添加或更改数据时，客户端都必须从一定数量的副本中收到某种确认，以使更改注册为已提交。这有助于防止节点发生数据冲突，但是这会增加延迟，因为客户端必须等待执行另一项操作，直到从一定数量的副本中收到回音为止。异步复制：一旦将数据写入本地存储，客户端就会看到确认操作已完成的确认。但是，这与副本实际写入数据的时间之间可能会有滞后。如果其中一个副本在写入更改之前失败，则该写入将永远丢失。因此，尽管异步复制允许客户端继续执行操作而没有等待副本引起的延迟，但它可能导致节点之间的数据冲突，并且可能需要数据库管理员方面的额外工作来解决这些冲突。由于注重性能和低延迟，Redis 默认情况下实现异步复制。但是，您可以使用该 wait 命令模拟同步复制。wait 在指定的时间（以毫秒为单位）内阻止当前客户端连接，直到所有先前的写命令成功传输并被指定数量的副本接受为止。此命令使用以下语法：wait number_of_replicas number_of_milliseconds例如，如果要在 30 毫秒的超时时间内至少 3 个副本注册所有先前的写入操作之前阻止客户端连接，则 wait 语法应如下所示：wait 3 30该 wait 命令返回一个整数，该整数表示已确认写入命令的副本数，即使并非每个副本都这样做：2要解除阻止以前通过 wait（brpop，或 xread）命令阻止的客户端连接，可以 client unblock 使用以下语法运行命令：client unblock client_id要暂时挂起当前连接到 Redis 服务器的每个客户端，可以使用以下 client pause 命令。这在需要以受控方式更改 Redis 设置的情况下很有用。例如，如果要将一个副本提升为主实例，则可以事先暂停每个客户端，以便升级副本并使客户端作为新的主实例连接到该副本，而不会丢失任何写操作。该 client pause 命令要求您指定要挂起客户端的时间（以毫秒为单位）。下面的示例将所有客户端暂停一秒钟：client pause 1000该 client kill 语法允许您基于许多不同的过滤器关闭单个连接或一组特定的连接。语法如下所示：client kill filter_1 value_1 ... filter_n value_n在 Redis 2.8.12 和更高版本中，可以使用以下过滤器：addr：允许您从指定的 IP 地址和端口关闭客户端连接client-id：允许您根据其唯一 ID 字段关闭客户端连接type：关闭一个给定类型，它可以是每一个客户端 normal，master，slave，或 pubsubskipme：此过滤器的值选项为 yes 和 no：如果 no 指定了，则调用该 client kill 命令的客户端不会被跳过，并且如果其他过滤器应用到该客户端，它将被杀死如果 yes 指定，则将跳过运行该命令的客户端，而 kill 命令将对该客户端无效。skipme 始终 yes 默认为结论本指南详细介绍了用于管理 Redis 客户端和副本的许多命令。如果您想在本指南中概述其他相关的命令、参数或过程，请在下面的评论中提出疑问或提出建议。有关 Redis 命令的更多信息，请参阅关于 如何管理 Redis 数据库的 系列教程。说明： 本文内容基于 Redis 4.0.9 版本及 Ubuntu 18.04 环境测试。请注意，Redis 5.0.0 及以上版本在复制命令上有所变更（如 slaveof 变更为 replicaof），部分命令行为在新版本中可能有所不同。
在项目开发过程中，如何保证代码质量？前期培训与规范制定 在项目初期进行定期的代码规范和最佳实践培训，明确高质量代码的标准（如可读性、可维护性、性能优化等）。通过实际案例分析，让团队成员深刻理解代码质量的重要性。在培训过程中，可以制定奖励机制，鼓励成员积极参与并养成良好的代码习惯。严格执行代码审查（Code Review） 在开发阶段，由经验丰富的开发人员对其他成员的代码进行审查，确保代码符合规范且满足功能与性能要求。对于发现的问题，要及时记录并进行跟踪，确保问题得到闭环解决。自动化检测工具的使用 积极推进代码质量深化检测工具的使用，在开发过程中进行自动化检测，及时发现潜在问题并修复。可以定期对检测工具的效果进行评估和优化，确保其能够准确地发现问题。请谈谈你对敏捷开发的理解？核心理念 敏捷开发是一种高度适应性和高效的开发方法。与传统瀑布流开发的线性流程不同，敏捷开发强调灵活性和快速响应变化。它将项目划分为一系列短周期的迭代（通常一到两周），每个迭代都致力于交付一个可工作的产品增量。每日站会（Daily Standup） 每日站会是团队沟通的重要环节，成员快速汇报昨日成果、今日计划及面临的障碍，确保信息流通畅。在我参与的一个移动应用开发项目中，每日站会让我们能够迅速协调资源，应对突发问题（如第三方接口不稳定），通过及时调整开发策略避免了进度延误。回顾会议（Retrospective） 每个迭代结束后，团队深入反思本阶段工作，分析优缺点。在一个企业级软件项目中，通过回顾会议我们发现测试环节存在沟通不畅的问题，于是制定了更明确的测试流程和沟通机制，在下一个迭代中显著提高了测试效率和质量。客户参与 敏捷开发注重客户参与。客户可以在每个迭代结束后看到实际的产品成果，及时提供反馈以调整需求方向。例如在一个电商平台项目中，客户在早期迭代中看到部分功能实现后提出优化建议，我们迅速响应并调整计划，使得最终产品更符合客户需求。实际落地与协作 在实际应用中，我们首先与客户密切合作确定产品愿景和高优先级需求，将项目分解为多个小的用户故事。团队采用 Scrum 框架，设立明确角色（产品负责人、开发团队、Scrum 主管）。开发过程中采用持续集成和持续部署（CI/CD），通过自动化测试保证质量，并利用即时通讯工具和协作平台保持高度协作。总结 总之，敏捷开发在实际项目中展现出了巨大优势，它提高了开发效率，增强了团队协作，更好地满足了客户需求，是一种非常有效的开发方法。如果项目进度落后了，你会采取什么措施？评估落后情况与原因 首先评估进度落后的具体时间和原因。如果落后时间不多，可以通过加班等方式追赶进度。同时分析导致落后的原因（任务分配不合理、技术难题或其他因素），以便采取针对性措施。如果是任务分配不合理，可以重新调整任务分配，让更合适的人去完成相应的任务；如果是技术难题，可以组织技术专家进行攻关。需求重新评估与调整 如果落后进度较多，需要对需求进行重新评估和调整。确定哪些是关键需求（必须按时上线），哪些需求可以延期。与相关方进行沟通，争取理解和支持。在调整需求的过程中，要确保不影响项目的整体目标和质量。你在团队中通常扮演什么角色？架构师角色 作为技术经理，一方面担任架构师，负责软件的架构设计，确保系统的稳定性、可扩展性和性能。在架构设计过程中，要充分考虑团队成员的技术水平和项目的实际需求，制定合理的架构方案。管理者角色 另一方面作为管理者，负责新员工的招聘、培训和人事管理，同时对团队的技术管理和代码质量进行监控。任务分配者 根据产品经理的需求，合理分配任务，确保项目顺利进行。在分配任务时，要充分了解团队成员的能力和特长，做到人尽其才。对于团队成员之间的技术分歧，你会如何处理？了解分歧点 首先了解团队成员之间的分歧点，认真听取双方的观点和理由。在了解分歧点的过程中，可以组织双方进行面对面的沟通，让他们充分表达自己的想法。分析与决策 根据公司现状和项目实际需求，分析双方方案的优缺点。如果分歧点不是关键因素，可以暂时搁置或寻找折中方案。如果双方方案都有优势，可以考虑融合形成一个更优方案。在融合方案的过程中，要充分尊重双方的意见，确保新方案能够得到双方的认可。达成一致 最后与团队成员进行沟通，达成统一意见，按照新方案进行实施。如果要提升团队的整体技术水平，你有哪些具体的计划？定期技术分享 组织技术分享会，让团队成员分享项目中遇到的技术难题和解决方案，促进知识共享。可以制定一个分享计划，确保每个成员都有机会进行分享；同时邀请外部专家进行技术讲座，拓宽团队视野。代码评审会议 在项目上线后，组织代码评审会议，对代码进行审查和评估，发现问题并及时改进。可以建立一个代码评审规范，确保评审的质量和效果。外部培训支持 鼓励团队成员参加外部培训和技术研讨会，提升技术水平。可以为团队成员提供一定的培训经费和时间支持。建立交流平台 建立一个技术交流平台，让团队成员可以随时交流技术问题和经验。可以使用在线工具或者内部论坛等方式建立交流平台。如果团队中有成员工作积极性不高，你会怎么做？了解原因并提供支持 首先了解成员工作积极性不高的原因。如果是家庭问题，给予关心和支持；如果是对公司制度不满，沟通具体意见并反馈上级争取改进；如果是薪资问题，坦诚沟通期望薪资并根据表现和市场行情评估。如果确实存在薪资不合理的情况，可以向上级领导提出调整薪资的建议。制定个人发展计划 如果成员对技术水平和职业发展感到迷茫，为他们制定个人发展计划，明确方向和目标，并提供相应培训和支持。可以与成员一起制定个人发展计划，确保计划的可行性和有效性。建立激励机制 建立激励机制，对表现优秀的成员进行奖励。可以设立一些奖项（如技术创新奖、优秀团队奖等），对表现突出的成员进行表彰和奖励。如果有一个新的技术框架出现，你会如何评估它是否适合引入团队？技术调研 首先了解新技术框架的原理、特点和优势。通过查阅资料、参加研讨会等方式，深入了解应用场景和适用范围。可以组织团队成员一起进行技术调研，共同评估新框架的可行性。Demo 测试 进行 Demo 测试，验证新框架的功能和性能。关注与现有系统的兼容性、可扩展性和可维护性等问题。可以制定一个详细的测试计划，确保测试的全面性和有效性。小项目试用 选择一个小的项目或影响不大的项目进行试用，观察实际应用效果。在试用过程中，要及时收集团队成员的反馈意见，对新框架进行优化和改进。风险与成本评估 评估新框架的风险和成本（包括学习成本、迁移成本、维护成本等）。可以与团队成员一起进行风险评估，制定相应的风险应对措施。在项目开发过程中，如何平衡技术创新和项目进度的关系？确保进度优先 首先要确保项目进度的顺利进行。按照项目计划合理分配任务，确保按时交付。在分配任务时，要充分考虑团队成员的技术水平和项目的实际需求，避免因为技术难题而影响进度。鼓励适度创新 在保证项目进度的前提下，鼓励团队成员进行技术创新。为团队提供一定的时间和资源，尝试新的技术和方法。可以设立一个创新基金，为团队成员的技术创新提供资金支持。风险评估 在进行技术创新时，充分评估创新的风险和收益，确保不会对项目进度和质量造成重大影响。可以组织技术专家进行风险评估，制定相应的风险应对措施。创新激励 建立创新激励机制，对表现优秀的团队成员进行奖励。可以设立一些创新奖项（如技术创新奖、最佳实践奖等），对表现突出的团队成员进行表彰和奖励。说明：本文内容主要基于通用软件工程管理实践与敏捷开发方法论，适用于大多数 Java 技术团队的管理场景。具体实施时请结合团队规模、业务类型及公司文化进行调整。
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) { System.out.println("Hello, World!"); } }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），接着是常量池中项的总数。项总数（本例中，是 26，或 0x001a）后面是实际的常量池数据。这里放着类定义所用的所有常量。它包括类名和方法名、特征符以及字符串（您可以在十六进制转储右侧的文本解释中识别它们），还有各种二进制值。常量池中各项的长度是可变的，每项的第一个字节标识项的类型以及对它解码的方式。这里我不详细探究所有这些内容的细节，如果感兴趣，有许多可用的参考资料，从实际的 JVM 规范开始。关键之处在于常量池包含对该类所用的其它类和方法的所有引用，还包含了该类及其方法的实际定义。常量池往往占到二进制类大小的一半或更多，但平均下来可能要少一些。常量池后面还有几项，它们引用了类本身、其超类以及接口的常量池项。这些项后面是有关字段和方法的信息，它们本身用复杂结构表示。方法的可执行代码以包含在方法定义中的 代码属性 的形式出现。用 JVM 的指令形式表示该代码，一般称为 字节码，这是下一节要讨论的主题之一。在 Java 类格式中，属性 被用于几个已定义的用途，包括已提到的字节码、字段的常量值、异常处理以及调试信息。但是属性并非只可能用于这些用途。从一开始，JVM 规范就已经要求 JVM 忽略未知类型的属性。这一要求所带来的灵活性使得将来可以扩展属性的用法以满足其它用途，例如提供使用用户类的框架所需的元信息，这种方法在 Java 派生的 C# 语言中已广泛使用。遗憾的是，对于在用户级利用这一灵活性还没有提供任何挂钩。字节码和堆栈构成类文件可执行部分的字节码实际上是针对特定类型的计算机 — JVM — 的机器码。它被称为 虚拟 机，因为它被设计成用软件来实现，而不是用硬件来实现。每个用于运行 Java 平台应用程序的 JVM 都是围绕该机器的实现而被构建的。这个虚拟机实际上相当简单。它使用堆栈体系结构，这意味着在使用指令操作数之前要先将它们装入内部堆栈。指令集包含所有的常规算术和逻辑运算，以及条件转移和无条件转移、装入／存储、调用／返回、堆栈操作和几种特殊类型的指令。有些指令包含立即操作数值，它们被直接编码到指令中。其它指令直接引用常量池中的值。尽管虚拟机很简单，但实现却并非如此。早期的（第一代）JVM 基本上是虚拟机字节码的解释器。这些虚拟机实际上 的确 相对简单，但存在严重的性能问题 — 解释代码的时间总是会比执行本机代码的时间长。为了减少这些性能问题，第二代 JVM 添加了 即时（just-in-time，JIT）转换。在第一次执行 Java 字节码之前，JIT 技术将它编译成本机代码，从而对于重复执行提供了更好的性能。当代 JVM 的性能甚至还要好得多，因为使用了适应性技术来监控程序的执行并有选择地优化频繁使用的代码。装入类诸如 C 和 C++ 这些编译成本机代码的语言通常在编译完源代码之后需要链接这个步骤。这一链接过程将来自独立编译好的各个源文件的代码和共享库代码合并起来，从而形成了一个可执行程序。Java 语言就不同。使用 Java 语言，由编译器生成的类在被装入到 JVM 之前通常保持原状。即使从类文件构建 JAR 文件也不会改变这一点 — JAR 只是类文件的容器。链接类不是一个独立步骤，它是在 JVM 将这些类装入到内存时所执行作业的一部分。在最初装入类时这一步会增加一些开销，但也为 Java 应用程序提供了高度灵活性。例如，在编写应用程序以使用接口时，可以到运行时才指定其实际实现。这个用于组装应用程序的 后联编 方法广泛用于 Java 平台，servlet 就是一个常见示例。JVM 规范中详细描述了装入类的规则。其基本原则是只在需要时才装入类（或者至少看上去是这样装入 — JVM 在实际装入时有一些灵活性，但必须保持固定的类初始化顺序）。每个装入的类都可能拥有其它所依赖的类，所以装入过程是递归的。清单 2 中的类显示了这一递归装入的工作方式。Demo 类包含一个简单的 main 方法，它创建了 Greeter 的实例，并调用 greet 方法。Greeter 构造函数创建了 Message 的实例，随后会在 greet 方法调用中使用它。清单 2. 类装入演示的源代码public class Demo { public static void main(String[] args) { System.out.println("**beginning execution**"); Greeter greeter = new Greeter(); System.out.println("**created Greeter**"); greeter.greet(); } } public class Greeter { private static Message s_message = new Message("Hello, World!"); public void greet() { s_message.print(System.out); } } public class Message { private String m_text; public Message(String text) { m_text = text; } public void print(java.io.PrintStream ps) { ps.println(m_text); } }在 java 命令行上设置参数 -verbose:class 会打印类装入过程的跟踪记录。清单 3 显示了使用这一参数运行清单 2 程序的部分输出：清单 3. -verbose:class 的部分输出[Opened /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Opened /usr/java/j2sdk1.4.1/jre/lib/sunrsasign.jar] [Opened /usr/java/j2sdk1.4.1/jre/lib/jsse.jar] [Opened /usr/java/j2sdk1.4.1/jre/lib/jce.jar] [Opened /usr/java/j2sdk1.4.1/jre/lib/charsets.jar] [Loaded java.lang.Object from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Loaded java.io.Serializable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Loaded java.lang.Comparable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Loaded java.lang.CharSequence from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Loaded java.lang.String from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] ... [Loaded java.security.Principal from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Loaded java.security.cert.Certificate from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Loaded Demo] **beginning execution** [Loaded Greeter] [Loaded Message] **created Greeter** Hello, World! [Loaded java.util.HashMap$KeySet from /usr/java/j2sdk1.4.1/jre/lib/rt.jar] [Loaded java.util.HashMap$KeyIterator from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]这只列出了输出中最重要的部分 — 完整的跟踪记录由 294 行组成，我删除了其中大部分，形成了这个清单。最初的一组类装入（本例中是 279 个）都是在尝试装入 Demo 类时触发的。这些类是每个 Java 程序（不管有多小）都要使用的核心类。即使删除 Demo main 方法的所有代码也不会影响这个初始的装入顺序。但是不同版本的类库所涉及的类数量和名称都不同。在上面这个清单中，装入 Demo 类之后的部分更有趣。这里的顺序显示了只有在准备创建 Greeter 类的实例时才会装入该类。不过，Greeter 类使用了 Message 类的静态实例，所以在可以创建 Greeter 类的实例之前，还必须先装入 Message 类。在装入并初始化类时，JVM 内部会完成许多操作，包括解码二进制类格式、检查与其它类的兼容性、验证字节码操作的顺序以及最终构造 java.lang.Class 实例来表示新类。这个 Class 对象成了 JVM 创建新类的所有实例的基础。它还是已装入类本身的标识 — 对于装入到 JVM 的同一个二进制类，可以有多个副本，每个副本都有其自己的 Class 实例。即使这些副本都共享同一个类名，但对 JVM 而言它们都是独立的类。非常规类路径装入到 JVM 的类是由 类装入器（ClassLoader）控制的。JVM 中构建了一个 引导程序 类装入器，它负责装入基本的 Java 类库类。这个特殊的类装入器有一些专门的特性。首先，它只装入在引导类路径上找到的类。因为这些是可信的系统类，所以引导程序装入器跳过了对常规（不可信）类所做的大量验证。引导程序不是唯一的类装入器。对于初学者而言，JVM 为装入标准 Java 扩展 API 中的类定义了一个 扩展 类装入器，并为装入一般类路径上的类（包括应用程序类）定义了一个 系统 类装入器。应用程序还可以定义它们自己的用于特殊用途（例如运行时类的重新装入）的类装入器。这样添加的类装入器派生自 java.lang.ClassLoader 类（可能是间接派生的），该类对从字节数组构建内部类表示（java.lang.Class 实例）提供了核心支持。每个构造好的类在某种意义上是由装入它的类装入器所“拥有”。类装入器通常保留它们所装入类的映射，从而当再次请求某个类时，能通过名称找到该类。每个类装入器还保留对父类装入器的引用，这样就定义了类装入器树，树根为引导程序装入器。在需要某个特定类的实例（由名称来标识）时，无论哪个类装入器最初处理该请求，在尝试直接装入该类之前，一般都会先检查其父类装入器。如果存在多层类装入器，那么会递归执行这一步，所以这意味着通常不仅在装入该类的类装入器中该类是 可见 的，而且对于所有后代类装入器也都是可见的。这还意味着如果一条链上有多个类装入器可以装入某个类，那么该树最上端的那个类装入器会是实际装入该类的类装入器。在许多环境中，Java 程序会使用多个应用程序类装入器。J2EE 框架就是一个示例。该框架装入的每个 J2EE 应用程序都需要拥有一个独立的类装入器以防止一个应用程序中的类干扰其它应用程序。该框架代码本身也将使用一个或多个其它类装入器，同样用来防止对应用程序产生的或来自应用程序的干扰。整个类装入器集合形成了树状结构的层次结构，在其每个层次上都可装入不同类型的类。装入器树作为类装入器层次结构的实际示例，图 1 显示了 Tomcat servlet 引擎定义的类装入器层次结构。这里 Common 类装入器从 Tomcat 安装的某个特定目录的 JAR 文件进行装入，旨在用于在服务器和所有 Web 应用程序之间共享代码。Catalina 装入器用于装入 Tomcat 自己的类，而 Shared 装入器用于装入 Web 应用程序之间共享的类。最后，每个 Web 应用程序有自己的装入器用于其私有类。图 1. Tomcat 类装入器在这种环境中，跟踪合适的装入器以用于请求新类会很混乱。为此，在 Java 2 平台中将 setContextClassLoader 方法和 getContextClassLoader 方法添加到了 java.lang.Thread 类中。这些方法允许该框架设置类装入器，使得在运行每个应用程序中的代码时可以将类装入器用于该应用程序。能装入独立的类集合这一灵活性是 Java 平台的一个重要特性。尽管这个特性很有用，但是它在某些情况中会产生混淆。一个令人混淆的方面是处理 JVM 类路径这样的老问题。例如，在图 1 显示的 Tomcat 类装入器层次结构中，由 Common 类装入器装入的类决不能（根据名称）直接访问由 Web 应用程序装入的类。使这些类联系在一起的唯一方法是通过使用这两个类集都可见的接口。在这个例子中，就是包含由 Java servlet 实现的 javax.servlet.Servlet。无论何种原因在类装入器之间移动代码时都会出现问题。例如，当 J2SE 1.4 将用于 XML 处理的 JAXP API 移到标准分发版中时，在许多环境中都产生了问题，因为这些环境中的应用程序以前是依赖于装入它们自己选择的 XML API 实现的。使用 J2SE 1.3，只要在用户类路径中包含合适的 JAR 文件就可以解决该问题。在 J2SE 1.4 中，这些 API 的标准版现在位于扩展的类路径中，所以它们通常将覆盖用户类路径中出现的任何实现。使用多个类装入器还可能引起其它类型的混淆。图 2 显示了 类身份危机（class identity crisis） 的示例，它是在两个独立类装入器都装入一个接口及其相关的实现时产生的危机。即使接口和类的名称和二进制实现都相同，但是来自一个装入器的类的实例不能被认为是实现了来自另一个装入器的接口。图 2 中通过将接口类 I 移至 System 类装入器的空间就可以解除这种混淆。类 A 仍然有两个独立的实例，但它们都实现了同一个接口 I。图 2. 类身份危机结束语Java 类定义和 JVM 规范一起为运行时组装代码定义了功能极其强大的框架。通过使用类装入器，Java 应用程序能使用多个版本的类，否则这些类就会引起冲突。类装入器的灵活性甚至允许动态地重新装入已修改的代码，同时应用程序继续执行。这里，Java 平台灵活性在某种程度上是以启动应用程序时较高的开销作为代价的。在 JVM 可以开始执行甚至最简单的应用程序代码之前，它都必须装入数百个独立的类。相对于频繁使用的小程序，这个启动成本通常使 Java 平台更适合于长时间运行的服务器类型的应用程序。服务器应用程序还最大程度地受益于代码在运行时进行组装这种灵活性，所以对于这种开发，Java 平台正日益受宠也就不足为奇了。在本系列文章的第 2 部分中，我将介绍使用 Java 平台动态基础的另一个方面：反射 API（Reflection API）。反射使执行代码能够访问内部类信息。这可能是构建灵活代码的极佳工具，可以不使用类之间任何源代码链接就能够在运行时将代码挂接在一起。但象使用大多数工具一样，您必须知道何时及如何使用它以获得最大利益。请阅读 Java 编程的动态性 第 2 部分以了解有效反射的诀窍和利弊。相关主题您可以参阅本文在 developerWorks 全球站点上的 英文原文.直接到 The Java Virtual Machine Specification 的出处，以了解二进制类格式、类的装入以及实际的 Java 字节码等细节。Martyn Honeyford 广受欢迎的"衡量 Java 本机编译"一文（developerWorks，2002 年 1 月）提供了有关 Java 语言本机代码编译问题及利弊的更多详细信息。二进制类格式包含大量重要的信息，通常这些信息甚至足够让您重新构造源代码（注释除外）。在 Greg Travis 的"How to lock down your Java code (or open up someone else's)"一文（developerWorks，2001 年 5 月）中，他向您显示了可以如何使用这些信息。获取有关 Jikes Research Virtual Machine (RVM) 的细节，它是用 Java 语言实现的，并是自我托管的（即，它的 Java 代码是依靠自身运行的，不需要另一个虚拟机）。通过 Java 规范请求 175（Java Specification Request 175，JSR 175）的 A Metadata Facility for the Java Programming Language，紧跟使属性可用于 Java 开发人员的发展。了解 Apache Software Foundation 的 Apache Tomcat Java 语言 Web 服务器项目的细节，包括 Tomcat 类装入器用法 的细节。说明本文基于较早期的 Java 版本（如 J2SE 1.4）及当时的技术环境（如 Tomcat 4.1）编写。文中提到的类装入器层次结构、JVM 内部细节及性能优化机制在现代 JVM（如 Java 8+）中可能有所演进，但核心概念依然适用。部分外部链接可能因年代久远而失效。
深度剖析Java内存管理：泄漏、溢出与优化
一、引言在 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) { staticList.add(obj); } public static void main(String[] args) { for (int i = 0; i < 100000; i++) { addObject(new Object()); } // 这里模拟程序运行一段时间后，忘记移除部分对象 staticList.remove(0); // 即使调用了 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) { JFrame frame = new JFrame("Listener Memory Leak Example"); JButton button = new JButton("Click Me"); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Button clicked!"); } }); frame.add(button); frame.pack(); frame.setVisible(true); // 这里模拟界面关闭，但未注销监听器 frame.dispose(); // 由于监听器未被注销，它所引用的对象（包括匿名内部类实例）无法被垃圾回收，导致内存泄漏 } }3. 资源未关闭的隐患当程序打开文件、数据库连接、网络连接等外部资源时，如果在使用完毕后没有及时关闭，不仅会造成资源泄漏，还可能导致内存泄漏。因为这些资源对象通常会占用一定的内存空间，并且可能持有对其他对象的引用，使得相关对象无法被回收。例如，在一个数据库操作的程序中，如果忘记关闭数据库连接，那么连接对象及其相关的资源（如 Statement、ResultSet 等）将一直占用内存，直到程序结束。以下是一个简单的 JDBC 操作示例，展示了数据库连接未关闭可能导致的内存泄漏问题：import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class DatabaseConnectionMemoryLeak { public static void main(String[] args) { Connection connection = null; try { connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password"); Statement statement = connection.createStatement(); // 执行一些数据库操作 statement.executeUpdate("INSERT INTO users (name, age) VALUES ('John', 30)"); // 这里模拟忘记关闭连接和语句对象 } catch (SQLException e) { e.printStackTrace(); } // 由于连接未关闭，连接对象及其相关资源无法被垃圾回收，导致内存泄漏 } }4. 内部类的潜在风险内部类持有对外部类实例的隐式引用。如果内部类的生命周期比外部类长，并且内部类对象无法被回收，那么外部类对象也会被间接持有，无法被垃圾回收。例如，在一个多线程应用中，如果在主线程中创建了一个内部类对象，并将其提交到线程池中执行，而线程池中的线程一直运行，那么内部类对象及其引用的外部类对象将一直存在于内存中，即使主线程已经结束。以下是一个简单的多线程示例，展示了内部类可能导致的内存泄漏问题：import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class InnerClassMemoryLeak { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(1); OuterClass outerClass = new OuterClass(); executorService.submit(new OuterClass.InnerClass(outerClass)); // 这里模拟主线程结束，但线程池中的线程仍在运行 outerClass = null; // 由于内部类对象在线程池中执行，它持有对外部类对象的引用，即使将外部类对象设置为 null， // 外部类对象也无法被垃圾回收，导致内存泄漏 } } class OuterClass { private int value = 10; class InnerClass implements Runnable { private OuterClass outer; InnerClass(OuterClass outer) { this.outer = outer; } @Override public void run() { while (true) { System.out.println(outer.value); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }（三）内存泄漏的检测工具与方法1. JProfiler 的强大功能JProfiler 是一款功能强大的 Java 性能分析工具，它提供了丰富的功能来检测内存泄漏。通过实时监控内存使用情况，它可以帮助开发者快速定位内存泄漏的根源。它能够详细展示内存中对象的分布、引用关系以及对象的创建和销毁过程，让开发者清晰地了解内存的使用状况。例如，在一个复杂的企业级应用中，JProfiler 可以帮助开发者找出那些长时间存活且占用大量内存的对象，以及它们的引用路径，从而确定是否存在内存泄漏。以下是使用 JProfiler 检测内存泄漏的一般步骤：启动 JProfiler 并配置要分析的 Java 应用程序。在应用程序运行过程中，JProfiler 会实时收集内存使用数据。通过查看内存视图，如对象分配图、引用树等，分析内存使用情况，查找可能存在内存泄漏的对象。根据分析结果，定位到导致内存泄漏的代码位置，并进行修复。2. VisualVM 的便捷使用VisualVM 是 JDK 自带的一款性能分析工具，它集成了多种功能，方便开发者对 Java 应用程序进行监控和分析。在检测内存泄漏方面，VisualVM 可以提供实时的堆内存使用情况、类加载信息等。它还支持生成堆转储文件（Heap Dump），以便在应用程序出现问题后进行离线分析。例如，在开发一个 Web 应用时，VisualVM 可以帮助开发者监控应用的内存使用趋势，当发现内存持续增长且无法释放时，可以生成堆转储文件，进一步分析是否存在内存泄漏。以下是使用 VisualVM 检测内存泄漏的基本流程：启动 Java 应用程序后，打开 VisualVM。在 VisualVM 中找到要分析的应用程序进程，连接到该进程。选择“监视”选项卡，查看实时的内存使用情况，如堆内存使用量、已加载的类数量等。如果怀疑存在内存泄漏，可以点击“堆 Dump"按钮生成堆转储文件。使用 VisualVM 的分析功能，如“类”视图、“实例”视图等，分析堆转储文件，查找内存泄漏的线索。3. MAT 的深入分析Eclipse Memory Analyzer Tool（MAT）是一款专门用于分析 Java 堆转储文件的强大工具。它可以帮助开发者深入了解堆内存中的对象布局、引用关系以及对象的内存占用情况。MAT 能够自动检测内存泄漏的嫌疑点，并提供详细的报告和分析结果。例如，在分析一个大型的分布式系统的内存问题时，MAT 可以快速定位到那些占用大量内存且无法被回收的对象，通过分析对象的引用链，找出导致内存泄漏的原因。以下是使用 MAT 分析内存泄漏的典型步骤：获取 Java 应用程序的堆转储文件（可以使用 JProfiler、VisualVM 等工具生成）。打开 MAT，选择"File" -> "Open Heap Dump"，加载堆转储文件。MAT 会自动分析堆转储文件，并提供一系列的分析报告和视图。在"Leak Suspects"报告中，查看可能存在内存泄漏的嫌疑点，分析相关对象的引用关系和内存占用情况。根据分析结果，深入研究对象的引用链，找到导致内存泄漏的根源，并进行修复。（四）内存泄漏的修复策略1. 及时释放资源在使用完文件、数据库连接、网络连接等资源后，务必及时关闭。可以使用 try-finally 语句块来确保资源的正确关闭，无论程序是否发生异常。例如：import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class ResourceRelease { public static void main(String[] args) { FileInputStream fis = null; try { File file = new File("test.txt"); fis = new FileInputStream(file); // 读取文件内容 byte[] buffer = new byte[(int) file.length()]; fis.read(buffer); System.out.println(new String(buffer)); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }2. 正确管理集合类避免在静态集合中存储不必要的对象，或者在对象不再使用时及时从集合中移除。同时，注意集合的容量管理，避免过度分配内存。例如，在使用 ArrayList 时，如果预知集合的大致大小，可以在创建时指定初始容量，以减少动态扩容带来的性能开销和内存浪费。import java.util.ArrayList; import java.util.List; public class CollectionManagement { public static void main(String[] args) { List<Object> list = new ArrayList<>(100); // 指定初始容量为 100 for (int i = 0; i < 50; i++) { list.add(new Object()); } // 当对象不再需要时，及时从集合中移除 list.clear(); } }3. 注销监听器和回调在对象不再需要接收事件通知或回调时，务必注销相应的监听器或回调方法。例如，在一个 Android 应用中，当一个 Activity 被销毁时，应该注销所有在该 Activity 中注册的监听器，以防止内存泄漏。import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; public class ListenerUnregistration extends Activity { private Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 处理按钮点击事件 } }); } @Override protected void onDestroy() { super.onDestroy(); button.setOnClickListener(null); // 注销监听器 } }4. 优化内部类使用如果内部类的生命周期可能比外部类长，考虑将内部类改为静态内部类，并避免在内部类中持有外部类的非静态引用。如果需要访问外部类的成员，可以通过构造函数传递必要的参数。例如：import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class InnerClassOptimization { private int value = 10; public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(1); OuterClass outerClass = new OuterClass(); executorService.submit(new OuterClass.StaticInnerClass(outerClass.value)); outerClass = null; } } class OuterClass { private int value = 10; static class StaticInnerClass implements Runnable { private int outerValue; StaticInnerClass(int outerValue) { this.outerValue = outerValue; } @Override public void run() { while (true) { System.out.println(outerValue); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }三、Java 内存溢出的全面解读（一）内存溢出的类型1. 堆溢出（OutOfMemoryError: Java heap space）堆是 Java 程序中用于存储对象实例的区域。当创建的对象数量过多，或者对象占用的内存过大，导致堆内存无法满足需求时，就会发生堆溢出。这种情况通常发生在程序中存在大量临时对象或者缓存数据没有及时清理的情况下。例如，在一个处理大数据集的程序中，如果一次性将所有数据加载到内存中，而不考虑内存的限制，就很容易引发堆溢出。2. 永久代溢出（OutOfMemoryError: PermGen space）永久代主要用于存储类的元数据信息，如类名、方法名、字段名等。在 Java 8 之前，如果应用程序加载了大量的类，或者动态生成了大量的代理类，可能会导致永久代溢出。例如，在一个使用了大量框架和库的 Web 应用中，由于框架会动态生成很多类，可能会超出永久代的内存限制。需要注意的是，Java 8 中已经将永久代移除，取而代之的是元空间（Metaspace），元空间使用本地内存，其内存管理方式与永久代有所不同。3. 栈溢出（StackOverflowError）栈用于存储方法调用的栈帧，每个方法调用都会在栈上创建一个栈帧，用于存储局部变量、方法参数、返回地址等信息。如果方法调用的层级过深，或者递归调用没有正确的终止条件，就会导致栈溢出。例如，以下递归方法会导致栈溢出：public class StackOverflowExample { public static void recursiveMethod() { recursiveMethod(); } public static void main(String[] args) { recursiveMethod(); } }（二）内存溢出的原因分析1. 内存需求超出预期在设计和开发程序时，如果对数据量和内存需求估计不足，可能会导致内存溢出。例如，在开发一个图像处理程序时，如果没有考虑到处理高清图像可能需要大量的内存，而直接将图像数据全部加载到内存中进行处理，就容易引发内存溢出。2. 内存泄漏的累积效应如前所述，内存泄漏会导致内存资源逐渐被占用，最终可能引发内存溢出。即使单个内存泄漏的对象占用的内存较小，但随着时间的推移，泄漏的对象越来越多，就会消耗大量的内存，使得可用内存越来越少，直到无法满足程序的正常需求。3. 不合理的内存配置JVM 的内存配置参数（如 -Xms、-Xmx、-XX:PermSize、-XX:MaxPermSize 等）设置不合理也可能导致内存溢出。如果初始堆内存设置过小，程序在运行过程中可能很快就会耗尽内存；如果最大堆内存设置过小，当程序需要更多内存时无法扩展，也会导致内存溢出。例如，在一个需要处理大量并发请求的 Web 应用中，如果将最大堆内存设置得过小，在高并发情况下就容易出现内存溢出。（三）内存溢出的解决方案1. 优化算法和数据结构选择更高效、更节省内存的算法和数据结构是应对内存溢出的关键一招。例如，处理海量数据时，原本使用数组存储数据可能因连续内存空间需求大而致溢出，此时若换成链表，它动态分配内存的特性，便能灵活存储数据，大幅削减内存压力；又或是在频繁查找场景里，摒弃普通数组改用哈希表，查找效率可从线性提至常数级，还能避免不必要内存占用。拿电商订单数据处理举例，若要统计各地区订单数量，起初用简单数组存订单，遍历查找对应地区订单极为耗时且占大量内存，代码如下：// 用数组存储订单信息，假设 Order 类包含地区信息等属性 Order[] orders = new Order[100000]; // 模拟填充订单数据 for (int i = 0; i < orders.length; i++) { orders[i] = new Order("地区" + i % 10); } // 统计各地区订单数量，低效且耗内存 int[] regionCount = new int[10]; for (Order order : orders) { int regionIndex = Integer.parseInt(order.getRegion().substring(2)); regionCount[regionIndex]++; }优化为使用 HashMap，内存利用更合理，效率飞升：import java.util.HashMap; import java.util.Map; // 使用 HashMap 统计订单数量 Map<String, Integer> regionCountMap = new HashMap<>(); for (Order order : orders) { String region = order.getRegion(); regionCountMap.put(region, regionCountMap.getOrDefault(region, 0) + 1); }2. 调整 JVM 内存参数精准调整 JVM 内存参数是治内存溢出“硬伤”的良方。-Xms 设定初始堆大小，-Xmx 限制最大堆大小。开发测试环境里，遇堆溢出，适度上调 -Xmx，给程序充足“施展空间”；生产环境则需权衡硬件资源、并发量谨慎配置。以 Tomcat 部署的 Web 应用为例，启动脚本里设参数：java -Xms512m -Xmx2g -jar myapp.jar，初始给 512M 堆内存，上限 2G，防程序初始内存浪费，也撑住高峰负载。但参数调整非万能，还得结合代码优化，不然只是延缓而非根治问题。3. 采用缓存策略缓存是把“双刃剑”，用好能降内存开销、提程序性能。像频繁查询数据库场景，引入本地缓存（如 Guava Cache）存常用数据，减少数据库查询，节省内存。可缓存管理不当，数据过期不清理、无限制存储，反而成内存溢出“帮凶”。示例代码展示 Guava Cache 使用：import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.TimeUnit; // 创建缓存，有效期 5 分钟，最大存 1000 条数据 Cache<String, Object> dataCache = CacheBuilder.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(1000) .build(); // 存入缓存 dataCache.put("key", "value"); // 从缓存读取 Object cachedValue = dataCache.getIfPresent("key");缓存要依数据访问频率、时效性等特性精细规划，定期清理过期数据、限制容量，让其助力而非拖累系统。4. 数据分批处理数据“暴饮暴食”易撑爆内存，分批处理是“细嚼慢咽”的巧法。大数据集读写、网络数据批量传输场景，化整为零分批操作，降低内存峰值。像读取超大文件，别一股脑全读入内存，按行或固定字节数分批读：import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class FileBatchRead { public static void main(String[] args) { try (BufferedReader reader = new BufferedReader(new FileReader("largefile.txt"))) { String line; while ((line = reader.readLine()) != null) { // 对每行数据处理，如解析、存储等，不占大量内存 System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } }网络数据接收也同理，限定每次接收量，处理完再收下一批，稳守内存防线。四、Java 内存管理优化实战（一）优化案例剖析曾接手一企业级报表生成系统，高峰时内存溢出频发致服务瘫痪。剖析发现，代码从数据库捞海量数据存 ArrayList，没分页、筛选，还多处静态集合囤数据，垃圾回收器难清理。优化先改数据查询逻辑，按页取数，用完即清；再将静态集合按需清理、复用；关键函数内复杂对象生命周期精控，及时断引用，让垃圾回收。经此番“瘦身”，内存占用锐减，系统稳过高峰。（二）性能监控与持续优化优化非一劳永逸，得靠性能监控“盯梢”。工具如 New Relic、AppDynamics 实时绘内存、CPU 等指标曲线。据监控数据，定期复盘代码，深挖潜在问题，迭代优化。像发现某接口响应慢、内存攀升，深挖是缓存未命中频繁查库，调整缓存策略、优化查询语句后，性能、内存双优。持续优化融入开发流程，才能让 Java 程序内存管理“长治久安”。五、总结Java 内存管理是贯穿开发全程的核心课题，内存泄漏与溢出是悬在程序头顶的“达摩克利斯之剑”。从精准揪出内存泄漏“元凶”，到多维度化解内存溢出危机；从巧用工具监控诊断，到实战里打磨优化策略，步步为营，才能筑牢程序内存防线。开发者要代码层严谨把控，又善用工具赋能，还秉持持续优化理念，让 Java 程序在复杂业务场景稳健飞驰，以高效内存管理夯实高性能、高可靠根基，于数字化浪潮踏浪前行。未来，Java 内存管理技术仍会随硬件革新、业务升级不断演进。新垃圾回收算法、智能内存分配策略将破茧，助力开发者驯服内存这头“猛兽”，解锁更多复杂应用开发可能，为 Java 生态注入澎湃活力。愿各位开发者吃透本文精髓，在内存管理战场游刃有余，打造更多精品 Java 应用！说明：本文涉及的技术概念主要基于 Java 8 及以上版本，其中关于永久代（PermGen）的描述适用于 Java 7 及更早版本，Java 8 已改用元空间（Metaspace）。在实际生产中，请根据具体使用的 JDK 版本调整内存参数配置。
系列教程导航Python 基础教程在 SublimeEditor 中配置 Python 环境Python 代码中添加注释Python 中的变量的使用Python 中的数据类型Python 中的关键字Python 字符串操作Python 中的 list 操作Python 中的 Tuple 操作Python max（）和 min（）–在列表或数组中查找最大值和最小值Python 找到最大的 N 个（前 N 个）或最小的 N 个项目Python 读写 CSV 文件Python 中使用 httplib2–HTTP GET 和 POST 示例Python 将 tuple 解包为变量或参数Python 解包 Tuple–太多值无法解压Python multidict 示例–将单个键映射到字典中的多个值Python OrderedDict–有序字典Python 字典交集–比较两个字典Python 优先级队列示例python 中如何格式化日期30 分钟 Python 爬虫教程什么是优先队列优先级队列（Priority Queue）是一种抽象数据类型，类似于常规队列（Queue）或堆栈（Stack）数据结构，但每个元素还具有与之关联的“优先级”。在优先级队列中，优先级高的元素会先于优先级低的元素被处理。如果两个元素具有相同的优先级，则将根据其进入队列的先后顺序（FIFO）为其提供服务。Python 中的优先级队列实现以下 Python 程序使用标准库 heapq 模块实现了一个简单的优先级队列。实现细节说明：heapq 实现的是小顶堆（Min-Heap），默认弹出最小值。为了实现“优先级越高越先出队”，我们将优先级取负数（-priority）存入堆中。引入 _index 索引是为了确保当优先级相同时，先入队的元素先出队（稳定性），同时也避免了直接比较 item 对象可能引发的类型错误。# PriorityQueue.py import heapq class PriorityQueue: def __init__(self): self._queue = [] self._index = 0 def push(self, item, priority): # 使用元组 (-priority, index, item) 存储 # 负优先级确保高优先级数值排在堆顶 # index 确保同优先级下的插入顺序 heapq.heappush(self._queue, (-priority, self._index, item)) self._index += 1 def pop(self): # 返回元组中的最后一个元素，即 item return heapq.heappop(self._queue)[-1]Python 优先级队列示例让我们通过一个具体的例子来演示如何使用上面创建的优先级队列。# example.py class Item: def __init__(self, name): self.name = name def __repr__(self): return 'Item({!r})'.format(self.name) # 交互式命令行示例 >>> q = PriorityQueue() >>> q.push(Item('how'), 1) >>> q.push(Item('to'), 5) >>> q.push(Item('do'), 4) >>> q.push(Item('in'), 2) >>> q.push(Item('java'), 1) >>> q.pop() Item('to') # 优先级 5 >>> q.pop() Item('do') # 优先级 4 >>> q.pop() Item('in') # 优先级 2 >>> q.pop() Item('how') # 优先级 1 (先入队) >>> q.pop() Item('java') # 优先级 1 (后入队)从输出结果可以看出，元素严格按照优先级从高到低出队。对于优先级相同的元素（如 'how' 和 'java' 优先级均为 1），则按照入队顺序依次出队。学习愉快！说明：本文示例基于 Python 3 标准库 heapq 实现，适用于大多数 Python 3 环境。