2.垃圾收集与内存分配策略

1.哪些内存需要回收

程序计数器,虚拟机栈,本地方法栈随线程而生随线程而灭。栈中栈帧随着方法的调用与执行完毕而入栈与出栈,每个栈帧分配的内存基本是类结构确定下来就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论中,大体可以认为编译其是可知的),因此这几个区域的内存分配和回收具备确定性。堆与方法区则不能确定接口类的内存,只有运行期间才会知道创建了哪些对象并动态分配内存。垃圾回收器关注的也就是这部分内存。

2.垃圾对象的判定

(1) 引用计数法

给对象中添加一个引用计数器,有引用则+1,引用失效-1,任何时刻计数器为0该对象不可用。缺陷:两个对象互相循环引用没法判断如objA.instance = objB。objB.instance = objA。

引用计数法缺陷demo:

/**
 * 引用计数算法的缺陷
 * testGC方法执行后,objA和objB会不会被GC呢
 * jvm args: -XX:+PrintGCDetails
 *
 * 会GC,虚拟机不是通过引用计数算法来判断对象存活的
 */
public class ReferenceCountuinGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /**
     * 该成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
     */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        ReferenceCountuinGC objA = new ReferenceCountuinGC();
        ReferenceCountuinGC objB = new ReferenceCountuinGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        //假设在这里发生GC,objA和objB能回收吗
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

结果:GC日志显示虚拟机并没有因为两个对象互相引用就不回收他们,这也从侧面证明了虚拟机并不是通过引用计数法来判断对象是否存活的。

[GC (System.gc()) [PSYoungGen: 7433K->712K(38400K)] 7433K->720K(125952K), 0.0860753 secs] [Times: user=0.14 sys=0.00, real=0.09 secs] 
[Full GC (System.gc()) [PSYoungGen: 712K->0K(38400K)] [ParOldGen: 8K->654K(87552K)] 720K->654K(125952K), [Metaspace: 3475K->3475K(1056768K)], 0.0049854 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 

附 GC日志分析

l GC和Full GC说明了这次垃圾收集的停顿类型。如果有“Full”,说明这次GC发生了STW(Stop-The-World),因为是调用了System.gc()方法触发的收集,所以会显示”[Full GC
(System.gc())”。
l PSYoungGen是采用Parallel
Scavenge收集器的年轻代,ParOldGen是采用Parallel
Old收集器的老年代,Tenured是采用Serial Old收集器的老年代。
l [PSYoungGen:
7433K->712K(38400K)] 表示GC前该内存区域已使用的容量7433K->GC后该内存区域已使用的容量712K(该内存区域总容量38400K)
l 7433K->720K(125952K)表示GC前Java堆已使用容量7433K->GC后Java堆已使用容量720K(Java堆总容量125952K)”
l 0.0860753secs 代表该内存区域GC所占用的时间,单位是秒 secs

查看垃圾收集器
java -XX:+PrintCommandLineFlags –version 查看默认垃圾回收器命令

附日志属性解析及垃圾收集器参数传送门
日志属性分析:https://blog.csdn.net/qiaqia609/article/details/50912683
垃圾收集器参数参照表:https://blog.csdn.net/MakeContral/article/details/79119050

(2) 可达性分析法

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



Java中可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,如各个线程被调用的方法堆栈中使用的参数,局部变量,临时变量等。
  • 方法区中的类静态属性引用的对象,如java类的引用类型静态变量
  • 方法区中的常量引用的对象,如字符串常量池里的引用。
  • 本地方法栈中 JNI(Native 方法)的引用对象。
  • java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointException,OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(Synchronized)持有的对象
  • 反应java虚拟机内部情况的JMXBean、JMTI中注册的回调、本地代码缓存等

(3) 对象引用

无论使用引用计数算法判断对象的引用数量,还是使用可达性分析算法判断对象的引用链是否可达,判断对象是否存活都与“引用”有关。jdk1.2后对象引用与垃圾会后的关联如下:

  • 强引用:永远不会回收。
    如“Object obj = new Object()”,这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:内存溢出之前进行回收。
    用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器二次回收,回收后内存还是不足,才会抛出内存溢出。场景:如上一个页面的链接缓存这种。SoftReference 类实现软引用SoftReference ref=new SoftReference(new User());
  • 弱引用:被弱引用关联的对象只能生存到下一次垃圾回收之前。
    用来描述非必须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference 类实现弱引用。WeakReference sr = new WeakReference(new User());
  • 虚引用:任何时候都可能被垃圾回收器回收。
    最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。PhantomReference 类来实现虚引用。
    PhantomReference pr = new PhantomReference(new User(), new ReferenceQueue());

附对象引用描述传送门
四大引用的具体描述:https://www.cnblogs.com/JamesWang1993/p/9347218.html

(4) 对象的自我救赎(了解)

即使在可达性分析算法中不可达的对象,可不是“非死不可“的,此时他们处于“缓刑”
阶段。
要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它会被第一次标记并且进行一次筛选(筛选的条件是此对象是否有必要执行 finalize()方法)。当对象没有覆盖 finalize()方法,或 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行 finalize()方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后由一条由虚拟机自动建立的、低优先级的 Finalizer 线程去执行 finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize()方法最多只会被系统自动调用一次),稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果要在 finalize()方法中成功拯救自己,只要在 finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

finalize自我救赎demo:

/**
 * finalize 对象自我拯救
 * 1对象在被GC时自我拯救
 * 2.这种自救的机会只有一次,因为一个对象的 finalize()方法最多只会被系统自动调用一次
 * @author wb-szy546826
 * @date 2019-6-3 14:34:04
 */
public class FinalizzeEscapeGC {

    public static FinalizzeEscapeGC SAVE_HOOK = null;

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

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method excuted!");
        FinalizzeEscapeGC.SAVE_HOOK = this;
    }

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

        //对象第一次成功救赎自己
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,暂停0.5秒等待
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no, i am dead");
        }

        //下面这段代码与上面完全相同,但是自我自救却失败了
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,暂停0.5秒等待
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no, i am dead");
        }
    }
}

