前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、垃圾收集

1.1、哪些需要回收 ,哪些不需要回收

1.1.1、不需要回收

程序计时器,虚拟机栈,本地方法栈

这3个区域都是线程所私有的,随着线程而生,而死。 关于栈的话,基本上就是在运行方法的时候开启一个栈帧。他们的内存大小(Xss)和声明周期是已知的,因此这几个区域内存分配和回收都具备确定性,不需要过多考虑回收问题,因为他们在方法结束者是线程结束,内存自然的就被回收了

1.1.2、需要回收的

JAVA堆和方法区 则是需要被垃圾收集器回收的,方法区回收效率不高,具体看上面方法区

1.2、判断对象是否活着

1.2.1、引用计数法

解释:给对象添加一个计时器,每当引用的时候加1,当引用失效时候减1,任何时候为0的对象就是不能再被使用的。(书上说,这样表达不太好)

Java虚拟机没有使用它来管理内存,因为它很难解决对象之间相互引用的问题

1.2.1.1、测试代码

-XX:+PrintGCDetails
public class Jvm01ReferenceCountingGC {

    public Object instance = null;
    private static final int _1MB = 1024;

    /**
     * 占点内存,以便在日志中看清楚是否被回收
     */
    private byte[] bigSize = new byte[1 * _1MB];


    public static void main(String[] args) {
        Jvm01ReferenceCountingGC objA = new Jvm01ReferenceCountingGC();
        Jvm01ReferenceCountingGC objB = new Jvm01ReferenceCountingGC();

        objA.instance = objB;
        objB.instance = objA;

        // A引用B B引用A
        //猜想,如果是jvm采用的是引用计数法的话,如果引用计数法, 因为他们互相引用这对方,导致他们的引用计数都不为0,
        System.gc(); //垃圾收集器回收内存
        //结果:JVM的内存由6676K->400K说明了a,b两个对象的内存还是被回收了,说明idea的虚拟机并不是通过引用计数法来判断对象是否存活。
    }

1.2.1.2、idea查看GC日志

[GC (System.gc()) [PSYoungGen: 3901K->1112K(75776K)] 3901K->1120K(249344K), 0.0013425 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1112K->0K(75776K)] [ParOldGen: 8K->917K(173568K)] 1120K->917K(249344K), [Metaspace: 3206K->3206K(1056768K)], 0.0054112 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 1951K [0x000000076b600000, 0x0000000770a80000, 0x00000007c0000000)
  eden space 65024K, 3% used [0x000000076b600000,0x000000076b7e7c68,0x000000076f580000)
  from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
  to   space 10752K, 0% used [0x0000000770000000,0x0000000770000000,0x0000000770a80000)
 ParOldGen       total 173568K, used 917K [0x00000006c2200000, 0x00000006ccb80000, 0x000000076b600000)
  object space 173568K, 0% used [0x00000006c2200000,0x00000006c22e5478,0x00000006ccb80000)
 Metaspace       used 3213K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 304K, capacity 388K, committed 512K, reserved 1048576K


这就说明JDK8的HotSpot虚拟机并没有采用引用计数算法来标记内存,它对上述代码中的两个死亡对象的引用进行了回收。( 1120K->917K  因为内存变小,肯定是回收了,要不然能变么) 具体看下面

1.2.2、可达性分析算法

解释:这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

可以作为GC Roots的对象包括下面几种

1、虚拟机栈(栈桢中的本地变量表)中的引用的对象

2、本地方法栈中JNI(Native方法)的引用的对象

方法区中的类静态属性引用的对象(方法区的回收) 方法区中的常量引用的对象(方法区的回收)

//当然不能是堆区对象中的引用了

WX20180409-141558@2x

1.3、垃圾收集一定非死不可吗

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段, 要真正宣告一个对象死亡,至少要经历两次标记过程:

1、如果对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,

2、第二次筛选的条件是此对象是否有必要执行finalize() 方法。当对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。这里理解吧,就是说finalize中添加的方法,有可能会影响对象的生命(可以在这个里面救活,如果不救的话,都会执行垃圾收集器)

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法, 那么就有可能在该方法中让对象重新被引用,从而实现自救。 自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。

package com.hlj.jvm.GC;

/*
 * @Description
 * @Author HealerJean
 * @Date 2018/4/9  下午3:30.
 *
 *此代码演示了两点
 * 对象可以在GC时自我拯救
 * 这种自救只会有一次,因为一个对象的finalize方法只会被自动调用一次
 * */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK=null;
    public void isAlive(){
        System.out.println("yes我还活着");
    }

