Java垃圾回收机制

本文非原创,翻译自Java Garbage Collection introduction 在Java中为对象分配和释放内存空间都是由垃圾回收线程自动执行完成的。和C语言不一样的是Java程序员不需要手动写垃圾回收相关的代码。这是使得Java如此流行,同时也是Java能帮助程序员写出更好的Java应用的优点之一。

计划写一个介绍Java垃圾回收基础的系列文章,共分四部分:

  1. Java垃圾回收简介
  2. Java垃圾回收器是如何工作的?
  3. 各种类型的Java垃圾回收器
  4. Java垃圾回收的监控和分析


1. Java垃圾回收简介

本文是这个系列的第一篇文章,这篇文章将会介绍一些基本术语,如:JDK,JVM,JRE,HotSpot VM,以及理解JVM的架构和Java堆内存结构。在开始学习Java垃圾回收机制之前确实有必要了解一下这些基本东西。

关键的Java术语

  • Java API–一个帮助程序员创建Java应用的打包好的库集合
  • Java Development Kit(JDK)–一个使得程序员能够开发Java应用的工具集合
  • Java Virtual Machine(JVM)–JVM是一个抽象的计算机。Java程序是针对JVM规范写的。JVM是根据不同的操作系统平台实现的,它将Java指令翻译成对应平台上的指令,并执行。这样Java程序能够达到平台独立性。
  • Java Runtime Environment(JRE)–JRE由一个JVM的实现和Java API组成。

Java HotSpot虚拟机

每个JVM的实现在对垃圾回收的原理的实现方式上会有一些不同。在收购SUN之前Oracle有JRockit JVM,收购SUN之后有了HotSpot虚拟机。目前Oracle同时维护了这两个虚拟机,并宣称将来会将两个虚拟机合并。

HotSpot虚拟机是Oracle标准版平台的核心组成部分。在本垃圾回收系列文章中我们将通过HotSpot虚拟机来了解垃圾回收的基本准则。

JVM 架构

下面的这幅图概括了一个JVM中的主要组成。在JVM架构中,堆内存和垃圾回收器这两个部分和垃圾回收相关。堆内存是运行时用来存储实例对象的数据空间,垃圾回收器运行在堆内存上。现在我们大概知道它们是怎样的一个工作模式。JVM 架构图

Java 堆内存

在Java的内存模型中,最重要的是要了解堆内存的概念。运行时的Java实例对象存储在堆内存空间中。当一个对象不再被引用了,它变成可被从堆内存中回收空间。在垃圾回收的过程中,这些对象将被从堆内存中清除,同时它们的空间也就被回收了。堆内存的空间主要分成了三部分,

  1. 年轻代

    1. Eden区(所有实例在运行时最初都分配到eden区中)
    2. S0 Survivor Space(老一些的对象被从eden区移动到S0区,其实是eden区中的对象经过一次对eden区的Young GC还存活的对象被移动到S0)
    3. S1 Survivor Space(再老一些的对象被从S0区移动到S1区,其实是在Young GC过程中S0区已满,则会将eden区中还存活的对象和S0区中的存活对象移动到S1区中)
  2. 老年代(经过S0,S1中几轮迭代后还存活的对象被提升到老年代)

  3. 永久代(包含一些元数据像类、方法等等)

Java堆内存空间

更新:永久代在Java8中被去掉了


2. Java垃圾回收器是如何工作的

Java垃圾回收是一个自动运行的管理程序运行时使用的内存的进程。通过GC的自动执行JVM将程序员从申请和释放内存的繁重操作中解放出来。

Java垃圾回收GC初始化

作为一个自动执行的进程,程序员不需要在代码中主动初始化GC。Java提供了System.gc()Runtime.gc()这两个hook来请求JVM调用GC进程。