结果:

finalize method excuted!
yes, i am still alive
no, i am dead

finalize 对象自我拯救
(1)对象在被GC时自我拯救
(2)自救的机会只有一次,因为一个对象的 finalize()方法最多只会被系统自动调用一次
(3)不建议使用finalize方法

(5) 方法区回收

方法区回收内容:废弃的常量,不再使用的类型(无用的类)。
类需要同时满足下面3个条件才能算是 “无用的类” :

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

3.垃圾回收算法

分代收集理论
弱分代假说:绝大多数对象都是朝生夕灭的
强分代假说:熬过多次垃圾收集过程的对象就越难消亡
这两个假说奠定了常用垃圾收集器的一致的设计原则:收集器应该将java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域存储(新生代、老年代)

GC名词定义-深入理解java虚拟机第三版

  • 部分收集(Partial GC):目标不是完整收集整个java堆的垃圾收集
    1.新生代(Minor GC/Young GC):目标只是新生代的垃圾收集
    2.老年代(Major GC/Old GC):目标只是老年代的垃圾收集,目前只有CMS收集器会有单独收集老年代的行为。
    3.混合(Mixed GC):目标是收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器有这样的行为。
  • 整堆收集(FullGC):收集整个java堆和方法区的垃圾收集。

标记-清除算法-老年代

首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法-新生代

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
缺点:将可用内存缩小为了原来的一半

当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实 际上大多就是老年代)进行分配担保(Handle Promotion)


标记-整理算法-老年代

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
缺点:移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。

标记清除与标记整理对比