    public void finalize() throws Throwable{
        super.finalize();
        System.out.println("执行finalize方法");
        FinalizeEscapeGC.SAVE_HOOK=this;//自救
    }

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


        //对象的第一次回收
        SAVE_HOOK=null;
        System.gc();
        //因为finalize方法的优先级很低所以暂停0.5秒等它
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no我死了");
        }


        //下面的代码和上面的一样,但是这次自救却失败了,因为finalize方法已经调用过一次,而且它只能执行一次
        //对象的第二次回收
        SAVE_HOOK=null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no我死了");
        }
    }
}

运行结果,说明第一次成功逃脱,finalize为对象逃脱的最后一次机会

执行finalize方法
yes我还活着
no我死了

1.4、垃圾收集算法

解释:不同平台的虚拟机操作内存的方法是不同的,这里主要介绍下几种算法的思想和发展过程

1.4.1、标记-清除算法

很明显,两个阶段,标记和清除踏实最基础的算法,因为后续的手机算法都是基于这种思路并对他的不足进行改进而得到的

不足有两处

1、效率问题:这两个过程效率都不高

2、空间问题:标记清除会产生大量不连续的碎片,碎片太多费配给大的对象的时候,无法找到连续的控件而不得不触发另一次垃圾收集动作

WX20180409-165031@2x

1.4.2、复制算法

为了解决上面的效率问题,就出现了复制,它将内存分为大小相等的两块,每次只使用其中一块,当这一块的内存满了,就会将里面活着的对象复制到另一块上面,然后再把已经使用过的空间一次清理掉(牛逼了,相当于的夫妻二人打架,满了就跑)

这样就不需要考虑是否存在碎片了,但是但是,它他妈的把内存缩小了一半,这代价太高了

WX20180409-165738@2x

现在的商业虚拟机都采用这种收集算法收集新生代,IBM公司研究到其实新生代中的对象98%都是早上出生,晚上就挂了。所以其实不需要1:1来配置,而是分成3块,一块较大的和两块较小的 比为8:1:1。

每次使用的时候,都是使用一个快大的和一块小的,当垃圾收集器回收的时候,就会把这两个上面存活的对象放到另外一个小的上面。然后清理刚刚的那两个空间。 这个时候,如果继续使用的话,就会继续放到大的上面。也就是说,只会浪费10%的空间

从实际出发,其实我们不能保证每次都只有10%的对象存活,但是当它这个小的空间不够用的时候,会依赖其他内存进行分配担保。这个时候这些对象就会进入老年代。关于担保后面讲吧,哈哈,是不是很简单呢

1.4.3、标记-整理算法

​ 复制算法在存活率特别高的时候,效率就会降低,更关键的是,老年代存活率高,假如所有对象对100%存活,那么需要有额外的空间来进行担保。所以在老年代一般不能使用复制算法。

老人不是喜欢收拾东西吗,哈哈,标记整理吧

这里不是讲标记的对象之间进行清理,而是先将可用的对象都像一边移动,然后之间清理掉除它以外的内容

WX20180409-171221@2x

1.4.4、分代收集算法

当前商业虚拟机都采用这种算法来收集,这种算法将对象存活周期的不同而将Java堆分为新生代和老年代,

1、新生代总每次都有大量的对象死去,只有少量存活,就使用复制算法,这样就付出存活少量对象的复制成本就可以完成收集,

2、老年代因为存活率高,没有额外的空间为它担保就必须使用标记-清除或者是标记-整理算法。

1.5、垃圾收集器关注点和七月份

1.5.1、关注点:

@:停顿时间 (垃圾收集器垃圾的时候用户线程的停顿时间),停顿时间越短就适合需要与用户交互的程序;良好的响应速度能提升用户体验;

@:吞吐量:高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;主要适合在后台计算而不需要太多交互的任务;

@:覆盖区(Footprint):在达到前面两个目标的情况下,尽量减少堆的内存空间;可以获得更好的空间局部性;

