垃圾收集器与内存分配策略

目录

垃圾收集器与内存分配策略

一、如何判断对象是否存活

1、引用计数算法

2、可达性分析算法

3、对象是否已死?

二、垃圾收集器算法

1、分代收集理论

2、标记-清除算法

3、标记-复制算法

4、标记-整理算法

5、安全点和安全区域

三、垃圾收集器

1、Serial收集器

2、ParNew收集器

3、Parallel Scavenge收集器

4、CMS收集器

5、Garbage First收集器

6、ZGC收集器

7、如何选择垃圾收集器

8、GC日志查看

四、内存分配与回收策略

1、对象优先在Eden分配

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

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

4、动态对象年龄判断

5、空间分配担保


垃圾收集器与内存分配策略

对于我们Java程序员来说,垃圾收集?我们写代码的时候从来没有处理过垃圾收集啊,而且程序也运行的很稳定呀。
那是因为我们的jvm虚拟机已经帮我们把那些无用的垃圾对象给处理掉了,无需我们程序员再来处理那些垃圾对象。
设想一下,如果我们在程序中创建的对象没有被销毁掉,我们的内存就那么大,那么内存很快就会被占满,程序也就无法继续运行下去了,这样很明显是不行的。
那么问题就来了,Java程序会创建出大量的对象,jvm虚拟机是怎么知道哪些是还在用的,哪些是可以销毁掉的呢?
那么我们就来看下jvm虚拟机是怎么识别哪些对象是无用的对象,要被销毁掉,哪些对象还得继续保留,销毁掉相应的无用对象后,后续内存的分配又是如何处理的。

 

一、如何判断对象是否存活

1、引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用这个对象的时候,这个计数器就加一,当这个引用失效的时候,计数器就减一,任何时刻计数器为零的对象就是不可能在被使用的。
这个算法的原理简单,效率很高,但是主流的java虚拟机都没有用这个算法来管理内存,因为这个算法有很多例外的情况要考虑,需要配合大量的额外处理才能保证正确的工作,比如相互循环引用的情况就无法用这个算法直接来解决。
根据这个示例来看,除了gc1和gc2相互引用,后面都无任何引用,实际上两个对象都已经不可能再被访问,但是由于相互引用,导致计数器都不为零,则引用计数算法无法回收他们。
7097K->3000K(15872K) 由日志可以看出,有4M内存被腾出来了,同时也说明了Java虚拟机不是通过引用计数算法来判断对象是否存活的。

代码示例:

/**
 * @ClassName ReferenceGC
 * @Author chenxuan
 */
public class ReferenceGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;
    //占点内存,能够在日志里看的比较清除
    private byte[] bytes = new byte[2 * _1MB];

    /**
     * gc后看新创建的gc1和gc2是否被回收
     * @Author chenxuan
     **/
    public void test(){
        ReferenceGC gc1 = new ReferenceGC();
        ReferenceGC gc2 = new ReferenceGC();

        gc1.instance = gc2;
        gc2.instance = gc1;

        gc1 = null;
        gc2 = null;
        //模拟发生gc
        System.gc();
    }

    /**
     * VM Args: -XX:+PrintGCDetails
     * 打印GC日志详情
     * @Author chenxuan
     **/
    public static void main(String[] args) {
        ReferenceGC gc = new ReferenceGC();
        gc.test();
    }
}

控制台输出:

Connected to the target VM, address: '127.0.0.1:52574', transport: 'socket'
    [GC (Allocation Failure) [DefNew: 4280K->512K(4928K), 0.0012588 secs] 4280K->953K(15872K), 0.0012942 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [GC (Allocation Failure) [DefNew: 4694K->0K(4928K), 0.0024762 secs] 5135K->5049K(15872K), 0.0024979 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
    [Full GC (System.gc()) [Tenured: 5049K->3000K(10944K), 0.0014451 secs] 7097K->3000K(15872K), [Metaspace: 2051K->2051K(4480K)], 0.0014752 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    Heap
    def new generation   total 4992K, used 179K [0x05200000, 0x05760000, 0x0a750000)
    eden space 4480K,   4% used [0x05200000, 0x0522ce58, 0x05660000)
    from space 512K,   0% used [0x05660000, 0x05660000, 0x056e0000)
    to   space 512K,   0% used [0x056e0000, 0x056e0000, 0x05760000)
    tenured generation   total 10944K, used 3000K [0x0a750000, 0x0b200000, 0x15200000)
    the space 10944K,  27% used [0x0a750000, 0x0aa3e300, 0x0aa3e400, 0x0b200000)
    Metaspace       used 2055K, capacity 2306K, committed 2368K, reserved 4480K
    Disconnected from the target VM, address: '127.0.0.1:52574', transport: 'socket'

 

2、可达性分析算法

这个算法的思路就是通过一系列称为"GC Roots"的根对象作为其实节点集,从这些节点开始,根据引用关系往下搜索,搜索过程所走的路径称为"引用链",如果某个对象到GC Roots间没有任何引用链相连,那么这个对象就是不可能再被使用的,就是可以回收的对象。
GC Roots的对象大致包括以下几种:
1)在虚拟机栈中引用的对象,如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量
2)在方法区中类静态属性引用的对象,如Java类引用类型静态变量
3)在方法区中常量引用对象,如字符串常量池里的引用
4)在本地方法栈中JNI,如基本数据类型对应的Class对象,还有系统类加载器
5)所有被同步锁(synchronized关键字)持有的对象
6)反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存等。