移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种移动操作必须全程暂停用户应用程序才能进行(STW)
不移动和整理存活对象,弥漫于堆中的存货对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。
从对象分配空间方式来看,移动对象(规整堆内存),使用指针碰撞(简单位移指针即可分配内存);不移动对象(不规整堆内存),使用空闲列表(使用过的与空闲的混在一起,需要维护一个列表记录)。详见上一章
从垃圾收集停顿时间来看,不移动对象停顿时间更短-侧面验证CMS基于标记-清除注重低停顿,低延迟。
从整个程序吞吐量来看,移动对象更划算-侧面验证Parallel Scavenge基于标记-整理注重吞吐量。

4.垃圾收集器

下面一张图是HotSpot虚拟机包含的所有收集器,如果两个收集器之间有连线,说明可以搭配使用。

Serial 收集器(复制算法 -XX:+UseSerialGC)

新生代单线程收集器,标记和清理都是单线程,在进行垃圾收集时,必须要暂停其他所有的工作线程,直到它收集结束。优点是简单高效,由于是单线程,此处可以获取最高的单线程收集效率,但是其中STW停顿时间长。


Serial Old收集器(标记-整理算法 -XX:+UseSerialOldGC)

老年代的单线程收集器,使用标记 - 整理算法,运行过程同Serial收集器。

ParNew收集器(复制算法 -XX:+UseParNewGC)

ParNew是Serial的多线程版本,除了使用多线程进行垃圾收集外,其他行为与Serial完全一样,默认收集线程数与cpu核数相同。除了Serial收集器外,只有ParNew能与CMS收集器配合工作,开启CMS后默认该新生代收集器。
JDK9取消ParNew+Serial Old组合以及Serial+CMS组合,取消-XX:+UseParNewGC参数,这意味着ParNew和CMS只能搭配使用,没有其他收集器能够与他们配合了。也可以认为ParNew合并到了CMS,成为其处理新生代的组成部分。

Parallel Scavenge收集器(复制算法 -XX:+UseParallelGC)

Server 模式(内存大于2G,2个cpu)下的 默认收集器(Parallel Scavenge+Parallel Old)。
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,并且虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应调节策略。


Parallel Old收集器(标记-整理算法 XX:+UseParallelOldGC)

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。

并行:多条垃圾收集线程并行工作,但是此时用户线程仍然处理等待状态。
并发:用户线程与垃圾收集线程同时执行(不一定是并行的,可能交替执行),用户程序在继续执行,而垃圾收程序运行在另一个CPU上

CMS(Concurrent Mark Sweep)收集器(标记-清除算法)

以获取最短回收停顿时间为目标的收集器。特点是高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短。

过程:
初始标记(initial mark,STW): 暂停所有的其他线程,仅仅标记下gc roots直接能关联到的对象,速度很快 ;
并发标记(concurrent mark): 从GC Roots的直接联系对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发进行。
重新标记(remark, STW): 重新标记阶段就是为了修正并发标记期间,因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
并发清理(concurrent sweep): 清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。


缺点
1.对CPU资源敏感,可能导致应用程序变慢;虽然不会导致用户线程停顿,但却会占用一部分线程导致应用程序变慢,降低总吞吐量。
2.无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。要是CMS预留的内存无法满足程序分配新对象的需要,会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将启动后被预案,冻结用户线程,临时启动Serial Old收集器老重新进行老年代的垃圾收集,这样停顿时间就很长了。(-XX:CMSInitiatingOccupancyFraction设置预留空间占比,默认92%)
3.由于采用的标记 - 清除算法,会产生大量的内存碎片,当无法找到足够大的连续空间来分配当前对象,可能会提前触发一次Full GC。当JVM不得不进行FULL GC时,JVM会提供参数开启碎片合并整理(-XX:+UseCMSCompactAtFullCollection),内存整理必须移动存活对象,肯定是无法并发的,停顿时间自然会增加。jvm又提供了另一个参数解决上边出现的停顿时间增加的问题(-XX:+CMSFullGCsBeforeCompaction),该参数要求CMS收集器执行FullGC提前进行碎片整理(默认0,即每次进入FullGC都进行碎片整理),以上两个参数Jdk9都已废弃。