响应时间是提交请求和返回该请求的响应之间使用的时间。示例包括:

1、数据库查询花费的时间

2、将字符回显到终端上花费的时间

3、访问 Web 页面花费的时间

吞吐量是对单位时间内完成的工作量的量度。示例包括:

1、每分钟的数据库事务

2、每秒传送的文件千字节数

3、每秒读或写的文件千字节数

4、每分钟的 Web 服务器命中数

一个例子,比如一个理发店,原先只有一个理发师,因为穷,只买的一张理发椅子,和一个长凳用来方便等待的人休息。理发师一次只能处理一个客户,其他等待的用户显得很不耐烦,外面打算进来理发的人也放弃了这家店理发的打算。。。   

有一天,理发师有钱了,他多买了2个理发椅子,这样,他可以同时给3个人理发,当其中一个人理到一定阶段需要调整或者定型的时候,他就转到另外一个客户去修剪头发,依次类推,这样,他发现一天他可以理的人数比以前增多了,但是还会有一些后来的客户抱怨等待时间太长。   

后来,理发师打算招聘2名学徒帮助他一起干活,这样,他发现每天的理发效率增加了将近2倍,而且客户的等待时间明显也减少了许多。但是成本增多了,理发用具,洗发水,发工资,这让他觉得开个理发店也要精打细算:)

1.5.2、垃圾收集器的划分

如果收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体表现,

Java虚拟机堆垃圾收集器如何实现并没有任何规定,因此不同的厂家,不同版本的虚拟机所提供的垃圾收集器可能会有很大差别,并且一般都是提供参数,用户根据自己的特定和要求组合出各个年代所用的收集器。

1.5.2.1、串行、并行、并发垃圾收集的区别

1、串行

单线程收集,进行垃圾收集时,必须暂停所有工作线程,直到完成;即会”Stop The World”; 相当于是妈妈在打扫房间,让我们乖乖在凳子上站着,等妈妈打扫完成。这种在用户不可见的情况下把用户正常的工作的线程全部关掉,这对于很多应用来说是不能够接受的

2、并行(Parallel)

 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;如ParNewParallel ScavengeParallel Old

3、并发(Concurrent)

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;    如CMS、G1(也有并行);

1.5.2.2、收集器的搭配使用

@:新生代收集器:Serial、Parallel Scavenge;ParNew、

@:老年代收集器:Serial Old、Parallel Old、CMS;

@:整堆收集器:G1;

WX20180411-142826@2x

1.6、垃圾收集器

1.6.1、Serial收集器 (串行收集器)

1.6.1.1、收集对象:新生代

1.6.1.2、采用算法:复制算法

1.6.1.3、JVM参数

 -XX:+UseSerialGC   添加该参数来显式的使用串行垃圾收集器

1.6.1.4、使用说明

进行垃圾收集时,必须暂停所有工作线程,直到完成; 相当于是妈妈在打扫房间,让我们乖乖在凳子上站着,等妈妈打扫完成。这种在用户不可见的情况下把用户正常的工作的线程全部关掉,这对于很多应用来说是不能够接受的

1、它现在依然是client模式下的虚拟机默认新生代的收集器,简单而且高效,因为它是单线程的,没有线程加护的开销,专心做事

总之 :Serial垃圾收集器在client模式下的虚拟机来说是一个不错的选择

2、在用户的桌面应用场景中,分配给虚拟机的内存不会很大,停顿时间非常少,只要这种听得不是频繁发生。这是可以接受的

1.6.2、ParNew收集器 (并行收集器)

1.6.2.1、收集对象:新生代

1.6.2.2、采用算法:复制算法

1.6.2.3、JVM参数

"-XX:+UseParNewGC":强制指定使用ParNew;    
"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

1.6.2.4、使用说明

1、其实它是serial的多线程版本,与serial相比并没有太多的创新之处,但是它是server模式下迅疾中首选的新生代收集器,但是不是默认的哦,其中有一个性能更重要的原因是,除了serial外,目前只有它能够CMS垃圾收集器配合工作

2、指定使用CMS后,会默认使用ParNew作为新生代收集器;

