Java GC系列(1):Java垃圾回收简介

Java的内存分配与回收全部由JVM垃圾回收进程自动完成。与C语言不同,Java开发者不需要自己编写代码实现垃圾回收。这是Java深受大家欢迎的众多特性之一,能够帮助程序员更好地编写Java程序。

下面四篇教程是了解Java 垃圾回收(GC)的基础:

  1. 垃圾回收简介
  2. 圾回收是如何工作的?
  3. 垃圾回收的类别
  4. 垃圾回收监视和分析

这篇教程是系列第一部分。首先会解释基本的术语,比如JDK、JVM、JRE和HotSpotVM。接着会介绍JVM结构和Java 堆内存结构。理解这些基础对于理解后面的垃圾回收知识很重要。

Java关键术语

  • JavaAPI:一系列帮助开发者创建Java应用程序的封装好的库。
  • Java 开发工具包 (JDK):一系列工具帮助开发者创建Java应用程序。JDK包含工具编译、运行、打包、分发和监视Java应用程序。
  • Java 虚拟机(JVM):JVM是一个抽象的计算机结构。Java程序根据JVM的特性编写。JVM针对特定于操作系统并且可以将Java指令翻译成底层系统的指令并执行。JVM确保了Java的平台无关性。
  • Java 运行环境(JRE):JRE包含JVM实现和Java API。

    Java HotSpot 虚拟机

    每种JVM实现可能采用不同的方法实现垃圾回收机制。在收购SUN之前,Oracle使用的是JRockit JVM,收购之后使用HotSpot JVM。目前Oracle拥有两种JVM实现并且一段时间后两个JVM实现会合二为一。

    HotSpot JVM是目前Oracle SE平台标准核心组件的一部分。在这篇垃圾回收教程中,我们将会了解基于HotSpot虚拟机的垃圾回收原则。

    JVM体系结构

    下面图片总结了JVM的关键组件。在JVM体系结构中,与垃圾回收相关的两个主要组件是堆内存和垃圾回收器。堆内存是内存数据区,用来保存运行时的对象实例。垃圾回收器也会在这里操作。现在我们知道这些组件是如何在框架中工作的。

  • Java堆内存

    我们有必要了解堆内存在JVM内存模型的角色。在运行时,Java的实例被存放在堆内存区域。当一个对象不再被引用时,满足条件就会从堆内存移除。在垃圾回收进程中,这些对象将会从堆内存移除并且内存空间被回收。堆内存以下三个主要区域:

  • 新生代(Young Generation)
    • Eden空间(Eden space,任何实例都通过Eden空间进入运行时内存区域)
    • S0 Survivor空间(S0 Survivor space,存在时间长的实例将会从Eden空间移动到S0 Survivor空间)
    • S1 Survivor空间 (存在时间更长的实例将会从S0 Survivor空间移动到S1 Survivor空间)
  • 老年代(Old Generation)实例将从S1提升到Tenured(终身代)
  • 永久代(Permanent Generation)包含类、方法等细节的元信息
  • 永久代空间在Java SE8特性中已经被移除
  • Java GC系列(2):Java垃圾回收是如何工作的?

    目录

  • 垃圾回收介绍
  • 垃圾回收是如何工作的?
  • 垃圾回收的类别
  • 垃圾回收监视和分析
  • 本教程是为了理解基本的Java垃圾回收以及它是如何工作的。这是垃圾回收教程系列的第二部分。希望你已经读过了第一部分:《Java 垃圾回收介绍》

    Java 垃圾回收是一项自动化的过程,用来管理程序所使用的运行时内存。通过这一自动化过程,JVM 解除了程序员在程序中分配和释放内存资源的开销。

    启动Java垃圾回收

    作为一个自动的过程,程序员不需要在代码中显示地启动垃圾回收过程。System.gc()Runtime.gc()用来请求JVM启动垃圾回收。

    虽然这个请求机制提供给程序员一个启动 GC 过程的机会,但是启动由 JVM负责。JVM可以拒绝这个请求,所以并不保证这些调用都将执行垃圾回收。启动时机的选择由JVM决定,并且取决于堆内存中Eden区是否可用。JVM将这个选择留给了Java规范的实现,不同实现具体使用的算法不尽相同。

    毋庸置疑,我们知道垃圾回收过程是不能被强制执行的。我刚刚发现了一个调用System.gc()有意义的场景。通过这篇文章了解一下适合调用System.gc() 这种极端情况。

    Java垃圾回收过程

    垃圾回收是一种回收无用内存空间并使其对未来实例可用的过程。

    Java GC系列(2):Java垃圾回收是如何工作的?

  • Eden 区:当一个实例被创建了,首先会被存储在堆内存年轻代的 Eden 区中。

    注意:如果你不能理解这些词汇,我建议你阅读这篇 垃圾回收介绍 ,这篇教程详细地介绍了内存模型、JVM 架构以及这些术语。

    Survivor 区(S0 和 S1):作为年轻代 GC(Minor GC)周期的一部分,存活的对象(仍然被引用的)从 Eden 区被移动到 Survivor 区的 S0 中。类似的,垃圾回收器会扫描 S0 然后将存活的实例移动到 S1 中。

    (译注:此处不应该是Eden和S0中存活的都移到S1么,为什么会先移到S0再从S0移到S1?)

    死亡的实例(不再被引用)被标记为垃圾回收。根据垃圾回收器(有四种常用的垃圾回收器,将在下一教程中介绍它们)选择的不同,要么被标记的实例都会不停地从内存中移除,要么回收过程会在一个单独的进程中完成。

    老年代: 老年代(Old or tenured generation)是堆内存中的第二块逻辑区。当垃圾回收器执行 Minor GC 周期时,在 S1 Survivor 区中的存活实例将会被晋升到老年代,而未被引用的对象被标记为回收。

    老年代 GC(Major GC):相对于 Java 垃圾回收过程,老年代是实例生命周期的最后阶段。Major GC 扫描老年代的垃圾回收过程。如果实例不再被引用,那么它们会被标记为回收,否则它们会继续留在老年代中。

    内存碎片:一旦实例从堆内存中被删除,其位置就会变空并且可用于未来实例的分配。这些空出的空间将会使整个内存区域碎片化。为了实例的快速分配,需要进行碎片整理。基于垃圾回收器的不同选择,回收的内存区域要么被不停地被整理,要么在一个单独的GC进程中完成。

    垃圾回收中实例的终结

    在释放一个实例和回收内存空间之前,Java 垃圾回收器会调用实例各自的 finalize() 方法,从而该实例有机会释放所持有的资源。虽然可以保证 finalize() 会在回收内存空间之前被调用,但是没有指定的顺序和时间。多个实例间的顺序是无法被预知,甚至可能会并行发生。程序不应该预先调整实例之间的顺序并使用 finalize() 方法回收资源。

  • 任何在 finalize过程中未被捕获的异常会自动被忽略,然后该实例的 finalize 过程被取消。
  • JVM 规范中并没有讨论关于弱引用的垃圾回收机制,也没有很明确的要求。具体的实现都由实现方决定。
  • 垃圾回收是由一个守护线程完成的。
  • 对象什么时候符合垃圾回收的条件?

  • 所有实例都没有活动线程访问。
  • 没有被其他任何实例访问的循环引用实例。
  • Java 中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。

    引用类型 垃圾收集
    强引用(Strong Reference) 不符合垃圾收集
    软引用(Soft Reference) 垃圾收集可能会执行,但会作为最后的选择
    弱引用(Weak Reference) 符合垃圾收集
    虚引用(Phantom Reference) 符合垃圾收集

    在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    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。因此,即使在 SOP 输出之前, finalize 函数也能够打印出 'Rest in Peace!'。我们不能证明这确定会发生,因为它依赖JVM的实现方式和运行时使用的内存。然而,我们还能学习到一点:如果编译器看到该实例在未来再也不会被引用,能够选择并提早释放实例空间。

  • 关于对象什么时候符合垃圾回收有一个更好的例子。实例的所有属性能被存储在寄存器中,随后寄存器将被访问并读取内容。无一例外,这些值将被写回到实例中。虽然这些值在将来能被使用,这个实例仍然能被标记为符合垃圾回收。这是一个很经典的例子,不是吗?
  • 当被赋值为null时,这是很简单的一个符合垃圾回收的示例。当然,复杂的情况可以像上面的几点。这是由 JVM 实现者所做的选择。目的是留下尽可能小的内存占用,加快响应速度,提高吞吐量。为了实现这一目标, JVM 的实现者可以选择一个更好的方案或算法在垃圾回收过程中回收内存空间。
  • GC Scope 示例程序

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    59

    60

    61

    62

    63

    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++;

        }

    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;

            // 所有三个对象都符合GC (它们中没有一个拥有引用。

            // 只有各对象的变量 t 还指向了彼此,

            // 形成了一个由对象组成的环形的岛,而没有任何外部的引用。)

        }

        protected void finalize() {

            System.out.println("Garbage collected from object" + i);

            i++;

        }

    GC OutOfMemoryError 的示例程序

    GC并不保证内存溢出问题的安全性,粗心写下的代码会导致 OutOfMemoryError

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    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);

        }

    }

    输出:

    1

    2

    3

    4

    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)

  • 当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。

    GC Scope 示例程序

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    59

    60

    61

    62

    63

    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++;

        }

    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;

            // 所有三个对象都符合GC (它们中没有一个拥有引用。

            // 只有各对象的变量 t 还指向了彼此,

            // 形成了一个由对象组成的环形的岛,而没有任何外部的引用。)

        }

        protected void finalize() {

            System.out.println("Garbage collected from object" + i);

            i++;

        }

    GC OutOfMemoryError 的示例程序

    GC并不保证内存溢出问题的安全性,粗心写下的代码会导致 OutOfMemoryError

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    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);

        }

    }

    输出:

    1

    2

    3

    4

    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)

    接下来是垃圾收集系列教程的第三部分,我们将会看到常用的 不同 的Java垃圾收集器

  • Java GC系列(3):垃圾回收器种类

    2014/11/19 | 分类: 基础技术教程 | 0 条评论 | 标签: GC垃圾回收教程

    分享到:

    本文由 ImportNew - 好好先生 翻译自 javapapers。欢迎加入翻译小组。转载请见文末要求。

    目录

  • 垃圾回收介绍
  • 垃圾回收是如何工作的?
  • 垃圾回收的类别
  • 垃圾回收监视和分析
  • 在这篇教程中我们将学习几种现有的垃圾回收器。在Java中,垃圾回收是一个自动的进程可以替代程序员进行内存的分配与回收这些复杂的工作。这篇是垃圾回 收教程系列的第三篇,在前面的第2部分我们看到了在Java中垃圾回收是如何工作的,那是篇有意思的文章,我推荐你去看一下。第一部分介绍了Java的垃 圾回收,主要有JVM体系结构,堆内存模型和一些Java术语。

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

  • 串行垃圾回收器(Serial Garbage Collector)
  • 并行垃圾回收器(Parallel Garbage Collector)
  • 并发标记扫描垃圾回收器(CMS Garbage Collector)
  • G1垃圾回收器(G1 Garbage Collector)
  • 每种类型都有自己的优势与劣势。重要的是,我们编程的时候可以通过JVM选择垃圾回收器类型。我们通过向JVM传递参数进行选择。每种类型在很大程度上有 所不同并且可以为我们提供完全不同的应用程序性能。理解每种类型的垃圾回收器并且根据应用程序选择进行正确的选择是非常重要的。

    1、串行垃圾回收器

    串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。

    通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

    2、并行垃圾回收器

    并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候

    3、并发标记扫描垃圾回收器

    并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。

  • 当标记的引用对象在tenured区域;
  • 在进行垃圾回收的时候,堆内存的数据被并发的改变。
  • 运行的垃圾回收器类型

    配置 描述
    -XX:+UseSerialGC 串行垃圾回收器
    -XX:+UseParallelGC 并行垃圾回收器
    -XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
    -XX:ParallelCMSThreads= 并发标记扫描垃圾回收器 =为使用的线程数量
    -XX:+UseG1GC G1垃圾回收器

    GC的优化配置

    配置 描述
    -Xms 初始化堆内存大小
    -Xmx 堆内存最大值
    -Xmn 新生代大小
    -XX:PermSize 初始化永久代大小
    -XX:MaxPermSize 永久代最大容量

    使用JVM GC参数的例子

    1

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

    在Java垃圾回收教程的下一部分,我们将会用一个Java程序演示如何监视和分析垃圾回收。

    相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。

    通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

    4、G1垃圾回收器

    G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域

    通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器

    Java 8 的新特性

    在使用G1垃圾回收器的时候,通过 JVM参数 -XX:+UseStringDeduplication 。 我们可以通过删除重复的字符串,只保留一个char[]来优化堆内存。这个选择在Java 8 u 20被引入。

    我们给出了全部的四种Java垃圾回收器,需要根据应用场景,硬件性能和吞吐量需求来决定使用哪一种。

    垃圾回收的JVM配置

    下面的JVM关键配置都与Java垃圾回收有关。

  • 运行的垃圾回收器类型

    配置 描述
    -XX:+UseSerialGC 串行垃圾回收器
    -XX:+UseParallelGC 并行垃圾回收器
    -XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
    -XX:ParallelCMSThreads= 并发标记扫描垃圾回收器 =为使用的线程数量
    -XX:+UseG1GC G1垃圾回收器

    GC的优化配置

    配置 描述
    -Xms 初始化堆内存大小
    -Xmx 堆内存最大值
    -Xmn 新生代大小
    -XX:PermSize 初始化永久代大小
    -XX:MaxPermSize 永久代最大容量

    使用JVM GC参数的例子

    1

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

    在Java垃圾回收教程的下一部分,我们将会用一个Java程序演示如何监视和分析垃圾回收。

  • Java GC系列(4):垃圾回收监视和分析

    2014/11/20 | 分类: 基础技术教程 | 0 条评论 | 标签: GC垃圾回收教程

    分享到:

    本文由 ImportNew - lomoxy 翻译自 javapapers。欢迎加入翻译小组。转载请见文末要求。

    目录

  • 垃圾回收介绍
  • 垃圾回收是如何工作的?
  • 垃圾回收的类别
  • 垃圾回收监视和分析
  • 在这个Java GC系列教程中,让我们学习用于垃圾回收监视和分析的工具。然后,选用一种工具来监视一个Java示例程序的垃圾回收过程。如果你是一名初学者,你最好仔细阅读该系列教程。你可以从这里(垃圾回收介绍)开始。

    Java GC监视和分析工具

    下面是一些可用的工具,每个都有自己的优势和缺点。我们可以通过选择正确的工具并分析,来提升应用程序的性能。这篇教程中,我们选用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能够被用于:

  •   生成并分析堆的内存转储;
  •   在MBeans上观察并操作;
  •   监视垃圾回收;
  •   内存和CPU性能分析;
  • Java VisualVM是一个Java应用程序,因此它也会被列在其中,教程的意图在于使用VisualVM来监视它自己的GC进程。

    双击“Local”(本地)下的VisualVM图标。

    现在,程序监控窗口在右侧打开,这有许多不同关于应用程序性能的相关监视指数的tab页,目前为止,我们最感兴趣的是“Visual GC”,点击它。

    上面图片显示在Old、Eden、S0和S1上空间利用情况,下图显示了每部分空间的分配和释放情况。它按照指定的刷新率保持持续刷新。

    上面图片所展示的是正常运行程序的情况,当出现内存泄露或者反常的行为时,它会在图表中明确的显示出来。最少我们能理解他是与对象内存分配和垃圾回收相关的事情。随后,通过其他tab页(像“Threads”)和Thread Dump的帮助,我们能够减少这个问题。

    在“Monitor”tab页中,我们能够监控并定时展示所有堆内存使用情况图。通过“Perform GC”按钮可以启动垃圾回收进程。

    在“Sampler”tab页中,我们能够启动内存和CPU性能分析,它将显示详细每个实例使用的实时报告,它将帮助我们明确性能问题。

    这篇教程是我们四篇Java垃圾回收系列教程的最后一篇。

    原文链接: javapapers 翻译: ImportNew.com lomoxy
    译文链接: http://www.importnew.com/13838.html

    1、启动VisualVM

    jvisualvm位于JDK bin文件夹下,直接点击就可以。

    2、安装可视化GC插件

    我们需要安装可视化GC插件,以便在Java GC过程中有良好的视觉感受。

    3、监视GC

    现在,是时候监视垃圾回收进程了,开启你的Java程序,它将自动被检测到并显示到Java VisualVM界面,左侧“Application”(应用程序)窗口下,“Local”(本地节点)下,所有本地运行的Java程序都会被列出。

  • JVM 源码分析之 System.gc()

    2018/01/15 | 分类: 基础技术 | 0 条评论 | 标签: GC

    分享到:

    原文出处: 占小狼

    System.gc()是触发FGC最简单暴力的方式,在JDK源码中实际上是调用了Runtime的gc方法。

    1

    public static void gc() { Runtime.getRuntime().gc(); }

    Runtime.gc的底层实现位于Runtime.c文件中

    1

    JNIEXPORT void JNICALL Java_java_lang_Runtime_gc(JNIEnv *env, jobject this) { JVM_GC(); }

    其中JVM_GC()的实现位于jvm.cpp

    1

    JVM_ENTRY_NO_ENV(void, JVM_GC(void)) JVMWrapper("JVM_GC"); if (!DisableExplicitGC) { Universe::heap()->collect(GCCause::_java_lang_system_gc); } JVM_END

    这里有一个DisableExplicitGC参数,默认是false,如果启动JVM时添加了参数-XX:+DisableExplicitGC,那么JVM_GC相当于一个空函数,并不会进行GC。

    其中Universe::heap()返回当前堆对象,由collect方法开始执行GC,并设置当前触发GC的条件为_java_lang_system_gc,内部会根据GC条件执行不同逻辑。

    JVM的具体堆实现,在Universe.cpp文件中的initialize_heap()由启动参数所设置的垃圾回收算法决定。

  • JVM 垃圾回收算法及回收器详解

    2017/08/27 | 分类: 基础技术 | 2 条评论 | 标签: GCJVM

    分享到:

    原文出处: ZIWENXIE

    本文主要讲述JVM中几种常见的垃圾回收算法和相关的垃圾回收器,以及常见的和GC相关的性能调优参数。

    GC Roots

    我们先来了解一下在Java中是如何判断一个对象的生死的,有些语言比如Python是采用引用计数来统计的,但是这种做法可能会遇见循环引用的问题,在Java以及C#等语言中是采用GC Roots来解决这个问题。如果一个对象和GC Roots之间没有链接,那么这个对象也可以被视作是一个可回收的对象。

    Java中可以被作为GC Roots中的对象有:

    虚拟机栈中的引用的对象。
    方法区中的类静态属性引用的对象。
    方法区中的常量引用的对象。
    本地方法栈(jni)即一般说的Native的引用对象。

    垃圾回收算法

    标记清除

    标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段首先通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不连续的,这样给大对象分配内存的时候可能会提前触发full gc。

    复制算法

    将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

    现在的商业虚拟机都采用这种收集算法来回收新生代,IBM研究表明新生代中的对象98%是朝夕生死的,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一个Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1(可以通过-SurvivorRattio来配置),也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

    标记整理

    复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
    标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

    增量算法

    增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

    垃圾回收器

    Serial收集器

    Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即stop the world。到现在为止,它依然是虚拟机运行在client模式下的默认新生代收集器,与其他收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。

    Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用”标记-整理“算法。这个收集器的主要意义也是被Client模式下的虚拟机使用。在Server模式下,它主要还有两大用途:一个是在JDK1.5及以前的版本中与Parallel Scanvenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。

    通过指定-UseSerialGC参数,使用Serial + Serial Old的串行收集器组合进行内存回收。

    ParNew收集器

    ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。

    ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

    -UseParNewGC: 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收,这样新生代使用并行收集器,老年代使用串行收集器。

    Parallel Scavenge收集器

    Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

    Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程和”标记-整理”算法。这个收集器是在jdk1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外别无选择。由于单线程的老年代Serial Old收集器在服务端应用性能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合”给力“。直到Parallel Old收集器出现后,”吞吐量优先“收集器终于有了比较名副其实的应用祝贺,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

    -UseParallelGC: 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收。-UseParallelOldGC: 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收

    CMS收集器

    CMS(Concurrent Mark Swep)收集器是一个比较重要的回收器,现在应用非常广泛,我们重点来看一下,CMS一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。它的收集过程分为四个步骤:

  • 初始标记(initial mark)
  • 并发标记(concurrent mark)
  • 重新标记(remark)
  • 并发清除(concurrent sweep)
  • 注意初始标记和重新标记还是会stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。

    不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行FullGC完开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

    不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

    由于CMS收集器现在比较常用,下面我们再额外了解一下CMS算法的几个常用参数:

    UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。
    为了减少第二次暂停的时间,通过-XX:+CMSParallelRemarkEnabled开启并行remark。如果ramark时间还是过长的话,可以开启-XX:+CMSScavengeBeforeRemark选项,强制remark之前开启一次minor gc,减少remark的暂停时间,但是在remark之后也立即开始一次minor gc。
    CMS默认启动的回收线程数目是(ParallelGCThreads + 3)/4,如果你需要明确设定,可以通过-XX:+ParallelCMSThreads来设定,其中-XX:+ParallelGCThreads代表的年轻代的并发收集线程数目。
    CMSClassUnloadingEnabled: 允许对类元数据进行回收。
    CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
    CMSIncrementalMode:使用增量模式,比较适合单 CPU。
    UseCMSCompactAtFullCollection参数可以使 CMS 在垃圾收集完成后,进行一次内存碎片整理。内存碎片的整理并不是并发进行的。
    UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。
    一些建议
    对于Native Memory:

  • 使用了NIO或者NIO框架(Mina/Netty)
  • 使用了DirectByteBuffer分配字节缓冲区
  • 使用了MappedByteBuffer做内存映射
  • 并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
  • 分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。
  • 空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。
  • 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • 在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

    虽然G1看起来有很多优点,实际上CMS还是主流。

    与GC相关的常用参数

    除了上面提及的一些参数,下面补充一些和GC相关的常用参数:

  • -Xmx: 设置堆内存的最大值。
  • -Xms: 设置堆内存的初始值。
  • -Xmn: 设置新生代的大小。
  • -Xss: 设置栈的大小。
  • -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
  • -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。
  • -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。
  • -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。
  • -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
  • -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
  • -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。
  • 由于Native Memory只能通过FullGC回收,所以除非你非常清楚这时真的有必要,否则不要轻易调用System.gc()。

    另外为了防止某些框架中的System.gc调用(例如NIO框架、Java RMI),建议在启动参数中加上-XX:+DisableExplicitGC来禁用显式GC。这个参数有个巨大的坑,如果你禁用了System.gc(),那么上面的3种场景下的内存就无法回收,可能造成OOM,如果你使用了CMS GC,那么可以用这个参数替代:-XX:+ExplicitGCInvokesConcurrent。

    此外除了CMS的GC,其实其他针对old gen的回收器都会在对old gen回收的同时回收young gen。

    G1收集器

    G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • 并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
  • 分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。
  • 空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。
  • 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • 虽然G1看起来有很多优点,实际上CMS还是主流。

    与GC相关的常用参数

    除了上面提及的一些参数,下面补充一些和GC相关的常用参数:

  • -Xmx: 设置堆内存的最大值。
  • -Xms: 设置堆内存的初始值。
  • -Xmn: 设置新生代的大小。
  • -Xss: 设置栈的大小。
  • -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
  • -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。
  • -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。
  • -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。
  • -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
  • -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
  • -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。
  • 在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

猜你喜欢

转载自blog.csdn.net/qq_37928340/article/details/88550093