参数信息

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做内存碎片整理
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后做内存碎片整理,默认是0,代表每次 FullGC后都会整理一次碎片
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认68%)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在 remark阶段

Garbage First(G1)收集器(分区/标记-整理算法)

G1收集器是垃圾收集器技术发展历史上的里程碑式的成果,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
jdk9发布之日,G1宣告取代Parallel Scavenge+Parallel Old组合,成为服务端模式下默认垃圾收集器,而CMS则沦落至不推荐使用收集器(Deprecate)。

面向局部的设计思想

“停顿时间模型”收集器:指能够支持在一个长度为M毫秒内的时间片段内,消耗在垃圾收集器上的时间大概率不超过N毫秒这样的目标。
G1之前的垃圾收集器,目标范围要么是整个新生代,要么是整个老年代,要么是整个java堆。G1则跳出该思想,它可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是那块内存存放的垃圾数量最多,回收受益最大,这就是G1收集器的Mixed GC模式,实现上边的停顿时间模型目标。

基于Region的内存布局形式

G1也仍是遵循分代收集理论设计的,但是其堆内存的布局与其他收集器有明显差异,G1不再坚持固定大小以及固定数量的分代区域划分,而是把java堆华为多个大小相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden,Survivor或者老年代空间(不再固定,不需要连续)。收集器对扮演不同角色的Region采用不同的策略处理。
Region还有一群特殊的Humongous区域,专门存储大对象。G1认为只要大小超过一个Region容量一般的对象,即可判定为大对象。每个Region大小通过参数-XX:G1HeapRegionSize设定,取值范围1MB~32MB,且应为2的N次幂。对于超哥整个Region的超级大对象,将会被存放在N个连续的Humongous Region中,G1的大多数行为都把Humongous Region作为老年代的一部分进行看待。

G1收集器Region分区示意图

具有优先级的区域回收方式,建立可预测的停顿时间模型

  • G1之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元(化整为零),即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划的避免在整个java堆中进行全区域的垃圾收集。
  • 具体思路就是让G1去跟踪每个Region里面垃圾堆积的价值大小(回收所获得的的空间大小以及回收所需要时间的经验值),然后再后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis,默认200ms),优先处理回收价值is后以最大的Region,这就是“Garbage First”名字的由来。

难点

  • 1.跨Region引用对象
    记忆集RSet避免全堆作为GC Roots扫描。
    存在于新生代引用老年代对象,老年代引用老年代对象。
    如某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活。我们就不应该为了少量的跨代引用去扫描整个老年代,只需在新生代建立一个记忆集RSet,RSet标识出老年代的哪一块内存会存在跨代引用。此后当发生MinorGC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描
  • 2.并发标记阶段如何保证收集线程与用户线程互不干扰的运行
    1).首先要解决的是用户线程改变对象引用关系时,必须保证不能打破原来的对象图结构,导致标记结果出现错误。CMS通过增量更新算法实现,G1通过原始快照(SATB)实现 --待了解
    2).此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上。G1为每个Region设计了两个TAMS指针,把Region中的一部分内容划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址必须在这两个指针之上。该指针之上的默认存活,不回收。

特点

并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念,采用不同的方式处理新建的对象、已经存活了一段时间和熬过多次GC的旧对象获取更好的收集效果。
空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法 实现的收集器;从局部上来看是基于“复制”算法实现的。即G1运作期间不会产生内存空间碎片,收集后仍可提供规整的可用内存。
可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同 的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指 定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。但是也不能完全无限度的降低停顿时间,每次停顿目标时间太短,导致每次选出来的回收集只占堆内存的一小部分,收集器收集的速度跟不上分配器分配的速度,导致垃圾慢慢堆积。