1.6.3、parallel Scavenge ((并行收集器,吞吐量收集器)

1.6.3.1、收集对象:新生代

1.6.3.2、采用算法:复制算法

1.6.3.3、JVM参数

-XX:+UseParallelGC   明确指定使用Parallel Scavenge收集器

1.6.3.4、使用说明

它的特点是与其他的垃圾收集器关注点不同,CMS等收集器所关注的是尽可能缩短垃圾收集器收集时候的用户线程的停顿时间,但是它的目标是达到一个可控制的吞吐量,

是JAVA虚拟机在Server模式下的默认值(比如我的电脑就是), 使用Server模式后,JDK1.5及之前,Java虚拟机使用Parallel Scavenge收集器(新生代)+ Serial Old收集器(老年代) ` JDK1.6之后有Parallel Old`收集器可搭配 的收集器组合进行内存回收。

1、吞吐量 :垃圾收集时间越少,吞吐量余越高,(高吞吐量目标,即减少垃圾收集时间,让用户代码获得更长的运行时间)

公式: 吞吐量= 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

比如虚拟机总共运行了100分支,垃圾收集花掉1分钟,那么吞吐量就是99% 高的吞吐量就是可以高效的利用cpu时间

2、使用场景 :主要适应主要适合在后台运算而不需要太多交互的任务,不是与用户交互

比如需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务等。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序; 

1.6.3.5、配合使用的JVM参数

1、-XX:MaxGCPauseMillis :设置每次年轻代垃圾回收的最长时间(更关注垃圾收集停顿时间)

“-XX:MaxGCPauseMillis” 默认值是 200 毫秒

垃圾收集器将尽可能的保证内存回收花费的时间不超过该值,不过千万不要认为把这个参数的值设置的小一点就会让垃圾收集速度变得快。

但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁,GC停顿时间的缩短是以牺牲吞吐量和和新生代空间来换取的,假如我们将这个值设置的比较小,JVM为了达到这个要求会把生代调小一下,比如由500M调成40M0,收集300M肯定比收集500M快吧,但是这也让垃圾收集变的更加频繁 。原来10秒收集一次,停顿100毫秒。现在变成5秒收集一次,每次停顿70毫秒 , 停顿时间在下降但是吞吐量也在下降,导致youngGC的频率大大增高。所以我们一般并不设定这个参数

2、-XX:GCTimeRatio:设置垃圾收集时间占总时间的比率(更关注吞吐量 )

这个值相当于就是一个吞吐量的值,默认是99 ,就是允许1%(1/1+99) 的垃圾收集时间

垃圾收集所花费的时间是年轻一代和老年代收集的总时间;如果没有满足吞吐量目标,则增加新生代的内存大小以尽量增加用户程序运行的时间;

1.6.4、Serial Old收集器 (串行收集器)

1.6.4.1、收集对象:老年代

1.6.4.2、采用算法:标记-整理

1.6.4.3、JVM参数

1.6.4.4、使用说明

主要用于Client模式,在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配)

作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

    

1.6.5、Parallel Old收集器 (并行收集器)

1.6.5.1、收集对象:老年代

1.6.5.2、采用算法:标记-整理

1.6.5.3、JVM参数

1.6.5.4、使用说明

JDK1.6及之后用来代替老年代的Serial Old收集器, 特别是在Server模式,多CPU的情况下;这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的”给力”应用组合;

1.6.6、CMS收集器(Concurrent Mark Sweep)

1.6.6.1、收集对象:老年代

1.6.6.2、采用算法:标记-清除

1.6.6.3、JVM参数

1.6.6.4、使用说明

以获取最短回收停顿时间为目标 , 并发收集(不进行压缩操作,产生内存碎片)、低停顿, 需要更多的内存(看后面的缺点)

1576228989934

使用场景:   

与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用 。CMS收集器是一种以获取最短回收停顿时间为目标的收集器;

1.6.6.5、CMS过程

1、初始标记:

在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)仅标记GC Roots能直接关联到的对象,这样极大的缩短了初始标记时间;

这个过程从垃圾回收的”根对象”开始,有两个目标

一是标记老年代中所有的GC Roots;

二是标记被年轻代中活着的对象引用的对象。

1576229270539

2、并发标记

。该阶段可以划分为三小阶段:并发标记 -> 并发预清理 -> 并发可中止预清理三个小阶段。

2.1、并发标记

对于初始标记后的所有对象,开始向下遍历,标记,此时由于GC root已经确定,GC线程已经可以和工作线程同时进行,此时已经不用STW

