《深入理解Java虚拟机》之垃圾收集器与内存分配策略

阅读《深入理解Java虚拟机》第2版,结合JDK8的读书笔记。当前文章为书本的第3章节。

3.1.概述

GC需要完成3件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

垃圾收集器关注的是Java堆的内存回收,因为堆存放的是对象的实例,例如一个接口中不同的实现类所需要的内存可能是不一样的,一个方法中不同的分支所需要的内存可能也不一样,只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。

3.2.对象已死吗

垃圾收集器在对堆进行回收前,第一件事就是要判断这些对象是否存活?下文介绍判断对象是否可以回收的方案:

  • 引用计数算法(Reference Counting)
  • 可达性分析算法(Reachability Analysis)

3.2.1.引用计数算法

每当有对象引用它时,计数器值加1,引用失效时,计数器值减1,任何时候计数器为0的对象就是不可能再被使用。

算法实现简单,判定效率也很高,在大部分情况下是一个不错的算法。不过无法解决互相引用的问题。

public class ReferenceCounting{

    public Object instance = null;
    
    public static void main(String[] args){
    
        // 互相引用案例
        ReferenceCounting objA = new ReferenceCounting();
        ReferenceCounting objB = new ReferenceCounting();
        
        objA.instance = objB;
        objB.instance = objA;
    }
}

3.2.2.可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference-Chain),当一个对象到GC Roots没有任何引用链相连时(用图论的话来说,就是从GC-Roots到该对象不可达),证明此对象不可用。

在JAVA中,可以作为GC Roots的对象包括:

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

3.2.3.引用等级

无论是通过引用计数算法还是可达性分析算法,判断对象是否存活都和“引用”有关,在JDK1.2之后,Java对引用的概念进行了扩充,将应用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种。

  • 强引用

强引用就是指在程序代码址中普遍存在的,类似"Object obj = new Object()"这里的引用,只要强引用还存在,垃圾回收器就不会回收掉被引用的对象。

  • 软引用

对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

在JDK1.2之后,提供了SoftReference类来实现软引用。

软引用使用设计对象的cache。对于缓存,我们希望被缓存的对象最好常驻内存,但是如果内存吃紧,为了防治发生内存溢出导致系统崩溃,可以允许虚拟机收回内存。

public static void main(String[] args) {
    // 模拟对象缓存
    Map<String, String>  map = new HashMap<>(3);
    map.put("a", "a-value");
    map.put("b", "b-value");
    map.put("c", "c-value");

    SoftReference<Map<String, String>> softReference = new SoftReference<Map<String, String>>(map);
    Map<String, String> cacheMap = softReference.get();

    System.out.println("a = " +cacheMap.get("a"));
}
  • 弱引用

对于弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉弱引用关联的对象。

在JDK1.2之后,提供了WeakReference类来实现弱引用。用法同软引用一样。

通常用于Debug、内存监视工具等程序中。因为这类程序一般要求即要观察到对象,又不能影响该对象正常的GC过程。

  • 虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

在JDK1.2之后,提供了PhantomReference类来实现虚引用。用法同软引用一样。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

3.2.4.生存还是死亡

对象被回收要经历两次标记过程:

  1. 对象在进行可达性分析后发现不可达,则被第一次标记并进行筛选,筛选的条件是该对象是否有必要执行finalize()方法。

当对象没有重写finalize方法或者该方法已经被调用过,这两种情况都视为“没有必要执行”。

  1. 当发现对象有必要执行finalize()方法时,将其加入F-Queue对象。Finalizer线程(低优先级)执行F-Queue队列,会执行该队列中对象的finalize()方法,如果该对象还是不可达状态,则被回收。

这里的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。有可能对象的finalize()方法执行缓慢,也有可能finallize()方法是个死循环。

以下代码演示对象的自我解救:

/**
 * @author guoyu.huang
 * @version 1.0.0
 */
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

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

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("do finalize.");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        // 等待被回收
        SAVE_HOOK = null;
        System.gc();

        Thread.sleep(500);

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

        // 第二次等待被回收
        SAVE_HOOK = null;
        System.gc();

        Thread.sleep(500);

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


// 日志打印:

do finalize.
i am still alive.
i am dead.

3.2.5.回收方法区

java虚拟机允许方法区不进行垃圾回收。方法区的垃圾回收主要分两部分:废弃常量和无用的类。

  • 废弃常量

判断常量是否废弃,只要判断该常量是否被引用即可。

例如:一个字符串“abc”已经进入常量池,但是当前系统没有一个String对象引用常量池中的“abc”常量

  • 无用的类

判断类是否无用,需要满足以下三个条件:

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

虚拟机可以对满足以上三个条件的无用类进行回收,并不是一定会回收。

是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。