尽管要求系统机制给程序员提供调用GC的机会,但是实际上这是由JVM负责决定的。JVM可以选择拒绝启动GC的请求,因此并不保证这些请求会真的调用垃圾回收。这是JVM基于内存堆空间的Eden区的使用情况做出的决定。JVM规范将这个选择权利留给了各个JVM的具体实现,因此实际上JVM是如何选择的视不同JVM的实现而定(不过要记住的是,不能依赖于这两个方法的调用,它们是不被保证执行的)。

毫无疑问的是,我们知道垃圾回收进程是不能强制执行的。不过我刚发现一个调用System.gc()确实有意义的场景。看下这篇文章你就会了解System.gc()调用是可用的这个特殊的场景。

Java 垃圾回收进程

垃圾回收是一个回收不再使用的内存空间并将它变成能够为将来的实例使用的过程。java gc collection process3

Eden Space:当一个实例被创建的时候,它最初被存放在堆内存空间的年轻代的Eden区中。

注意:如果您不太理解这些术语,建议您先看下介绍内存模型、JVM架构及这些术语的详细解释的文章:garbage-collection-introduction-tutorial

Survivor Space(S0 和S1):作为minor回收周期的一部分,还活着的对象(还有引用指向它)被从eden区中移动到survivor空间S0。同样的,垃圾回收器扫描S0并将活着的实例移动到S1。

无用的对象(没有引用指向)被标记并回收。垃圾回收器(有四种可用的垃圾回收器,将在下一篇文章中介绍)决定这些被标记的实例是在扫描的过程中移出内存还是在另外独立的迁移进程中执行。

Old Generation:老年代或者永久代是堆内存的第二个逻辑部分。当垃圾回收器在做minor GC周期中,S1 survivor区中还活着的实例会被提升到老年代中。S1区中不再被引用的对象被标记并清除。

Major GC:在Java垃圾回收过程中实例生命周期的最后一个阶段。Major GC在垃圾回收过程中扫描属于Old Generation部分的堆内存。如果实例没有被任何引用关联,它们将被标记、清除;如果它们还被引用关联着,则将继续存留在old generation。

Memory Fragmentation:一旦实例从堆内存中删除了,它们原来的位置将空出来给以后分配实例使用。显然这些空闲空间很容易在内存空间中产生碎片。为了能够更快地分配实例地址,需要对内存做去碎片化操作。根据不同垃圾回收器的策略,被回收的内存将在回收的过程同时或者在GC另外独立的过程中压缩整合。

垃圾回收过程中的对象销毁–Finalization

就在移除一个对象并回收它的内存空间之前,Java垃圾回收器将会调用各个实例的finalize()方法,这样实例对象就有机会可以释放掉它占用的资源。尽管finalize()方法是保证在回收内存空间之前执行的,但是对具体的执行时间和执行顺序是没有任何保证的。多个实例之间的finalize()执行顺序是不能提前预知的,甚至有可能它们是并行执行的。程序不应该预先假设实例执行finalize()的方法,也不应该使用finalize()方法来回收资源。

  • 在finalize过程中抛出的任何异常都默认被忽略掉了,同时对象的销毁过程被取消
  • JVM规范并没有讨论关于弱引用的垃圾回收,这是明确声明的。具体的细节留给实现者决定。
  • 垃圾回收是由守护进程执行的

对象何时变成可被垃圾回收的?

  • 所有不能被活着的线程到达实例
  • 不能被其他对象到达的循环引用对象 Java中有多种不同的引用类型。实例的可回收性取决于它的引用类型。
ReferenceGarbage Collection
Strong Refrence不被垃圾回收
Soft Reference作为最后的选择,有可能被回收
Weak Reference可以被垃圾回收
Phantom Reference可以被垃圾回收

在编译过程中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 = null。这样finlizer可能会在Main方法的SOP之前打印结果。

Rest in Peace!
Main is completed.