Java中堆引用的概念有4种,
1)强引用:最传统的引用,如Object obj = new Object(); 这种。无论在任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。
2)软引用:用来描述一些还有用,但非必须的对象,只被软引用关联着的对象,在系统将要发生内存溢出异常前,才会把这些对象进入回收返回进行第二次回收。
3)弱引用:描述那些非必须对象,但是他的强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
4)虚引用:最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无发通过虚引用来取得一个对象实例。

 

3、对象是否已死?

即使在可达性分析算法中已经判定当前对象为可回收对象时,也不是立马进行回收,只是会进行标记,会经过2次的标记,才会真正的进行对象回收。
第一次是可达性分析算法筛选后对可回收对象进行第一次标记。
第二次是看这些对象中是否有重写finalize()方法,如果有,那么进行第二次筛选标记,如果在finalize()方法中再次将该对象救活,那么该对象就不会被垃圾收集器回收。

代码示例:

/**
 * 对象被垃圾回收前的一次自我拯救 finalize
 * @Author chenxuan
 */
public class FinalizeGC {

    public static FinalizeGC gc = null;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("execute finalize");
        //对该对象进行重新赋值,使其成为不可回收对象
        FinalizeGC.gc = this;
    }

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

        //gc置为null,使可达性分析算法中该对象没有引用链,认为是可回收对象
        gc = null;
        //进行GC
        System.gc();
        //因为finalize方法优先级很低,所以等一会
        TimeUnit.MILLISECONDS.sleep(500);
        if(gc != null){
            System.out.println("this is alive");
        }else {
            System.out.println("this is dead");
        }

        //再一次置空
        gc = null;
        //进行GC
        System.gc();
        //因为finalize方法优先级很低,所以等一会
        TimeUnit.MILLISECONDS.sleep(500);
        //这一次该对象被回收了。因为finalize只会被执行一次
        if(gc != null){
            System.out.println("this is alive");
        }else {
            System.out.println("this is dead");
        }
    }
}

控制台输出

execute finalize
this is alive
this is dead

对于finalize()方法,我查阅相关文档,是不推荐使用的,尽量避免使用,finalize所能做的使用try-finally或者其他方式都可以做的更好、更及时。

 

二、垃圾收集器算法

刚介绍完了如何判断一个对象是否要被回收,那么接下来再看看虚拟机是如何回收的

1、分代收集理论

 1)弱分代假说:绝大多树对象都是朝生夕灭的。
 2)强分代假说:熬过越多次垃圾收集过程的对象就越难被回收。
 3)跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
基于1、2两个假说,因此收集器将Java堆划分出不同的区域,所以才有了大家熟知的新生代(Eden空间,From Survivor空间,To Survivor空间),老年代。
因此也有了对应区域的收集划分,新生代收集("Minor GC/Young GC",收集新生代中的可回收对象),老年代收集("Major GC/Old GC",收集老年代中的可回收对象,CMS收集器),混合收集("Mixed GC",收集整个新生代及部分老年代的可回收对象,G1收集器),整堆收集("Full GC",收集整个Java堆和方法区中的可回收对象)。

2、标记-清除算法

标记-清除算法是最基础的垃圾收集算法。分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
缺点:
1)执行效率不稳定,如果包含大量对象,大部分是可回收对象,这个时候就要进行大量的标记和清除动作,对象越多,那么效率就会越低。
2)内存空间碎片化的问题,标记-清除后会有大量不连续的空间碎片,空间碎片过多的化可能会导致以后的程序在运行是需要分配较大对象是无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

3、标记-复制算法