这个阶段会遍历整个老年代并且标记所有存活的对象,从“初始化标记”阶段找到的GC Roots开始。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。

并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

1576229435530

2.1、并发预清理

正如上一个阶段所说,对象的引用在用户应用运行过程中一直在产生变化,那么只要引用发生变化,JVM中将用于存储变化对象的这部分堆空间(Card)标记为脏的(dirty)(这一过程也叫做Card Marking)。

1577091483799

该阶段称为预清理阶段,在该阶段结束时,会把脏的Card给清理为干净的状态,如下图所示:

1577091600832

2.2、并发可终止预清理

这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同(并发标记)的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。

ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻带的引用,使得下个阶段的重新标记阶段,扫描年轻带指向老年代的引用的时间减少;

3、重新标记

但是由于并发标记阶段时间较长,在并发标记阶段还会有新的垃圾出现,

为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,发现那些被并发标记错过的对象;需要”Stop The World”。

因为并发标记是和应用程序并发执行的,在标记线程完成对某个对象的跟踪那刻,应用程序可能对对象进行了更新。 且停顿时间比初始标记稍长,但远比并发标记短;因为是采用多线程并行执行来提升效率;

4、并发清除

收集那些在标记阶段没有标记的对象,消亡对象所占的空间会被添加到释放列表里用于重新分配注意:存活的对象不会被移动。

整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;并发收集、低停顿

1.6.6.6、缺点

1、容易产生空间碎片,导致无法分配大对象,标记-清除造成

CMS回收器采用的基础算法是Mark-Sweepbi标记清理。所以CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 `CMS`不对堆空间整理压缩节约了垃圾回收的停顿时间,**但也带来的堆空间的浪费。 **

为了解决堆空间浪费问题CMS回收器不再采用简单的指针指向一块可用堆空间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作

2、需要更多的CPU资源 并发造成

为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU ,CMS收集器对CPU资源非常敏感是指在并发阶段,它虽然不会导致用户线程停顿,但因为占用一部分CPU资源,还是会导致应用程序变慢(可以处理的数据就会变慢,用户线程相当于有点停顿),总吞吐量降低。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。当然,多核多CPU也是未来的趋势!

CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,随着CPU数量的增加而下降。

3、无法处理浮动垃圾,可能出现”Concurrent Mode Failure”失败, 并发造成

浮动垃圾(Floating Garbage): 在并发清除时,由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生(一边打扫房间,一遍丢新的垃圾),这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

 这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;也要可以认为CMS所需要的空间比其他垃圾收集器大; 如果CMS预留内存空间无法满足程序需要,就会出现一次”Concurrent Mode Failure"失败,而导致另一次Full GC的产生,触发serial old Gc。 CMS的做法是老年代空间占用率达到某个阈值时触发垃圾收集,有一个参数CMSInitiatingOccupancyFraction设置一个百分比,表明达到这个值就进行垃圾回收

 "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;
      JDK1.5默认值为68%;
      JDK1.6变为大约92%;       
      
      老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能        

      CMSInitiatingOccupancyFraction参数要设置一个合理的值,设置大了,再放浮动垃圾,一下子就内存满了,所以容易导致大量”concurrent mode Failure”失败,性能反而降低,因为设置的太高表示可以容纳的浮动垃圾越多。    
      设置的小了,又会增加CMS频率,所以要根据应用的运行情况来选取一个合理的值。如果发现这两个参数设置大了会导致full gc,设置小了会导致频繁的CMS GC,说明你的老年代空间过小,应该增加老年代空间的大小了。

1.6.6.7、JVM参数

1、CMSInitiatingOccupancyFraction 上面介绍了

2、UseCMSCompactAtFullCollection CMSFullGCsBeforeCompaction,减少碎片化

UseCMSCompactAtFullCollectionCMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,默认每次GC直接压缩,也就是关键在后者上。

CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,即每次full gc都对老生代进行碎片整理压缩;这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。

1.7、内存分配以及回收策略

1.7.1、对象优先在Eden分配

复制算法还记得吧,就是说的商业虚拟机关于新生代的垃圾收集就是采用的复制算法 将内存分为3分分别为8:1:1 那么Eden 就代表着8份 ,两块Survivor区

刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,

-verbose:gc -XX:+PrintGCDetails
private static final int _1MB = 1024 * 1024;
/**
* 1、打印内存分配信息
* -verbose:gc -XX:+PrintGCDetails 
*/
public static void main(String[] args) {
    byte[] b1 = new byte[4 * _1MB];
}

gc日志

Heap
 PSYoungGen      total 75776K, used 9298K [0x000000076b600000, 0x0000000770a80000, 0x00000007c0000000)
  eden space 65024K, 14% used [0x000000076b600000,0x000000076bf14a88,0x000000076f580000)
  from space 10752K, 0% used [0x0000000770000000,0x0000000770000000,0x0000000770a80000)
  to   space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
 ParOldGen       total 173568K, used 0K [0x00000006c2200000, 0x00000006ccb80000, 0x000000076b600000)
  object space 173568K, 0% used [0x00000006c2200000,0x00000006c2200000,0x00000006ccb80000)
 Metaspace       used 3211K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 304K, capacity 388K, committed 512K, reserved 1048576K

日志分析:可以看到到内存全部分配到了eden中

1.7.1.1、分析

实例代码

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC 


参数解释
-Xms20M -Xmx20MJava堆大小为20M  不可扩展Xms表示初始Java堆大小 Xmx为Java堆最大 这里设置相等就表明不可以扩展一般建议如此设置 
-Xmn10M 表示分给新生代 下面表示分给新生到10M那么剩余的就分配给了老年代
-XX:SurvivorRatio=8 表示新生代中Eden和Survivor 比为81 其实从下面的代码的输出结果也能够看到的
private static final int _1MB = 1024 * 1024;

/**
* 1、对象优先在Eden分配
* -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
* <p>
* 参数解释:
* -Xms20M -Xmx20M:Java堆大小为20M  不可扩展(Xms表示初始Java堆大小 Xmx为Java堆最大 这里设置相等,就表明不可以扩展,一般建议如此设置)
* -Xmn10M :表示分给新生代 (下面表示分给新生到10M,那么剩余的就分配给了老年代)
* -XX:SurvivorRatio=8 :表示新生代中Eden和Survivor 比为8:1 其实从下面的代码的输出结果也能够看到的, 所以实际上新生代大小是 eden + 一个survivor= 9M  eden=8M survivor两块分别1M
*/
 public static void main(String[] args) {

        byte[] b1 = new byte[2*1024*1024];
        byte[] b2 = new byte[2*1024*1024];
        byte[] b3 = new byte[2*1024*1024];
        byte[] b4 = new byte[4*1024*1024];
        //一定要加这个,强制老年代GC
        System.gc();
    }

GC日志

非常遗憾,下面的日志是复制的别人的,我的结果和这个有稍许的出入