回收集集合CSet
在垃圾收集过程中收集的Region集合可以称为收集集合CSet,也就是在垃圾收集暂停过程中被回收的目标。G1可以面向堆内任何部分来组成CSet进行回收。
年轻代收集CSet只收集年轻代分区,而混合收集则会通过启发算法(GC效率),在老年代候选分区中,筛选出回收收益最高的分区到CSet中。

G1的分代收集
minorGC
开始生成对象时,G1会选一个分区并指定他为eden分区。
不是现有的Eden满了就会马上触发minorGC,G1会计算现在Eden区回收大概需要多久时间,如果回收时间远小于(-XX:MaxGCPauseMillis)参数值,那么动态给Eden增加新的空闲的Region给新对象存放,-XX:G1MaxNewSizePercent参数代表eden内存可以动态添加的最大空间默认60%,直到下一次Eden区存满, G1重新计算回收时间与XX:MaxGCPauseMillis参数值对比,接近该参数才会触发minorGC。

minorGC后存活的对象,转移到survivor中,其余的过程与分代收集器中的minorGC一样

minor gc内部回收过程:stw,创建回收集CSet
1扫描根,主要是静态和栈帧本地变量表的本地变量被扫描
2更新处理RSet,主要检测跨region,从年轻代指向老年代的对象
3对象复制,eden->survivor/old
4处理引用,软引用,弱引用,虚引用

mixedGC
年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比阈值IHOP(-XX:InitiatingHeapOccupancyPercent默认45%)触发MixedGC,会触发新生代minorGC,也会收集部分老年代(根据期望的GC停顿时间确定old区垃圾回收的优先顺序)。MixedGC主要使用复制算法,需要把各个Region存活的对象拷贝到别的Region中,如果拷贝过程发现没有足够的空Region能够承载拷贝对象则触发FullGC。

RSset记录了什么
新生代对老年代的跨region引用,防止整堆扫描
不需要记录老年代对新生代的RSet吗?
不需要,因为新生代的minor GC在mixed GC之前,可以保证新生代会先被清除掉。
老年代对老年代的RSet呢?
可以记录,防止整堆扫面
cms 重新标记 增量更新
g1 最终标记 原始快照(STAB)

G1分代收集整体流程
年轻代满->G1判断收集时间->动态扩容新生代Region->再次满,且收集时间接近配置时间->触发minorGC->老年代增长->老年代占用空间(老年代+Humongous)达到阈值IHOP(-XX:InitiatingHeapOccupancyPercent)默认45%->G1开始准备收集老年代->mixed gc->初始标记->并发标记,最终标记->筛选回收(年轻代全部回收,百分百都是垃圾的region全部回收,部分垃圾的region计算出来,按优先机制多次回收,回收情况按参数分配-XX:G1MixedGCCountTarget 回收次数默认8,--XX:G1HeapWastePercent:堆浪费百分比,当G1发现可被回收的空间小于5%时,就不会再进行混合收集,也就是会结束当前的回收过程)->拷贝存活对象到新的Region时,没有足够空间会触发fullGC

很多博客gc流程
初始标记,并发标记,最终标记,清理并称为并发标记过程,并发标记过程后有混合回收(多次)过程;本文按深入理解java虚拟机中说法,将其理解为为初始标记,并发标记,最终标记,筛选回收(清理+混合回收多次,筛选范围为所有年轻代,全是垃圾的老年代region(包含humongous),部分垃圾的老年代region(按gc效率做优先级多次回收))

mixed gc内部回收过程同minor gc内部回收过程

G1整体流程
首先G1把Java分成多个Region,每个Region中存放着RSet,G1收集的时候扫描根节点GC Roots(如方法栈中的临时变量),然后由GC Roots找到直连的对象,然后找到RSet中引用的对象,以这两类对象进行堆的引用标记。标记完成后把新生代中所有的Region放到CSet,有时会触发全局标记然后选出部分收集效率高的老年代Region加入到Cset中区,然后清理CSet的Region,完成清理。

在R大的帖子中,给出了一个假象的G1垃圾收集运行过程,如下图所示,在结合上一小节的细节,就可以将G1 GC的正常过程理解清楚了。