将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块的内存用完了,就将还存活的对象复制到另一块上面,然后把之前那一块的内存空间全部清理掉。
缺点:这种算法的代价是将可用内存缩小为了原来的一半,资源的大大浪费。
因为这种算法对少量存活对象的情况来说运行效率很高,而年轻代中大部分的对象都是朝生夕灭的,所以比较适合年轻代的垃圾收集。
这里的话顺便介绍一下常见Java堆中新生代的内存布局吧
把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集是,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和刚用过的那块Survivor空间。HotSpot虚拟机默认的Eden和Survivor比例是8:1,即每次新生代中可用的内存空间为整个新生代空间的90%,因为有2个Survivor空间,则刚好是8:1:1。

4、标记-整理算法

标记-复制算法在对象存活率较高时,每次就要复制大量的对象,那么效率则会降低,而且会浪费50%的空间。像老年代中所有的对象几乎都是不可回收的,那么就得考虑另外一种算法了。
首先标记出所有需要回收的对象,在标记完成后,让所有存活的对象都向内存空间一端移动,然后清理掉边界以外的内存。与标记-清除算法的区别的,在标记后,清理前会将活着的对象进行整理到一起,然后再清理剩下的空间。
缺点:因为需要移动存活的对象,那么在移动对象的这个时候程序全部暂停一下,因为需要移动对象的话,假如我刚好在移动完A对象,删除原来的空间,程序还在正常运行,去获取A对象信息的时候,发现原来内存的A对象不见了,那么程序这个时候就会报错,这个肯定是不行的。这个暂停应用程序俗称"stop the world"。

5、安全点和安全区域

上面介绍了对垃圾的收集算法。比如需要停止应用程序,或者切换cpu时间片的时候,我们的程序随意任意一个指令的地方停止,会需要大量的存储空间来存储相应的停止位置和指令。
安全点:是决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。比如循环跳转的位置,方法调用的位置等等。
安全区域:既然有了安全点,为什么还要安全区域呢?因为有些时候程序是短时间内到达不了相应的安全点的,比如线程处于sleep状态或者blocked状态,那么这个时候不可能一直等待这个线程走到相应的安全点,那么这个时候就需要安全区域来解决这个问题了。
当线程执行到安全区域中的代码时,会标识自己已经进入了安全区域,那么当这段时间内发生了垃圾收集时,则不用再去管那些已经标识了在安全区域内的线程了。如果线程要离开安全区域,此时还在垃圾收集,那么就会暂停在那,等待垃圾收集完后再继续运行。

 

三、垃圾收集器

1、Serial收集器

启动参数:-XX:+UseSerialGC -XX:+UseSerialOldGC
是一个单线程工作的收集器,不仅仅只会使用一个线程去完成垃圾收集工作,在他进行垃圾收集时,必须暂停其他所有工作线程,直到他收集结束。
新生代采用复制算法,老年代采用标记-整理算法。
Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

2、ParNew收集器

启动参数:-XX:+UseParNewGC
实质上就是Serial收集器的多线程并行版本。
新生代采用复制算法,老年代采用标记-整理算法。

3、Parallel Scavenge收集器

启动参数:-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)
该收集器的目标是达到一个可控的吞吐量。所谓的吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:吞吐量=运行代码时间/(运行代码时间+垃圾收集时间)
垃圾收集停顿时间参数:-XX:MaxGCPauseMillis
吞吐量大小参数:-XX:GCTimeRatio ,该参数的值是0-100之间的整数。
根据不同的系统,不同的内存大小,不同的程序合理的设置参数值。
新生代采用复制算法,老年代采用标记-整理算法。

4、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种与获取最短回收停顿时间为目标的收集器。
整个过程分为4个步骤
  1)初始标记:只是标记一下GC Roots能够直接关联到的对象,速度很快。
  2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  3)重新标记:为了修正并发标记期间,因程序继续运行而导致标记产生变动的那一部分对象的标记记录。因为并发标记是与垃圾收集线程一起并发运行的,所以会导致一部分对象有变动
  4)并发清除:清理删除掉标记阶段已经标记为可回收的对象,由于不需要移动存活对象,所以这个阶段也是可用和用户线程同时并发的
在初始标记和重新标记这两个步骤仍然是需要stop the world 的。

缺点:
  1)在并发阶段,虽然不会导致用户线程停顿,但是他会占用一部份线程而导致程序变慢,降低了吞吐量。
  2)在并发标记和并发清理步骤中,用户线程还在继续运行,程序就会产生新的可回收的垃圾对象,这一部分垃圾对象是出现在标记重新标记后的,那么在并发清除步骤是就不会将这新产生的垃圾对象进行回收,只能留在下次垃圾收集的时候再清理。这段时间产生的垃圾对象也称为浮动垃圾。
  3)因为CMS是基于标记-清除算法实现的收集器,就意味着收集结束后会出现大量的空间碎片,空间碎片过多时,以后对大对象的分配就带来很大困难,当连续空间不够来分配这个大对象时,则会触发一次Full GC。JDK9之前可以通过-XX:CMSFullGCsBeforeCompaction来整理内存空间。

