深刻理解Java虚拟机 - 垃圾收集器与内存分配策略

Java与C++间有一堵由动态内存分配和垃圾收集技术所围成的墙,外面的人想进来,里面的人却想出去。java

概述

  • GC须要完成的3件事情
  1. 哪些内存须要回收
  2. 何时进行回收
  3. 怎么进行回收
  • 意义 目前动态内存分配和垃圾手记技术已经很成熟,一切彷佛已经进入自动化时代,为何咱们还要去了解GC和动态内存分配呢?答案很简单:当出现内存泄露、内存溢出问题时,当垃圾回收成为系统达到更高并发量的瓶颈时,了解这些自动化技术就显得颇有必要。算法

  • 前章回顾 前章介绍了Java运行时内存的各个区域,其中程序计数器、虚拟机栈、本地方法栈都是随线程而生,随线程而灭,栈的栈帧随方法的调用而入栈,随方法的完成而出栈。每个栈帧中分配的内存大小在编译期就明确可知,所以这几个区域的内存分配和回收都具备肯定性,因此这几个区域不须要过多考虑内存回收的问题,由于方法或线程结束时,内存也随之跟着回收。而Java堆和方法区不同,由于只有程序处于运行期间才能知道会建立哪些对象,这部份内存的分配和回收是动态的。垃圾收集所关注的也是这部份内存,一下提到的内存都指这一部份内存。bash

对象已死吗

引用计数法

给对象添加一个引用计数器,每当对象被引用时,计数器值加1,当引用失效时,计数器值减1,当计数器值为0时,说明对象没有被其余地方引用,即对象已死。客观地说,引用计数法(Reference Counting)的实现简单,判断效率也很高,可是,主流的Java虚拟机都没有采用引用计数法来判断对象是否已死,由于它有一个致命问题-没法解决对象间相互引用的问题。
代码展现:多线程

复制代码

可达性分析法

基本思路:经过一系列的称为“GC Roots”的对象做为起始点,从这些起始点往下搜索,搜索走过的路径称为引用链,当一个对象和“GC Roots”没有任何引用链时(即GC Roots到这个对象是不可达的),说明对象是无用的。 并发

在Java中可做为GC Roots的对象有下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中引用的对象

引用的四种类型

  1. 强引用
  2. 软引用
  3. 弱引用
  4. 虚引用

对象死亡的历程

可达性分析法中不可达的对象也不是非死不可的,而是处于缓刑阶段。要宣告一个对象的死亡至少要通过两次标记过程:当通过可达性分析后发现对象与GC Roots不可达,那么它会被第一次标记而且进行一次刷选,刷选的条件是此兑对象是否有必要执行finalize方法。当对象没有覆盖finalize方法或对象的finalize方法已经被虚拟机执行过,这两种状况都会被视为不须要执行finalize方法。
若是这个对象有必要执行finalize方法,那么对象会被放在F-Queue的队列中,而且会被由Java虚拟机自动建立的、低优先级的Finalizer线程去执行。finalize方法是对象最后一次逃脱死亡的机会,在finalize方法后,GC将会对对象进行第二次标记。若是对象在finalize方法中成功拯救本身,那么在第二次标记时会被移出回收集合,不然就真的被回收了。
代码展现:ide

package com.whut.java;

/**
 * User:  Chunguang Li
 * Date:  2018/3/8
 * Email: 1192126986@foxmail.com
 */