[GC (Allocation Failure) [DefNew: 7129K->520K(9216K), 0.0053010 secs] 7129K->6664K(19456K), 0.0053739 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [Tenured: 6144K->6144K(10240K), 0.0459449 secs] 10920K->10759K(19456K), [Metaspace: 2632K->2632K(1056768K)], 0.0496885 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]
Heap
def new generation total 9216K, used 4779K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 58% used [0x00000000fec00000, 0x00000000ff0aad38, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
Metaspace used 2638K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K

[GC (Allocation Failure) [DefNew: 7129K->520K(9216K), 0.0053010 secs] 7129K->6664K(19456K), 0.0053739 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

新生代垃圾收集完成,将新生代的放到了老年代,

[Full GC (System.gc()) [Tenured: 6144K->6144K(10240K), 0.0459449 secs] 10920K->10759K(19456K), [Metaspace: 2632K->2632K(1056768K)], 0.0496885 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]

老年代6M不清理,保持不变

Heap def new generation total 9216K, used 4779K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 58% used [0x00000000fec00000, 0x00000000ff0aad38, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000) Metaspace used 2638K, capacity 4486K, committed 4864K, reserved 1056768K class space used 281K, capacity 386K, committed 512K, reserved 1048576K

最终结果,6M最后进入了老年代,4M进入新生代eden区

在分配完,b1,b2,b3后如下所示。eden,已经分配了6M,还剩2M

1577955148336

这个时候分配b4(4M),发现eden剩余2M已经容纳不下b4了,这个时候发了两次GC,看日志应该很容易看到6M最后进入了老年代,4M进入新生代eden区

1577955138172

1.7.2、大对象直接进入老年代

-XX:PretenureSizeThreshold 默认是0,意思是不管多大都是先在eden中分配内存:

所谓的大对象其实就是需要大量连续内存空间的JAVA对象,最典型的就是那种很长的字符串和数组,大对象对于虚拟机来说是一个坏消息,(更要命的是遇到短命大对象,所以写程序的时候要尽量避免) 经常出现大对象,容易导致内存还有很多空间,就提前触发垃圾收集来获取足够的空间

JAVA虚拟机提供 XX:PretenureSizeThreshold参数用来设置大于它的直接放到老年代分配,这样的目的是避免了Eden和两个Survivor区直接发送大量的内存复制

1.7.3、分析

设置6M为大对象

-XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 
-verbose:gc -XX:+PrintGCDetails 
-XX:PretenureSizeThreshold=6M

5M不进入老年代

 public static void main(String[] args) {
        byte[] b1 = new byte[5 * _1MB];
    }
Heap
 def new generation   total 9216K, used 7919K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  96% used [0x00000000fec00000, 0x00000000ff3bbcd8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3272K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 311K, capacity 388K, committed 512K, reserved 1048576K

7M进入老年代

public static void main(String[] args) {
    byte[] b1 = new byte[7 * _1MB];
}


Heap
 def new generation   total 9216K, used 2799K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  34% used [0x00000000fec00000, 0x00000000feebbcc8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7168K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  70% used [0x00000000ff600000, 0x00000000ffd00010, 0x00000000ffd00200, 0x0000000100000000)
 Metaspace       used 3324K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 317K, capacity 388K, committed 512K, reserved 1048576K

1.7.3、长期存活的对象将进入老年代

-XX:MaxTenuringThreshold ,默认为15

虚拟机采用的是分代收集算法,java虚拟机就能够知道哪些在新生代中,哪些在老年代中。其实他对每个对象的年龄都定义了一个计数器,当对象在Ede出生并经历过地第一次Minor GC后能够进入Survivor区,会将它的年龄设置为1.每度过一次Minor GC 它的年龄就会增加1.知道增加到一定程度,

设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

1.7.3.1、分析

年龄设置为1

-XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8  
-verbose:gc -XX:+PrintGCDetails  
-XX:MaxTenuringThreshold=1  -XX:+PrintTenuringDistribution
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
    // b1可以在 SurvivorRatio 存储
    byte[]  b1 = new byte[_1MB / 4];
    byte[]  b2 = new byte[4 * _1MB];
    byte[]  b3 = new byte[4 * _1MB];
    b3 = null;
    b3 = new byte[4 * _1MB];
}

GC日志

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:    1048576 bytes,    1048576 total
: 6987K->1024K(9216K), 0.0049055 secs] 6987K->5270K(19456K), 0.0050479 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:       3488 bytes,       3488 total
: 5201K->3K(9216K), 0.0099020 secs] 9448K->5273K(19456K), 0.0099305 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4317K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0369b0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400da0, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5269K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  51% used [0x00000000ff600000, 0x00000000ffb25788, 0x00000000ffb25800, 0x0000000100000000)
 Metaspace       used 3265K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 311K, capacity 388K, committed 512K, reserved 1048576K

第一次GC之后,survior中有一个年龄为1的1048576 bytes,并且它的大小超过了survivor期望 大小524288,下次GC就会被移动到老年代,

4.4、动态对象年龄判断

为了更好适应不同程序上的内存状态,虚拟机并不是永远要求达到MaxTenuringThreshold,如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor的一半,年龄大于它的直接进入老年代。无需等待

/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
 * -XX:+PrintTenuringDistribution
 */
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
    byte[] allocation1, allocation2, allocation3, allocation4;
    allocation1 = new byte[_1MB / 4];   // allocation1+allocation2大于survivo空间一半
    allocation2 = new byte[_1MB / 4];
    allocation3 = new byte[4 * _1MB];
    allocation4 = new byte[4 * _1MB];
    allocation4 = null;
    allocation4 = new byte[4 * _1MB];
}

4.5、空间分配担保

在发生Minor GC之前,虚拟机会先检查 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

ContactAuthor