在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证方法区不会溢出。

3.3.垃圾收集算法

3.3.1.标记-清除算法

标记-清除算法(Mark-Sweep)是最基础的收集算法。

该算法分为标记和清除两个阶段,首先标记要被回收的对象,在标记完成之后统一回收所有被标记的对象。

该算法的不足:

  1. 标记和清除的效率都不高

第一阶段标记,需要遍历所有的对象,对要删除的对象进行标记。第二阶段删除,遍历所有的对象,对被标记的对象进行清除操作。当对象的数量很大的时候,这种方法的效率就很差了。

  1. 会产生过多不连续的空间碎片,导致后续无法申请比较大的连续内存

3.3.2.复制算法

复制算法(Copying)将内存平均分成两块,每次只使用其中一块,当这一块内存使用完之后,将还存活的对象复制到另一块内存,然后将使用过的那块内存全部清除。

因为每次都是对整个半区进行内存回收,内存分配的时候不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

该算法的不足:造成内存损失

3.3.3.标记-整理算法

标记-整理(Mark-Compact)算法,标记过程仍然与标记-清除算法的一样,但后续步骤不是直接对可回收对象进行清理,而是将存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

3.3.4.分代收集算法

根据对象存活周期的不同将内存分为新生代和老年代。

  • 新生代

对象存活率不高。一般选择复制算法,要操作的对象少,效率高,还有老年代可以进行内存的分配担保,可以减少内存损失。

  • 老年代

对象存活率高。而且没有额外空间对其进行分配担保,所以就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

  • JDK8 HotSpot 新生代收集算法

将新生代内存分为三块,分别为Eden和两块Survivor(from和to),默认比例为8:1:1,当回收的时候将Eden和其中一块Survivor中还存活的对象一次性复制到另一块Survivor内存中,如果这块内存不够存放,需要其他内存(这里指老年代)进行分配担保。

3.4.HotSpot的算法实现

3.4.1.枚举根节点

使用可达性分析算法,判断对象是否存活。

GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果逐个检查这里面的引用,必然会消耗很多时间。因此引入了准确式GC。

  • 准确式GC

准确式GC是指虚拟机可以知道内存中某个位置的数据具体是什么类型。

目前主流的Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,虚拟机应该是有办法得知哪些地方存放着对象引用。

在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这个,GC在扫描时就可以直接得知这些信息了。

另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为分析工作必须在一个能确保一致性的快照中进行。这个原因导致GC进行时必须停顿所有的java执行线程(Sun将这件事情称为“Stop The World”)。

3.4.2.安全点

在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举。出于性能考虑,HotSpot不能为每条指令都生成OopMap,前面也提到只是在“特定的位置”记录了这些信息,这些特定位置称为安全点(safepoint)。

安全点太少,需要长时间才能进入安全点导致GC等待时间太长;大多,会导致频繁进入GC增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。

“长时间执行”的最明显特征就是指令复用,例如方法调用(方法返回前),循环跳转(循环的末尾),异常跳转(抛出异常的位置)等。

接下来就是要考虑如何在GC发生时,让所有线程跑到安全点上再停顿下来。两种方案:抢先式中断和主动式中断。

  • 抢先式中断

不需要线程的执行代码配合,在发生GC时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。

现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件

  • 主动式中断

当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

3.4.3.安全区域

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点,但是,当线程处于Sleep状态或者“Blocked”状态,这时候线程没办法响应虚拟机的中断请求。对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。

线程在执行到Safe Region中代码时,首先标识自己已经进入了Safe Region。当发生GC时,可以忽略标识自己为Safe Region状态的线程。当线程要离开Safe Region时,它要检查系统是否已经完成枚举根节点(或者整个GC过程),如果完成线程继续执行,否则必须等待知道收到可以离开的信号。

3.5.垃圾收集器

  • 新生代:Serial,ParNew,Parallel Scavenge
  • 老年代:CMS,Serial Old(MSC),Parallel Old

允许的收集器组合:

  1. Serial + CMS / Serial Old
  2. ParNew + CMS / Serial Old
  3. Parallel Scavenge + Serial Old / Parallel Old
  4. G1

3.5.1.Serial收集器(复制算法)

Serial收集器是最基本,发展历史最悠久的收集器。

  • 缺点

它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,知道它收集结束。

  • 优点

简单高效(与其他收集器的单线程比)。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率

  • 参数列表
参数作用
-XX:SurvivorRatio指定Eden区域的占比
-XX:PretenureSizeThreshold大对象直接分配到老年代
-XX:MaxTenuringThreshold长期存活对象分配到老年代
-XX:+HandlePromotionFailure空间分配担保

下图为Serial/SerialOld收集器运行示意图,拍摄于周志明老师的《深入理解Java虚拟机 第2版》
Serial/SerialOld收集器运行示意图