但结果的顺序是不确定的,它取决于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();
        //没有任何一个对象是可以被GC的
        t1.t = t2;//没有任何一个对象是可以被GC的
        t2.t = t3;//没有任何一个对象是可以被GC的
        t3.t = t1;//没有任何一个对象是可以被GC的

        t1 = null;//没有任何一个对象是可以被GC的,t3.t还有对t1的引用

        t2 = null;//没有任何一个对象是可以被GC的,t3.t.t还有对t2的引用
        t3 = null;//所有3个对象都可以被GC(没有一个被引用了)
                  //只有各个对象的变量t互相循环引用形成了一个孤立的引用环,而没有外部引用
    }
    protected void finalize() {
        System.out.println("Garbage collected from boject" + i);
        i++;
    }
}

Example Program for GC OutOfMemoryError

垃圾回收机制并不保证发生内存溢出时的安全,事实上内存溢出将会导致程序的崩溃,抛出OutOfMemoryError

import java.util.LinkedList;
import java.util.List;

public class GC {
    public static void main(String[] args[]) {
        List l = new LinkedList();
        //进入内部无限循环直接向链表中不断添加元素
        do {
            l.add(new String("Hello, World!");
        } while (true);
    }
}

Output

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)

下面将会将垃圾回收系列文章的第三部分:Different types of Java garbage collectors avaliable



3. different types of Java GC



本文将会介绍各种不同类型的Java垃圾回收器。垃圾回收是Java用来将程序员从分配和释放内存的琐事中解放出来的自动过程。

Java有四种类型的垃圾回收器,

  1. Serial Garbage Collector
  2. Parallel Garbage Collector
  3. CMS Garbage Collector
  4. G1 Garbage Collector

各种类型的Java垃圾回收器

这四种类型的垃圾回收器都有各自的优点和缺点。最重要的是程序员可以选择JVM使用哪种类型的垃圾回收器。我们可以通过传递不同的JVM参数来设置使用哪一个。各个垃圾回收器在不同应用场景下的效率会有很大的差异。因此了解各种不同类型的垃圾回收器以及它们的应用场景是非常重要的。

1. Serial Garbage Collector

串行垃圾回收器控制所有的应用线程。它是为单线程场景设计的,只使用一个线程来执行垃圾回收工作。它暂停所有应用线程来执行垃圾回收工作的方式不适用于服务器的应用环境。它最适用的是简单的命令行程序。

使用-XX:+UseSerialGCJVM参数来开启使用串行垃圾回收器。

2. Parallel Garbage Collector

并行垃圾回收器也称作基于吞吐量的回收器。它是JVM的默认垃圾回收器。与Serial不同的是,它使用多个线程来执行垃圾回收工作。和Serial回收器一样,它在执行垃圾回收工作是也需要暂停所有应用线程。

3. CMS Garbage Collector

并发标记清除(Concurrent Mark Sweep,CMS)垃圾回收器,使用多个线程来扫描堆内存并标记可被清除的对象,然后清除标记的对象。CMS垃圾回收器只在下面这两种情形下暂停工作线程,

  1. 在老年代中标记引用对象的时候
  2. 在做垃圾回收的过程中堆内存中有变化发生

对比与并行垃圾回收器,CMS回收器使用更多的CPU来保证更高的吞吐量。如果我们可以有更多的CPU用来提升性能,那么CMS垃圾回收器是比并行回收器更好的选择。

使用-XX:+UseParNewGCJVM参数来开启使用CMS垃圾回收器。

4. G1 Garbage Collector

G1垃圾回收器应用于大的堆内存空间。它将堆内存空间划分为不同的区域,对各个区域并行地做回收工作。G1在回收内存空间后还立即堆空闲空间做整合工作以减少碎片。CMS却是在全部停止(stop the world,STW)时执行内存整合工作。对于不同的区域G1根据垃圾的数量决定优先级。

使用-XX:UseG1GCJVM参数来开启使用G1垃圾回收器。

Java 8 的优化

在使用G1垃圾回收器是,开启使用-XX:+UseStringDeduplacatonJVM参数。它会通过把重复的String值移动到同一个char[]数组来优化堆内存占用。这是Java 8 u 20引入的选项。