/**
 * 代码演示了两点:
 * 1. 对象能够在GC时自救
 * 2.自救的机会只有一次,由于一个对象的finalize方法只会被JVM调用一次
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC finalizeEscapeGC = null;

    public void isAlive(){
        System.out.println("i still alive...");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("execute finalize method...");
    }

    public static void main(String[] args) throws InterruptedException {
        finalizeEscapeGC = new FinalizeEscapeGC();

        finalizeEscapeGC = null;
        // 显示调用gc
        System.gc();
        // 第一次自救
        // 由于Finalizer线程优先级很低,须要暂停0.5秒时间等待Finalizer线程执行对象的finalize方法
        Thread.sleep(500);

        if (finalizeEscapeGC != null){
            finalizeEscapeGC.isAlive();
        }else {
            System.out.println("i am dead...");
        }

        finalizeEscapeGC = null;
        System.gc();
        // 自救失败
        Thread.sleep(500);

        if (finalizeEscapeGC != null){
            finalizeEscapeGC.isAlive();
        }else {
            System.out.println("i am dead...");
        }
    }
}

复制代码

回收方法区

不少人认为方法区(虚拟机中的永久代)是没有垃圾回收的,Java虚拟机规范也确实说过不要求虚拟机在方法区实现圾回收,由于方法区的垃圾收集效率很低。
方法区的垃圾收集主要回收两部份内容:废弃常量和无用的类。高并发

  • 回收废弃常量 回收废弃常量与回收Java堆中的对象相似,以常量池中的字面量的回收为例:若是“abc”字符串存储在常量池中,其余地方没有任何对象引用常量池中的“abc”常量,那么进行垃圾回收时“abc”常量会被清理出常量池。常量池中的其余类(接口)、方法、字段的符号引用也与此相似。性能

  • 无用的类 判断无用的类比废弃常量条件苛刻得多。必须知足一下三个条件:spa

  1. 该类的全部实例都已被回收
  2. 加载该类的ClassLoader已被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类。

垃圾收集算法

标记-清除算法

最基础的收集算法。线程

  • 工做原理 算法主要分为两个阶段-标记和清除:首先标记出全部须要回收的对象,标记完成后统一进行清除。

  • 缺点

  1. 效率问题:标记和清除两个过程效率都不高
  2. 空间问题:对象清除后会产生大量不连续的空间碎片,当须要分配给大对象分配较大的内存空间时会由于找不到足够的连续空间而不得不提早触发下一次垃圾收集。

复制算法

为解决效率问题,复制算法出现了:它将内存空间分为大小相等的两块区域,每次只使用其中一块,当进行垃圾收集时,将这块区域中还存活的对象复制到另外一块,而后将这一块内存回收。这样就不会产生内存碎片的问题。这种算法实现简单,运行高效,只是代价是每次只能使用内存的一半,代价太高。

如今的商用虚拟机都采用这种收集算法回收新生代内存。根IBM公司的研究代表,新生代中的内存对象98%是朝生夕死的,因此不须要按照1:1的比例来划份内存空间,而是将内存分为一块较大的Eden区域,两块较小的Survivor区域。每次只使用一块Eden区域和一块Survivor区域,当进行垃圾收集时,将Eden区域和Survivor区域仍然存活的对象复制到另外一块Survivor区域,而后将Eden区域和使用过的Survivor区域清除。HotSpot虚拟机默认的Eden和Survivor区域大小比例为8:1,这样只会浪费10%的内存。

标记-整理算法

复制算法在对象成活率较低的新生代比较适用,而对于对象成活率较高的老年代就须要进行较多的复制操做,效率明显会减低。因此针对老年代的特色,提出了标记-整理算法:标记清除过程仍然与标记清除算法同样,只是在清除后将存活的对象都向一端移动。

分代收集算法

当前商业的虚拟机的垃圾收集算法都采用分代收集算法:根据对象存活周期的不一样将内存划分为几块,通常把Java堆分为新生代和老年代,再根据各个年代的特色选择合适的收集算法。
在新生代中,对象存活率低,适合使用复制算法,而老年代对象的存活率高,适合使用标记-清除算法或标记-整理算法。

垃圾收集器

收集算法是内存回收的方法论,那么收集器就是收集算法的实现。

Serial 收集器 - 新生代收集器

Serial收集器是最基本、最悠久的收集器。这个收集器是一个单线程收集器,在它进行垃圾收集时,必须停掉全部其余的工做线程,而后以一条收集线程进行垃圾收集,直到收集工做结束,才能够恢复其余工做线程。这对于许多应用是难以接受的。可是对Client(客户端)模式的虚拟机来讲,Serial收集器是一个不错的选择,由于在桌面端应用,分配给虚拟机的内存不会太大,收集几十兆到几百兆的新生代内存停顿时间彻底能够控制在几十毫秒。

ParNew 收集器 - 新生代收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多协调线程进行垃圾收集外,其他的Serial收集器彻底同样。ParNew收集器在单CPU或CPU数量少的环境中性能不会有比Serial收集器更好的结果,可是随着CPU数量的增多,它GC时对CPU资源的的有效利用仍是颇有好处的,因此它是许多运行在Server模式下的虚拟机的首先新生代收集器。

Parallel Scavenge 收集器 - 新生代收集器

它看上去彷佛与ParNew同样,可是它的目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间))。停顿时间越短,就越适合与用户交互的程序,由于良好的响应时间能够提升用户的体验,而吞吐量则能够高效利用CPU时间尽快完成程序的计算任务,主要适合在后台运算而须要交互任务。
Parallel Scavenge 收集器提供了两个参数用于控制吞吐量:

  1. 最大垃圾收集停顿时间:-XX:MAxGCPauseMillis
  2. 设置吞吐量大小:-XX:GCTimeRatio

Serial Old 收集器 - 老年代收集器

Serial Old收集器是Serial收集器的老年代版本,一样是一个单线程收集器,使用标记-整理算法。

Parallel Old 收集器 - 老年代收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法,主要配合Parallel Scavenge收集器组成“吞吐量优先”组合。

CMS 收集器 - 老年代收集器

CMS(Concurrent Mark Sweep)是一款以获取最短回收停顿时间为目的的收集器。CMS很是适合B/S系统服务端的Java应用,由于这类应用尤为注重服务的响应时间,但愿系统的停顿时间越短。CMS是基于标记-清除算法的运做流程分为4个部分:

  1. 初始标记:标记GC Roots能关联到的对象,速度很快
  2. 并发标记:进行GC Roots Tracing
  3. 从新标记:为了修改并发标记期间因程序继续运行而致使标记产生变更的对象的标记
  4. 并发清除 初始标记和从新标记仍须要Stop The World,而并发标记和并发清除能够与用户线程一块儿并发工做。CMS的主要特色是:并发收集、低停顿。
相关文章
相关标签/搜索