3.5.2.ParNew收集器(复制算法)

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集以外,其余行为都是一致的。

因为除了Serial,只有ParNew能和CMS配合工作,所以ParNew成为运行在Server模式下的新生代收集器的首选。

随着可以使用的CPU的数量增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同。

  • 参数列表
参数作用
-XX:ParallelGCThreads垃圾收集的线程数

下图为ParNew/SerialOld收集器运行示意图,拍摄于周志明老师的《深入理解Java虚拟机 第2版》
ParNew/SerialOld收集器运行示意图

3.5.3.Parallel Scavenge收集器(复制算法)

该收集器的目标是达到一个可控制的吞吐量(Throughput)。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

例如:虚拟机总共运行了100分钟,其中垃圾收集花掉了1分钟,那吞吐量=99/100=99%;

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

  • 参数列表
参数作用
-XX:MaxGCPauseMillis大于0的毫秒数,最大内存回收花费的时间
-XX:GCTimeRatio大于0且小于100的整数,垃圾收集时间占总时间的比率,相当于吞吐量的倒数,默认值为99
-XX:+UseAdaptiveSizePolicy开启GC自适应的调节策略。虚拟机会根据当前系统的运行情况动态调整参数

3.5.4.Serial Old收集器(标记整理算法)

Serial Old是Serial收集器的老年代版本,同样是一个单线程的收集器。主要是给Client模式下的虚拟机使用。如果用于Server模式下,那么有两个用途:

  1. 在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
  2. 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

下图为Serial/SerialOld收集器运行示意图,拍摄于周志明老师的《深入理解Java虚拟机 第2版》
Serial/SerialOld收集器运行示意图

3.5.5.Parallel Old收集器(标记整理算法)

Parallel Scavenge收集器的老年代版本。

在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

下图为Parallel Scavebge/Parallel Old收集器运行示意图,拍摄于周志明老师的《深入理解Java虚拟机 第2版》
Parallel Scavebge/Parallel Old收集器运行示意图

3.5.6.CMS收集器(标记清除算法)

CMS收集器整个过程分为4个步骤,其中初始标记和重新标记两个步骤仍然需要“Stop The World”。

  1. 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程。
  3. 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间会比初始标记阶段稍长一些,但是远比并发标记的时间短。
  4. 并发清除(CMS concurrent sweep)

由于整个过程中最耗时的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

该收集器存在3个明显的缺点:

  1. CMS收集器对CPU资源非常敏感。

CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。如果CPU只有2个,那要分出一半的运算能力去执行收集器线程。

  1. CMS收集器无法处理浮动垃圾

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在档次收集中处理掉它们,只好留住等待下一次GC时再清理。这一部分垃圾就成为“浮动垃圾”。

由于垃圾收集阶段,用户线程还需要运行,因此需要预留一部分空间提供并发收集时的程序运作使用(可以通过参数-XX:CMSInitiatingOccupancyFraction设置)。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来进行老年代的垃圾收集,这样停顿的时间就很长了。

  1. 空间碎片

CMS是基于标记清除算法,所以在收集结束之后会有大量的空间碎片产生。提供参数来控制进行FullGC时开启内存碎片的合并整理,还有执行多少次不整理的Full GC后,来一次整理。

  • 参数列表
参数作用
-XX:CMSInitiatingOccupancyFraction在老年代空间被使用多少后触发垃圾收集,默认值为68%
-XX:UseCMSCompactAcFullCollection在完成收集后是否要进行一次内存碎片整理。(默认为开启)
-XX:CMSFullGCsBeforeCompaction在进行若干次垃圾收集后再启动一次内存碎片整理。(默认为0,表示每次Full GC都进行碎片整理)

下图为CMS收集器运行示意图,拍摄于周志明老师的《深入理解Java虚拟机 第2版》
CMS收集器运行示意图

3.5.7.G1收集器(复制算法+标记整理算法)

G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器。分为以下四个步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

具备以下特点:

  1. 并行与并发:使用多个CPU来缩短Stop-The-World停顿的时间,通过并发的方式让收集动作和Java程序同时进行
  2. 分代收集:不需要其他收集器配合,依然采用分代收集方式独立管理整个堆
  3. 空间整合
  4. 可预测的停顿:这是G1相对于CMS的一大优势。可预测的停顿是指允许使用者指定在长度为M毫秒内,消耗在垃圾收集的时间不得超过N毫秒。

下图为G1收集器运行示意图,拍摄于周志明老师的《深入理解Java虚拟机 第2版》
G1收集器运行示意图

3.5.8.理解GC日志