CMS的相关核心参数
1. -XX:+UseConcMarkSweepGC:启用cms
2. -XX:ConcGCThreads:并发的GC线程数
3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短stop the world
9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短stop the world;

5、Garbage First收集器

G1(Garbage First)收集器与之前介绍的收集器有很大不同。是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
是基于Region的堆内存布局是实现这个收集器的关键。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都要根据需要,扮演新生代的Eden、Survivor空间或者老年代空间。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。只要是对象大小超过Region容量的一半就认为是大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB-32MB,且为2的N次幂。
一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M。


G1收集器也是可以根据-XX:MaxGCPauseMillis 参数来设定垃圾收集而stop the world 的时间。
整个过程分为4个步骤:
  1)初始标记:暂停所有的其他线程,只是标记一下GC Roots能够直接关联到的对象,速度很快
  2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  3)最终标记:为了修正并发标记期间,因程序继续运行而导致标记产生变动的那一部分对象的标记记录。因为并发标记是与垃圾收集线程一起并发运行的,所以会导致一部分对象有变动
  4)筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来指定回收计划。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成。回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。

G1垃圾收集分类
YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

MixedGC 不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

Full GC 停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。

G1收集器参数设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为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:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

什么场景适合使用G1
1. 50%以上的堆被存活对象占用
2. 对象分配和晋升的速度变化非常大
3. 垃圾回收时间特别长,超过1秒
4. 8GB以上的堆内存(建议值)
5. 停顿时间是500ms以内

6、ZGC收集器

启用参数:-XX:+UseZGC
是在JDK11中新加入的低延迟的垃圾收集器。
内存布局与G1一样,采用Region的堆内存布局,分为大中小三类容量:
  1)小型Region:容量固定为2MB,用于存放小于256KB的小对象
  2)中型Region:固定容量是32MB,用于放至大于等于256KB但小于4MB的对象
  3)大型Region:容量不固定,可以动态变化,但必须是2MB的整数倍,用于放至4MB及以上的大对象。
运作过程分为4个大阶段:
  1)并发标记:与G1相同
  2)并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集。
  3)并发重分配:是ZGC的核心阶段,要把重分配集中的存活对象复制到新的Region上,并重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
  4)并发重映射:重映射所做的就是修正整个堆中指向重新分配集中旧对象的所以引用。

7、如何选择垃圾收集器

1. 优先调整堆的大小让服务器自己来选择
2. 如果内存小于100M,使用串行收集器
3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选
5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器
6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

8、GC日志查看

1)GC基本信息:jdk9之前-XX:+PrintGC jdk9之后-Xlog:gc:
2)GC详情信息:jdk9之前-XX:+PrintGCDetails jdk9之后-Xlog:gc*
3)查看GC前后堆、方法区可用容量变化:jdk9之前-XX:+PrintHeapAtGC jdk9之后-Xlog:gc+heap=debug:
4)查看GC过程中用户线程并发时间及停顿时间:jdk9之前-XX:+PrintGCApplicationConcurrentTime 以及-XX:+PrintGCApplicationStopTime jdk9之后-Xlog:safepoint:
5)查看收集器个分代区域大小,自动调节的相关信息:jdk9之前-XX:+PrintAdaptiveSizePolicy jdk9之后-Xlog:gc+ergo*=trace:
6)查看熬过收集后剩余对象的年龄分布信息:jdk9之前-XX:+PrintTenuringDistribution jdk9之后-Xlog:gc+age=trace:

 

四、内存分配与回收策略

1、对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配对象时,虚拟机将发起一次Minor GC

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

大对象就是指需要大量连续的内存空间的Java对象,最典型的大对象就是很长的那种字符串(图片转base64后),或者元素数量很庞大的数组。
-XX:PretenureSizeThreshold参数(只对Serial和ParNew收集器有效),指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Surivor区之间来回复制,产生大量内存复制操作。

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

虚拟机中多数收集器都是采用分代(新生代,老年代)收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活的对象放在老年代中。
对象通常在Eden区中诞生,如果经过第一次Monir GC后仍然存活,并且能被Survivor容纳的话,该对象会被移到另一个Survivor区中,并且起对象年龄设置为1.对象在Survivor区中每熬过一次Monir GC,年龄就会加1,当他的年龄到达一定的程度(默认15),就会将该对象放至老年代中。这个升级值老年代的阈值,可以通过-XX:MaxTenuringThreshold参数设置。

4、动态对象年龄判断

如果在Survivor空间中低于或等于某年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可用直接进入老年代。

5、空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么这一次的Minor GC可用确保时安全的。防止所有对象都进入老年代。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值