fullGC
1.转移失败的担保机制,如从年轻代转移存活对象,从老年代分区转移存活对象或者分配巨型对象,无法找到可用的空闲分区
2.当发生第一个条件后,G1会尝试增加堆使用量,如果扩展失败,那么会触发安全措施机制同时发生full GC
停止系统程序,采用单线程标记-清除算法GC,非常耗时

初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,并记录TAMS的值,让下一阶段用户线程并发进行时,能正确的在可用的Region分配对象。这个阶段需要停顿线程,但耗时很短,且是借用进行MinorGC的时候同步的,所以G1在这个阶段没有额外停顿;
并发标记(concurrent Marking):从GC Root开始对堆中对象进行可达性分析,队规扫描整个堆中的对象图,找出要会后的对象,耗时较长,不过可与用户程序并发执行。对象图扫描完成后,要重新处理STAB记录下的在并发时有引用变动的对象。
最终标记(remark,STW): 对用户线程做一个短暂的暂停,用于处理并打阶段结束后仍遗留下来的最后那少量的STAB记录。
筛选回收(cleanup,STW):负责更新Region的统计数据,对各个Region的回收价值和成本进行 排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,可以自由选择多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的region中,再清理掉旧Region的全部空间。这里操作涉及存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成。

参数信息
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为 2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄 1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行 新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近 1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时 候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他 Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的 Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着 本次混合回收就结束了。
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收 该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次混合回收(默认8次),在混合回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。

g1收集器推荐文集:https://www.cnblogs.com/GrimMjx/p/12234564.html
https://blog.csdn.net/coderlius/article/details/79272773
https://www.cnblogs.com/yufengzhang/p/10571081.html
https://baijiahao.baidu.com/s?id=1663956888745443356&wfr=spider&for=pc

5.垃圾回收器特点总结

收集器 特点
serial 单线程 新生代 复制
parnew 多线程并行 新生代 复制
parallel scavenge 多线程并行 新生代 复制 注重吞吐量
serial old 单线程 老生代 标记整理
parallel old 多线程并行 老生代 标记整理
cms 多线程并发 老年代 标记清除 最短停顿时间 (初始标记+并发标记+重新标记+并发清理) 两次stw(初始阶段+重新标记) 初始标记简单标记1次stw,重新标记解决并发标记时程序运作时产生的垃圾记录2次stw

缺点:1.cpu敏感,占用cpu线程=(cpu数量+3)/4 2.cms无法处理浮动垃圾,出现concurrent mode failure导致fullGC,原因:并发清理用户程序运行产生新的垃圾 3.标记-清除,产生不连续空间碎片,导致提前fullGC
g1 多线程并发+并行 老年代+新生代 全局标记整理,局部复制(无碎片化问题) 并行并发,分代收集,空间整合,可预测停顿 younggc动态调节 mixedgc(初始标记+并发标记+最终标记+筛选回收) 化整为零

其他收集器
后面还有ZGC(JDK11中的垃圾收集器)、Shenandoah GC(JDK12中的垃圾收集器)、C4 GC(Zing JVM)...

6.内存分配与回收策略

对象优先在Eden分配
大多数情况下,对象在新生代中 Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。垃圾收集期间发现已有对象无法全部放入Survivor空间,会通过分配担保机制提前转移到老年代去。
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象,-XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效
长期存活的对象进入老年代
虚拟机给每个对象一个对象年龄(Age)计数器,存储在对象头中。通过参数-XX:MaxTenuringThreshold来设置最大年龄,默认15。
动态对象年龄判断
如果在Survivor空间中相同年龄所有对象大小的总和大于这块Survivor区域内存大小的50%,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。对象动态年龄判断机制一般是在minor gc之后触发的。
空间分配担保
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和,就会看一个“-XX:-HandlePromotionFailure”(是否允许担保失败,jdk1.8默认就设置)的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352