参数功能说明
-XX:+PrintGC输出GC日志
-XX:+PrintGCDetails输出GC详细日志
-XX:+PrintGCTimeStamps输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC在进行GC的前后打印出堆的信息
-Xloggc:…/logs/gc.log日志文件的输出路径

为了打印以下GC日志,使用到的参数:-Xmx20m -Xms20m -XX:MetaspaceSize=12M -XX:MaxMetaspaceSize=20M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

5.002: [GC (Allocation Failure) [PSYoungGen: 5920K->288K(6144K)] 10999K->5495K(19968K), 0.0008073 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5.218: [GC (Allocation Failure) [PSYoungGen: 5920K->288K(6144K)] 11127K->5575K(19968K), 0.0005809 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5.221: [GC (Metadata GC Threshold) [PSYoungGen: 943K->224K(6144K)] 6231K->5663K(19968K), 0.0010454 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5.223: [Full GC (Metadata GC Threshold) [PSYoungGen: 224K->0K(6144K)] [ParOldGen: 5439K->3231K(13824K)] 5663K->3231K(19968K), [Metaspace: 11071K->11071K(1060864K)], 0.0266278 secs] [Times: user=0.22 sys=0.02, real=0.03 secs] 
  • 5.002表示GC执行的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
  • GC和Full GC表示这次垃圾收集的停顿类型,Full GC表示这次GC发生了Stop the world。附带了发生原因
  • PSYoungGen,ParOldGen,Metaspace表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的。PSYoungGen为Parallel Scavenge收集器(新生代),ParOldGen为Parallel Old收集器(老年代),Metaspace是元空间。
  • 5920K->288K(6144K)表示GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
  • 括号外面的10999K->5495K(19968K)表示的是GC前该Java堆已使用容量->GC后该Java堆已使用容量(该Java堆总容量)
  • 0.0025925表示该内存区域GC所占用的时间

3.5.9.垃圾收集器的参数总结

参数描述
UseSerialGC虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收
UseParNewGC打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收
UseConcMarkSweepGC打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用。
UseParallelOldGC打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收
SurvivorRatio新生代中Eden区域与Survivor区域的容量比例,默认为8,代表Eden:Survivor=8:1.
PretenureSizeThreshold直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenringThreshold晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就+1,当超过这个参数值时就进入老年代。
UseAdaptiveSizePolicy动态调整Java堆中各个区域的大小以及进入老年代的年龄。
HandlePromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
ParallelGCThreads设置并行GC时进行内存回收的线程数
GCTimeRatioGC时间占总时间的比率,默认值为99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效
MaxGCPauseMills设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效
CMSInitiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后触发垃圾收集,默认值为68%
UseCMSCompactAcFullCollection设置CMS收集器在完成收集后是否要进行一次内存碎片整理
CMSFullGCsBeforeCompaction设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理

3.6.内存分配与回收策略

可以使用-XX:PrintGCDetails参数,实时查看GC日志。

每个收集器的具体实现不同,得看具体使用的是什么收集器

3.6.1.对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配(部分大对象可能直接在老年代中分配)。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

以下代码通过参数-Xms20M和-Xmx20M限制了堆的大小为20M,参数-Xmn10M限制了新生代的大小为10M,参数-XX:SurvivorRatio=8定义新生代中Eden区与一个Survivor区的空间比例为8:1

/**
 * @author guoyu.huang
 * @version 1.0.0
 */
public class TestAllocation {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        // VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
        byte[] allocation1, allocation2, allocation3, allocation4;

        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }
}

// 打印内容
[GC (Allocation Failure) [PSYoungGen: 6335K->831K(9216K)] 6335K->4935K(19456K), 0.0028907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 7378K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 79% used [0x00000000ff600000,0x00000000ffc64e60,0x00000000ffe00000)
  from space 1024K, 81% used [0x00000000ffe00000,0x00000000ffecfcb0,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3055K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 323K, capacity 392K, committed 512K, reserved 1048576K

通过打印的日志可以看出:

  1. allocation1,allocation2,allocation3对象优先分配在Eden区域。
  2. allocation4因为新生代无法分配内存,直接进入老年代。

3.6.2.大对象直接进入老年代

大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。可以使用-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效

3.6.3.长期存活的对象将进入老年代

虚拟机给每个对象定义了对象年龄计数器。如果对象在Eden区出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每经历一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认为15),就将被晋升到老年代中。

3.6.4.动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

3.6.5.空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC有分享,如果小于或者HandlePromotionFailure设置为不允许冒险,将进行Full GC。

结论

  • 哪些内存需要回收?什么时候回收?

可用可达性分析算法,当内存不够时,回收没有任何引用的对象。

  • 如何回收?

根据三个回收算法:标记清除,标记整理,复制,实现分代收集。每个收集器具体的实现不同。

关注我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑾析编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值