Java垃圾回收
这篇博客是对Java垃圾回收的总结,主要是对Java Garbage Collection Introduction以及后续的三篇博客的翻译。我把这四篇博客翻译到这一篇博客里,把参考的其他博客的链接附在文章末尾。
Java Garbage Collection Introduction
在Java中,分配和回收对象的内存空间是在JVM中、由垃圾回收机制自动完成的。不像C语言,使用Java的开发人员不需要写代码来进行垃圾回收。Java之所以如此流行以及能够帮助程序员更好的开发,这是其中一个原因。
这个系列一共有四篇博客来介绍Java的垃圾回收,分别是:
- Java Garbage Collection Introduction
- How Java Garbage Collection Works?
- Types of Java Garbage Collectors
- Java Garbage Collection Monitoring and Analysis
这一节是这篇博客的第一部分。主要内容是解释一些基础的术语,比如JDK, JVM, JRE, HotSpot VM,然后理解JVM的整体架构和Java堆内存结构。理解这些概念对学习Java垃圾回收很有帮助。
Key Java Terminologies
Java API:Java API是帮助程序员进行Java开发的一系列封装库的集合。
Java Development Kit (JDK):JDK是一套帮助程序员进行Java开发的工具,这些工具主要是进行编译、运行、打包、分配和监视Java程序。
Java Virtual Machine (JVM):JVM是一个抽象的计算机。Java程序是针对JVM规范来写的。对不同的操作系统来说,JVM有所不同,它主要作用是把Java指令翻译为操作系统的指令,并且执行它们。JVM能够保证Java代码独立于平台。
Java Runtime Environment (JRE):JRE包括JVM实现和Java API。
Java HotSpot Virtual Machine
不同的JVM在垃圾回收的实现上可能有点不一样。 在SUN被收购之前,Oracle有JRockit JVM,并且在SUN收购之后,Oracle获得了HotSpot JVM。目前Oracle维护了两个JVM实现,并且已经声明,在一段时间内这两个JVM实现将被合并到一个。
HotSpot JVM是标准Oracle SE平台的核心组件。在这个垃圾回收教程中,我们将看到基于HotSpot虚拟机的垃圾回收原理。
JVM Architecture
下图总结了JVM的关键组件。在JVM架构中,两个主要涉及到垃圾回收的组件是堆内存(heap memory)和垃圾回收器(garbage collector)。堆内存是运行时数据所在的区域,这个区域存着对象实例,垃圾回收器也在这个区域内操作。我们现在看看这些东西如何适应到更大的方案。
Runtime Data Area
正如虚拟机规范所说的那样,JVM中的内存分为5个虚拟的区域:
- Method Area:也被称为非堆区域(在HotSpot JVM的实现当中)。它被分为两个主要的子区域:持久代和代码缓存。前者会存储包括类定义,结构,字段,方法(数据及代码)以及常量在内的类相关数据。后者是用来存储编译后的代码。编译后的代码就是本地代码,它是由JIT编译器生成的,这个编译器是Oracle HotSpot JVM所特有的。
- Heap Memory:后续会详细介绍。
- Java Stacks:和Java类中的方法密切相关,它会存储局部变量以及方法调用的中间结果及返回值,Java中的每个线程都有自己专属的栈,这个栈是别的线程无法访问的。
- PC Registers:特定线程的程序计数器,包含JVM正在执行的指令的地址(如果是本地方法的话它的值则未定义)
- Native Method Stack:用于本地方法(非Java代码),按线程分配
Java Heap Memory
理解Java Heap Memory在JVM模型中的角色很重要。在运行期间,Java实例保存在heap memory区域。当一个对象没有引用指向它时,它就需要从内存里被清除。在垃圾回收的过程中,这些对象在heap memory里被清除,然后这些空间能够再被利用。heap memory主要有三个区域:
- 年轻代。年轻代分三个区:Eden区(任何进去内存区域的对象实例都要经过eden区)、Survivor0区(从eden区过来S0区的、稍微老一点的对象)、Survivor1区(从S0区过来、稍微老一点的对象)
- 年老代。这个区主要是从S1过来的对象。
- 持久代。这个区包含一些元数据信息比如,类、方法等。
更新:持久代区域已经从Java SE 8移除。取代它的是另一个内存区域也被称为元空间(本地堆内存中的一部分)。
How Java Garbage Collection Works?
这个系列博客主要是帮助理解Java回收的基本概念以及其如何工作。这部分是这个系列的第二部分,希望你有阅读过第一部分Java Garbage Collection Introduction。
Java垃圾回收是一个自动处理的过程,目的是管理程序运行时的内存问题。通过这种方式,JVM能够自动减轻程序员在分配和释放内存资源的问题。
Java Garbage Collection GC Initiation
垃圾回收作为一个自动的过程,程序员不用在代码里显式地启动垃圾回收机制。System.gc()和Runtime.gc()能够显式地让JVM执行垃圾回收过程。
虽然这个机制能够让程序员启动垃圾回收,但是这个责任还是在JVM上。JVM能够选择拒绝你的请求,所以我们没法保证这两个命令肯定会执行垃圾回收。JVM会通过eden区的可用内存来判断是否执行垃圾回收。JVM规范把这个选择留给了JVM的具体实现,所以这些实现的细节对不同的JVM来说也不一样。
毫无疑问的是,垃圾回收机制不能强制执行。我刚刚发现了一个调用System.gc()的应用场景,看这篇文章就知道什么时候调用System.gc()是合适的。
Java Garbage Collection Process
垃圾回收是这样一个过程:回收不再使用的内存空间,让这些空间能够被将来的实例对象所用。
Eden Space
当一个实例被创建的时候,它首先存在堆内存区域的年轻代的eden区。
注意:假如你不懂这些术语,建议你去读Java Garbage Collection Introduction,这篇博客把内存模型、JVM架构和一些专业术语解释的很详细。
Survivor Space (S0 and S1)
作为次要垃圾回收(minor GC)周期的一部分,那些仍然被引用的对象会从eden区被搬到survivor的S0区。同样的过程,垃圾回收器会扫描S0区,然后把实例搬到S1区。
那些没有被引用的对象会被标记。通过不同的垃圾回收器(一共有四类垃圾回收器,后续的教程会讲这个)来决定这些被标记的对象在内存中被移除还是在单独的进程中来完成。
Old Generation
年老代或者持久代是堆内存中的第二个逻辑部分。当垃圾回收器执行次要垃圾回收周期的时候,那些仍在S1区存活的实例将被移到年老代,S1区域中没有被引用的实例将会被标记以便清除。
Major GC
在垃圾回收的过程中,年老代是实例对象生命周期的最后一个环节。Major GC是扫描年老代的堆内存的垃圾回收过程。如果有对象没有被引用,那它们将被标记以便清除,如果有被引用,那它们将继续留在年老代。
Memory Fragmentation
一旦实例对象在堆内存中被删除,那它们所占的区域就又可以被将来的实例对象使用。这些空的区域在内存中将会形成很多碎片。为了更快的为实例分配内存,我们应该解决这个碎片问题。根据不同垃圾回收器的选择,被回收的内存将在回收的过程同时或者在GC另外独立的过程中压缩整合。
Finalization of Instances in Garbage Collection
就在清除一个对象并回收它的内存空间之前,垃圾回收器将会调用各个实例的finalize()方法,这样实例对象就有机会可以释放掉它占用的资源。尽管finalize()方法是保证在回收内存空间之前执行的,但是对具体的执行时间和执行顺序是没有任何保证的。多个实例之间的finalize()执行顺序是不能提前预知的,甚至有可能它们是并行执行的。程序不应该预先假设实例执行finalize()的顺序,也不应该使用finalize()方法来回收资源。
- 在finalize过程中抛出的任何异常都默认被忽略掉了,同时该对象的销毁过程被取消
- JVM规范并没有讨论关于弱引用的垃圾回收,并且是明确声明的。具体的细节留给实现者决定。
- 垃圾回收是由守护进程执行的
When an object becomes eligible for garbage collection?
- 任何不能被活着的线程用到的实例
- 不能被其他对象用到的循环引用对象
Java中有多种不同的引用类型。实例的可回收性取决于它的引用类型。
引用 | 垃圾回收 |
---|---|
强引用 | 不适合被垃圾回收 |
软引用 | 可能会被回收,但是是在最后被回收 |
弱引用 | 适合被回收 |
虚引用 | 适合被回收 |
在编译期间,Java编译器有优化技术来选择把null值赋给一个实例,从而把这个实例标记为可回收。
class Animal {
public static void main(String[] args) {
Animal lion = new Animal();
System.out.println("Main is completed.");
}
protected void finalize() {
System.out.println("Rest in Peace!");
}
}
在上面的类中,lion实例在初始化后就没被用过了,所以Java编译器在lion实例化后把lion赋值为null而作为一个优化的手段。这样finlizer方法可能会在Main方法之前打印结果。我们不能确定地保证结果,因为这和JVM的具体实现以及运行时的内存有关。但是我们能知道的一点是:编译器发现一个实例在之后的程序中不再被引用时可以选择提前释放实例的内存。
- 这里有个实例何时变成可回收更好的例子。实例所有的属性可以被存储在寄存器中,之后可以从寄存器中读取这些属性值。在任何情况下,这些值将来都不会被写回到实例对象中。尽管这些值在未来还是被使用到了,但是实例对象依然可以被标记为可回收的。(注:这个例子我没看懂)
- 我们可以简单地认为,当一个实例被赋值为null时就可以被回收。我们也可以像上面提到的、复杂地考虑这个问题。这些都是由JVM的具体实现决定的。其目标都是希望留下最少的痕迹、提高响应性能和增大吞吐量。为了能够达到这些目的,JVM实现者可以在垃圾回收中选择更好的模式或算法来回收内存。
- 当finalize()被调用的时候,JVM释放掉当前线程的所有同步块。
Example Program for GC Scope
Class GCScope {
GCScope t;
static int i = 1;
public static void main(String args[]) {
GCScope t1 = new GCScope();
GCScope t2 = new GCScope();
GCScope t3 = new GCScope();
// No Object Is Eligible for GC
t1.t = t2; // No Object Is Eligible for GC
t2.t = t3; // No Object Is Eligible for GC
t3.t = t1; // No Object Is Eligible for GC
t1 = null;
// No Object Is Eligible for GC (t3.t still has a reference to t1)
t2 = null;
// No Object Is Eligible for GC (t3.t.t still has a reference to t2)
t3 = null;
// All the 3 Object Is Eligible for GC (None of them have a reference.
// only the variable t of the objects are referring each other in a
// rounded fashion forming the Island of objects with out any external
// reference)
}
protected void finalize() {
System.out.println("Garbage collected from object" + i);
i++;
}
Example Program for GC OutOfMemoryError
垃圾回收不保证内存的安全问题,粗心的代码会造成内存溢出。
import java.util.LinkedList;
import java.util.List;
public class GC {
public static void main(String[] main) {
List l = new LinkedList();
// Enter infinite loop which will add a String to the list: l on each
// iteration.
do {
l.add(new String("Hello, World"));
} while (true);
}
}
输出为
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.LinkedList.linkLast(LinkedList.java:142)
at java.util.LinkedList.add(LinkedList.java:338)
at com.javapapers.java.GCScope.main(GCScope.java:12)
Types of Java Garbage Collectors
在这个教程中,我们将学习各种各样的垃圾回收器。Java垃圾回收是一个自动处理的过程,目的是管理程序运行时的内存问题。这是这个系列教程的第三部分,在前面两个部分,我们学习了How Java Garbage Collection Works,这部分很有意思,我建议你完整读一遍。而第一部分Java Garbage Collection Introduction中,我们学习了JVM的架构、堆内存模型和Java相关的术语。
Java有四种垃圾回收器:
- Serial Garbage Collector
- Parallel Garbage Collector
- CMS Garbage Collector
- G1 Garbage Collector
这四种垃圾回收器各有自己的利弊。更重要的是,我们程序员可以选择JVM用哪种垃圾回收器。我们一般是通过设置JVM参数来选择垃圾回收器。各个垃圾回收器在不同应用场景下的效率会有很大的差异。因此了解各种不同类型的垃圾回收器以及它们的应用场景是非常重要的。
Serial Garbage Collector
串行垃圾回收器控制所有的应用线程。它是为单线程环境设计的,只使用一个线程来执行垃圾回收工作。它的工作方式是暂停所有应用线程来执行垃圾回收工作,这种方式不适用于服务器的应用环境。它最适用的是简单的命令行程序。
使用-XX:+UseSerialGC JVM参数来开启使用串行垃圾回收器。
Parallel Garbage Collector
并行垃圾回收器也称作基于吞吐量的回收器。它是JVM的默认垃圾回收器。与Serial垃圾回收器不同的是,它使用多个线程来执行垃圾回收工作。和Serial回收器一样,它在执行垃圾回收工作是也需要暂停所有应用线程。
CMS Garbage Collector
并发标记清除(Concurrent Mark Sweep,CMS)垃圾回收器,使用多个线程来扫描堆内存并标记可被清除的对象,然后清除标记的对象。CMS垃圾回收器只在下面这两种情形下暂停工作线程:
- 在老年代中标记引用对象的时候
- 在并行垃圾回收的过程中堆内存中有变化发生
对比与并行垃圾回收器,CMS回收器使用更多的CPU来保证更高的吞吐量。如果我们可以有更多的CPU用来提升性能,那么CMS垃圾回收器是比并行回收器更好的选择。
使用-XX:+UseParNewGC JVM参数来开启使用CMS垃圾回收器。
G1 Garbage Collector
G1垃圾回收器应用于大的堆内存空间。它将堆内存空间划分为不同的区域,对各个区域并行地做回收工作。G1在回收内存空间后还立即堆空闲空间做整合工作以减少碎片。CMS却是在全部停止(stop the world,STW)时执行内存整合工作。G1会根据各个区域的垃圾数量来对区域评判优先级。
使用-XX:UseG1GC JVM参数来开启使用G1垃圾回收器。
Java 8 Improvement
在使用G1垃圾回收器时,开启使用-XX:+UseStringDeduplacaton JVM参数。它会通过把重复的String值移动到同一个char[]数组来优化堆内存占用。这是Java 8 u 20引入的选项。
以上给出的四个Java垃圾回收器,在什么时候使用哪一个取决于应用场景,硬件配置和吞吐量要求。
Garbage Collection JVM Options
下面是与Java垃圾回收相关、比较重要的JVM选项。
Type of Garbage Collector to run
选项 | 描述 |
---|---|
-XX:+UseSerialGC | Serial Garbage Collector |
-XX:+UseParallelGC | Parallel Garbage Collector |
-XX:+UseConcMarkSweepGC | CMS Garbage Collector |
-XX:ParallelCMSThreads= | CMS Collector – number of threads to use |
-XX:+UseG1GC | G1 Gargbage Collector |
GC Optimization Options
选项 | 描述 |
---|---|
-Xms | Initial heap memory size |
-Xmx | Maximum heap memory size |
-Xmn | Size of Young Generation |
-XX:PermSize | Initial Permanent Generation size |
-XX:MaxPermSize | Maximum Permanent Generation size |
Example Usage of JVM GC Options
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
Java Garbage Collection Monitoring and Analysis
这篇教程主要介绍垃圾回收机制的监控和分析,我们会用到一个工具来监控一个Java应用的垃圾回收过程。如果你是一个新手,你最好把这系列博客的前几篇都看一下,你可以从Java Garbage Collection Introduction开始。
Java Garbage collection monitoring and analysis tools
下面说的工具各自都有利弊,我们可以通过选择合适的工具来进行分析以此提高Java应用的性能。我们这篇教程主要用Java VisualVM。
- Java VisualVM
- Naarad
- GCViewer
- IBM Pattern Modeling and Analysis Tool for Java Garbage Collector
- HPjmeter
- IBM Monitoring and Diagnostic Tools for Java – Garbage Collection and Memory
- Visualizer
- Verbose GC Analyzer
Java VisualVM
Java VisualVM可以通过Java SE SDK来免费安装使用。看看你的Java JDK安装路径,就在\Java\jdk1.8.0\bin路径下。当然还有很多其他的工具,jvisualvm只是其中之一。
Java VisualVM提供了一个包含Java程序信息的可视化接口。它包含很多其他的工具且集成到这一个。现在像JConsole, jstat, jinfo, jstack, and jmap都被集成到Java VisualVM中了。
Java VisualVM主要用来:
- generate and analyze heap memory dumps
- view and operate on MBeans
- monitor garbage collection
- memory and CPU profiling
Launch VisualVM
JDK目录下就有Java VisualVM。
Install Visual GC Plugin
我们需要安装一个可视化插件,以便观察Java GC过程。
Monitor GC
启动你的Java应用,Java VisualVM会自动开始监控,并把结果展示在Java VisualVM可视化接口中。你可以选择不同的组件来观察你感兴趣的指标。