以上给出的四个Java垃圾回收器,在什么时候使用哪一个去决于应用场景,硬件配置和吞吐量要求。

Garbage Collection JVM Options

下面是些主要的与Java垃圾回收相关的JVM选项。

Type of Garbage Collector to run

选项描述
-XX:+UseSerialGC串行垃圾回收器
-XX:+UseParallelGC并行垃圾回收器
-XX:+UseConcMarkSweepGCCMS垃圾回收器
-XX:ParallesCMSThread=CMS垃圾回收器–使用的线程数量
-XX:UseG1GCG1垃圾回收器

GC 优化选项

选项描述
-Xms初始堆内存大小
-Xmx最大堆内存大小
-Xmn年轻代的大小
-XX:PermSize初始永久代的大小
-XX:MaxPermSize最大的永久代的大小

Example Usage of JVM GC Options

java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar

在垃圾回收系列的下一篇中,将通过一个例子介绍如何区监控和分析垃圾回收。



4.Monitoring and analyzing Java GC

在这篇Java垃圾回收系列文章中,让我们看下一些监控和分析垃圾回收的工具。然后用工具尝试监控和分析一个例子。初学者最好先看下此系列,从Java 垃圾回收简介开始吧。

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安装目录下的/bin目录,\Java\jdk1.8.0\bin。此目录下有许多其他工具包括javac和java工具,jvisualvm就是其中一个。

Java VisualVM对运行中的Java应用提供了可视化的信息展示。它是很多工具的整合包,像工具JConsole,jstat,jinfo,jstack以及jmap现在都是Java VisualVM的一部分。

Java VisualVM可以用来 - 生成和分析堆内存的dump - 观察和操作MBeans - 监控垃圾回收 - 内存和CPU性能分析

1. Launch VisualVM

jvisualvm已经包含在了JDK的bin目录下,如果以设置环境变量path,则可以直接在命令行中运行jvisualvm,将出现如下启动界面

starting jvisualvmstarting jvisualvm

jvisualvm viewjvisualvm view

2.安装Visual GC插件

我们需要安装visual GC 插件才能有个堆Java GC线程的漂亮和有价值的视觉感受。在上面的启动界面中点击工具->插件->可用插件->Visual GC

visual gcvisual gc

安装。

3. 监控GC

现在可以监控垃圾回收过程了。开启你的Java应用程序,Java VisualVM会自动检测并将其显示到界面上。在左边“应用程序”面板的“本地”节点下面,所有本地运行的Java应用都会被列举出来。

Java VisualVM自己本身也是一个Java应用程序,因此它自身也被列了出来。作为本文的目的我们将监控VisualVM应用本身的GC过程。

双击“本地”节点下的VisualVM图标。

visualvm gcvisualvm gc

这样应用程序的监控面板会在右边打开。面板上有多个标签,每个里面展示了与应用程序性能相关的方面。由于我们现在主要关注“Visual GC”,so,点之。

Heap Memory Space AllocationHeap Memory Space Allocation

上面的图片显示了Old,Eden,S0,S1使用的内存空间。下面的图表显示了各个部分申请和释放内存的详细信息。它以设定的刷新频率不停地跟新。

gc graphgc graph

上图显示的是一个正常运行的程序。当发生内存泄露或者不正常的操作时,可以从图形本身明显地看出来。至少我们可以知道程序中存在和对象内存分配和垃圾回收相关的问题。然后利用其他标签如“Threads”中提供的信息和Thread Dump我们可以缩小发生问题的范围。

在“监视”标签中,我们可以看到以时间线发展的内存使用概况图。这里有个“执行垃圾回收”按钮可以调用垃圾回收过程。 perform gcperform gc

在“抽样器”标签中我们可以开始内存和CPU分析工作。抽样器会实时显示各个实例的使用情况。它可以帮助确定性能问题到底发生在哪里。

instance memory allocationinstance memory allocation

到此为止,关于Java 垃圾回收的系列文章已经完结了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值