jvm_2_垃圾回收

如何判断对象可以回收

引用计数法

循环引用

image-20210911105425373

引用计数法即某对象被引用一次,则引用次数加1,当引用次数为0时,则被回收。

但是出现上图循环引用则无法被回收。

可达性分析算法

  • java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。

  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收

  • 需要确定根对象,即肯定不会被回收的对象。

  • 垃圾回收前,会对堆中所有的对象进行扫描,只有没有被根对象直接或者间接引用的对象才会被回收。

  • 举个例子,当提起一串葡萄,连在根上的没有掉下来的葡萄即被根对象引用的对象,不能被回收;掉在盘子里的葡萄即可以被回收的对象。

示例

/**
 * 演示GC Roots
 */
public class Demo2_2 {

    public static void main(String[] args) throws InterruptedException, IOException {
        List<Object> list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        System.out.println(1);
        System.in.read();

        list1 = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end...");
    }
}

运行程序,分别生成垃圾回收前后的dunp文件

jps // 查看进程号
// format=b 生成文件格式为二进制;live主动触发垃圾回收,保留存活对象;file表示存放位置
jmap -dump:format=b,live,file=1.bin 进程号51125

list1置空前,生成dunmp文件1.bin

image-20210911114300327

List<Object> list1 = new ArrayList<>();

list1是局部变量,存在于活动栈帧;new ArrayList<>()产生的对象才是存在于堆中的对象。即此处new ArrayList<>()对应的那个对象才能作为根对象。

list1置空后,生成dunmp文件2.bin

image-20210911114535971

因为在执行

jmap -dump:format=b,live,file=2.bin 51125

使用了live参数,主动调用了垃圾回收。由于list1被置空,list对象无人引用,所以被垃圾回收了。所以在根对象中找不到了。

四种引用

  1. 强引用
  2. 软引用
  3. 弱引用
  4. 虚引用
  5. 终结器应用

特点

  1. 强引用
    • 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
  2. 软引用(SoftReference)
    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
    • 可以配合引用队列来释放软引用自身
  3. 弱引用(WeakReference)
    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
    • 可以配合引用队列来释放弱引用自身。
  4. 虚引用(PhantomReference)
    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
  5. 终结器引用(FinalReference)
    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()方法,第二次GC时才能回收被引用对象。

图例

image-20210911154819757

强引用对象回收

image-20210911155008018

软引用对象回收

image-20210911155152121

image-20210911155706625

此时A2对象可能被回收。

  • A2对象仅仅只被软引用对象引用
  • 在执行GC时,内存空间不足了,才会被垃圾回收
  • 回收后,软引用对象本身可以通过进入引用队列进行释放
弱引用对象的回收

image-20210911155448697

此时A3对象可能会被回收

  • A3对象仅仅被弱引用对象引用
  • 当执行GC时,无论内存是否不足,都会被垃圾回收
  • 回收后,弱引用对象本身可以通过进入引用队列进行释放
虚引用对象的回收

image-20210911155912137

image-20210911155959727

虚引用一般是对直接内存分配的应用。

  • 当声明ByteBuffer时,ByteBuffer会分配一块直接内存,并把直接内存的地址传递给虚引用对象Cleaner。

  • 当ByteBuffer不再被强引用时,被回收后,直接内存还没有被释放。这时会将虚引用放入虚引用的引用队列,由Reference Handler线程监控,发现虚引用对象,调用虚引用相关方法Unsafe.freeMemory释放直接内存。

终结器引用的回收

image-20210911161140742

  • 所有的类都继承自Object类,里面有一个终结方法finalize()方法,当对象重写了finalize()方法,且没有强引用引用它时,它就可以被当成垃圾进行垃圾回收。
  • 当对象没有被强引用时,会由jvm为该对象创建一个对应的终结器引用。当这个对象被垃圾回收时,会将终结器引用加入引用队列,但是对象不会被垃圾回收。
  • 再由一个优先级较低的Finalizer线程去监控引用队列是否有终结器引用,如果有,就通过终结器引用找到A4对象,调用其finalize()方法,等调用之后,等下一次垃圾回收时,就可以被垃圾回收了。
  • 工作效率低,第一次GC不会回收对象,先将终结器引用入队,等到第二次垃圾回收才有可能被回收。

代码示例

软引用
# 虚拟机参数
-Xmx20m -XX:+PrintGCDetails -verbose:gc
/**
 * 演示软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_3 {

    private static final int _4MB = 4 * 1024 * 1024;



    public static void main(String[] args) throws IOException {
        /*List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }

        System.in.read();*/
        soft();


    }

    public static void soft() {
        // list --> SoftReference --> byte[]

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

image-20210911161930580

/**
 * 演示软引用, 配合引用队列
 */
public class Demo2_4 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) {
            // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }

    }
}

image-20210911162312048

弱引用
# 虚拟机参数
-Xmx20m -XX:+PrintGCDetails -verbose:gc
/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();

        }
        System.out.println("循环结束:" + list.size());
    }
}

image-20210911162751214

垃圾回收算法

标记清除

image-20210911172543719

将没有被引用的对象标记出来,然后清除。这里的清除并不是把内存空间置零操作,而是把这些空间记录下来,待后面分配空间的时候,去寻找是否有空闲的空间,然后进行覆盖分配。

优点:速度较快

缺陷:清除的空间比较零碎,当待分配的新对象过大,即使零碎空间加起来总共是够的,但是由于过于零散,所以无法对其进行分配。

标记整理

image-20210911173213209

优点:没有内存碎片,连续空间比较充足

缺点:涉及到地址的改变,开销大,效率低

复制

image-20210911173423835

image-20210911173456246

image-20210911173545768

优点:不会有内存碎片

缺陷:始终会占用双倍的内存空间

分代垃圾回收

image-20210914152909431

  • jvm将堆分为了新生代和老年代。
  • 对新生代的垃圾回收更加频繁,对老年代的垃圾回收频率低一些
  • 新生代主要存放临时的、迭代快的对象,老年代存放仍然经常使用的对象
  • 新生代类似每天打扫,老年代类似大扫除

image-20210914154200284

  • 对象首先分配在伊甸园区域。
  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from 和 to 所指向的空间,即始终让to空间保持空闲。
  • minor gc 会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。
  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长
  • 如果仍然不足,会抛出OutOfMemory异常。

相关VM参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn或(-XX:NewSize=size±XX:MaxNewSize=size)
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC-XX:+ScavengeBeforeFullGC

实例

# 虚拟机参数
-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
/**
 *  演示内存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {
        
    }
}

image-20210914163100944

当分配空间后

public static void main(String[] args) throws InterruptedException {
    ArrayList<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
}

image-20210914164808294

再次分配

public static void main(String[] args) throws InterruptedException {
    ArrayList<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    list.add(new byte[_512KB]);
    list.add(new byte[_512KB]);
}

image-20210914165616334

大对象直接晋升到老年代

public static void main(String[] args) throws InterruptedException {
    ArrayList<byte[]> list = new ArrayList<>();
    list.add(new byte[_8MB]);
}

image-20210914170604688

当对象过大,新生代放不下后,不会触发GC,会直接放到老年代。当老年代也不足时,会OOM异常。

进程内的OOM不会影响到主线程的运行

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);
    }).start();

    System.out.println("sleep....");
    Thread.sleep(1000L);
}

image-20210914170902360

垃圾回收器

串行

  • 底层是一个单线程的垃圾回收器

  • 适合堆内存较小,cpu数量少,适合个人电脑

吞吐量优先

  • 多线程

  • 适合堆内存较大的场景

  • 需要多核cpu支持(否则多线程争强一个cpu效率低)

  • 让单位时间内,STW的时间最短

    0.2 + 0.2 = 0.4

响应时间优先

  • 多线程

  • 适合堆内存较大

  • 需要多核cpu

  • 尽可能让单次STW的时间最短

    0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5

串行

# 虚拟机参数
-XX:+UseSerialGC=Serial+SerialOld

串行垃圾回收器分为两个部分,分开运行的。新生代空间不足了触发Serial完成MinorGC,老年代空间不足了触发SerialOld完成FullGC

  • Serial
    • 工作在新生代
    • 复制算法
  • SerialOld
    • 工作在老年代
    • 标记整理算法

image-20210914172536866

吞吐量优先

# 虚拟机参数
# 并行
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n

image-20210914173554990

image-20210914173602885

  • parallel并行,指的是,多个垃圾回收器可以并行的运行,占用不同的cpu。但是在此期间,用户线程是被暂停的,只有垃圾回收线程在运行。

响应时间优先

# 虚拟机参数
# 并发
-XX:+UseConcMarkSweepGC~ -XX:+UseParNewGC~SerialOld
-XX:ParallelGCThreads=n~ -XX:ConcGCTreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

image-20210914192429184

  • Concurrent 并发、MarkSweep标记清除。基于标记清除且支持并发的一个垃圾回收器
  • 并发意味着垃圾回收时,其他的用户线程也可以并发运行,与垃圾回收线程抢占cpu
  • CMS在垃圾回收的某些阶段是不需要STW,进一步减少需要STW的时间
  • UseConcMarkSweepGC是工作在老年代的垃圾回收器,与之对应的是UseParNewGC,工作在新生代的垃圾回收器,基于复制算法。
  • CMS回收器有时候会发生并行失败的情况,这时候CMS回收器会退化成SerialOld的单线程的基于标记整理的垃圾回收器。

CMS老年代回收过程

image-20210914193509273

  • 当老年代空间不足时,所有进程运行到安全点暂停,然后垃圾回收的线程进行初始标记,初始标记比较快,只是标记根对象。此过程会Stop The World,阻塞其他用户线程。
  • 之后达到下一个安全点,其他用户线程也可以继续运行了,此时垃圾回收线程进行并发标记,即可以跟其他用户线程并发工作,然后讲垃圾标记出来。此过程不会STW
  • 达到下一个安全点后,进行重新标记,因为上一个并发标记时,其他用户线程也在并发执行,所有可能会产生新对象新引用,对垃圾回收线程造成了干扰,需要重新标记。此过程会STW
  • 到下一个安全点后,其他用户进程恢复,垃圾回收线程开始并发地清理垃圾,恢复运行。

细节

  • 垃圾回收的并发数受参数影响。
    • -XX:ParallelGCThreads=n 表示并行的垃圾回收线程数,一般跟cpu数目相等
    • -XX:ConcGCTreads=threads 并发的垃圾回收线程数目,一般是ParallelGCThreads的 1/4。即一个cpu做垃圾回收,剩下3个cpu留给人家用户线程。
  • CMS垃圾回收器对cpu的占用率并不高,但是用户线程不能完全占用cpu,吞吐量变小了。
  • CMS在执行最后一步并发清理的时候,由于其他线程还在运行,就会产生新的垃圾,而新的垃圾只有等到下次垃圾回收才能清理了。这些垃圾被称为浮动垃圾
  • 所以要预留一些空间来存放浮动垃圾。
  • -XX:CMSInitiatingOccupancyFraction=percent,开始执行CMS垃圾回收时的内存占比,早期默认65,即只要老年代内存占用率达到65%的时候就要开始清理,留下35%的空间给新产生的浮动垃圾。
  • -XX:+CMSScavengeBeforeRemark。在重新标记阶段,有可能新生代的对象会引用老年代的对象,重新标记时需要扫描整个堆,做可达性分析时,只要新生代的引用存在,不管有没有必要,都会通过新生代引用找到老年代,但是这其实对性能影响有些大。因为新生代对象很多,且很多要作为垃圾被回收。可达性分析又会通过新生代引用去找老年代,但是就算找到了老年代,这些新生代还是要被回收,也就是说,本来没有必要查找老年代。所以在重新标记之前,把新生代先回收了,就不会存在新生代引用老年代,然后去查找老年代了。
  • 新生代的回收是通过-XX:+UseParNewGC,垃圾回收之后,新生代对象少了,自然重新标记的压力就轻了。
  • 因为CMS基于标记清除,有可能会产生比较多的内存碎片。这样的话,会造成将来给对象分配空间时,空间不足时,如果minorGC后内存空间也不足。那么由于标记清除,老年代的空间也不足,造成并发失败。于是CMS退化成SerialOld串行地垃圾回收,通过标记整理,来得到空间。但是这样会导致垃圾回收的时间变得很长(要整理),结果本来是响应时间优先的回收器,响应时间长,给用户造成不好的体验。

G1

定义:Garbage First

  • 2004论文发布
  • 2009JDK6u14体验
  • 2012JDK7u4官方支持
  • 2017JDK9默认

使用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region(每个Region可以分新生代老年代)
  • 整体上是标记+整理算法,两个区域之间是复制算法

相关JVM参数

-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

G1垃圾回收阶段

image-20210915112021863

新生代的回收之后,可以在进行新生代回收时,同时并发标记,然后再进行混合垃圾回收,即对新生代、老年代都进行一次较大的垃圾回收。

Young Collection

  • 会STW(时间较短)

image-20210915112537122

image-20210915113142967

image-20210915113421900

Young Collection +CM

  • 新生代的垃圾回收+并发标记阶段
  • 在YoungGC时会进行GC Root的初始标记
  • 并发标记,顺着GC Roots找到其他对象
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent(默认45%

image-20210915114111537

Mixed Collection

混合收集, 会对E、S、O进行全面垃圾回收

  • 最终标记(Remark)会STW
  • 拷贝存活(EVacuation)会STW
-XX:MaxGCPauseMillis=ms

image-20210915152035617

  • 因为在此之前有并发标记,所以需要最终标记并拷贝存活
  • 新生代的存活对象通过复制算法到幸存区
  • 幸存区的一些达到晋升寿命的对象,晋升到老年代
  • 对于老年代的垃圾回收来说,并不会全部回收。为了达到暂停时间短(STW),会优先让一部分垃圾回收价值高的老年代回收。与MaxGCPauseMillis参数有关

Full GC

  • SerialGC
    • 新生代内存不足发生的垃圾收集 ——minor gc
    • 老年代内存不足发生的垃圾收集 ——full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 ——minor gc
    • 老年代内存不足发生的垃圾收集 ——full gc
  • CMS
    • 新生代内存不足发生的垃圾回收 ——minor gc
    • 老年代内存不足
    • 当垃圾回收速度跟不上垃圾生成速度时,会full gc
    • 并发收集失败前是minor gc,并发失败退化为串行垃圾收集器,触发full gc
  • G1
    • 新生代内存不足发生的垃圾回收 ——minor gc
    • 老年代内存不足
    • 当垃圾回收速度跟不上垃圾生成速度时,会full gc
    • 并发收集失败前是minor gc,并发失败退化为串行垃圾收集器,触发full gc

Young Collection跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题

image-20210915154920105

  • 老年代会再细分,会形成一个卡表,老年代里引用新生代的对象会被标记为dirty card。
  • 卡表与Remember Set(标记为dirty card的集合)
  • 在引用变更时通过post-write barrier + dirty card queue
  • concurrent refinement threads 更新 Remember Set

Remark

  • 重标记

  • pre-write barrier + satb_mark_queue

image-20210915155443865

黑色:已处理完成,不会被垃圾回收

白色:已处理完成,会被垃圾回收

灰色:正在处理

image-20210915155930677

B会被标记为黑色

因为是并发标记,其他用户线程在工作,可能将C的引用改变

image-20210915160019674

C已经被处理过了,本来应该被直接回收,但是显然C被其他用户线程改变引用了。所以需要对对象的引用进行进一步检查。

当对象的引用发生改变时,jvm会对该对象加上写屏障pre-write barrier,会把对象C加入到一个队列satb_mark_queue,并且标记为灰色,表示正在处理。

等到并发标记结束,进行重新标记,会STW,暂停其他线程,然后将线程里的对象取出来检查,发现对象是灰色,还需要进行处理。在处理的时候发现,有强引用引用该对象,那么标记为黑色,即处理结束且不会被回收。

JDK 8u20字符串去重

  • 优点:节省大量空间
  • 缺点:略微多占用了cpu空间,新生代回收时间略微增加
-XX:+UseStringDeduplication
String s1 = new String("hello"); //char[]{'h','e','l','l','o'}
String s1 = new String("hello"); //char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个char[]
  • 注意,与String.intern()不一样
    • String.intern()关注的是字符串对象
    • 而字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串表

JDK 8u40并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。

-XX:+ClassUnloadWithConcurrentMark # 默认启用

JDK 8u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。

image-20210915164307591

JDK 9并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为FullGC
  • JDK9之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

目的是尽早地开始垃圾回收,避免Full GC的发生。

JDK 9更高效的回收

  • 250+增强
  • 180+bug修复
  • https://docs.oracle.com/en/java/javase/12/gctuning

垃圾回收调优

  • 掌握GC相关的VM参数,会基本的空间调整
  • 掌握相关工具
  • 调优跟应用、环境有关,没有放之四海皆准的法则
public class Demo2_8 {
    public static void main(String[] args) {

    }
}
# 查看虚拟机运行参数
-XX:+PrintFlagsFinal -version | findstr "GC"

image-20210915172646971

调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1,ZGC(响应时间优先)
  • ParallelGC(高吞吐量)
  • Zing(号称0停顿)

最快的GC是不发生GC

查看FullGC前后的内存占用,考虑下面几个问题

  • 数据是不是太多
    • resultSet = statement.executeQuery(“select * from 大表”),加载到堆内存应该limit,避免把不必要的数据加载到java内存中
  • 数据表示是否太臃肿
    • 对象图(用到对象的哪个属性就查哪个)
    • 对象大小 至少16字节,Integer包装类型24字节,而int 4字节
  • 是否存在内存泄露
    • static Map map作为缓存等,静态的,长时间存活的对象,一直添加,会造成OOM
    • 可以用软引用、弱引用
    • 可以使用第三方的缓存实现,redis等,不会给java堆内存造成压力

新生代调优

新生代的特点

  • 所有的new操作的内存分配非常廉价
    • TLAB thread-local allocation buffer
  • 死亡对象的回收代价是零
    • 复制算法,复制之后直接释放空间,不整理
  • 大部分对象用过即死
  • MinorGC的时间远远低于FullGC

新生代越大越好吗

  • 小了容易经常触发MinorGC
  • 新生代太大的话,老年代的空间就不足了,当老年代空间不足会触发FullGC,耗费更多时间

image-20210916155118395

  • 新生代最好能容纳所有【并发量 X (请求响应)】

  • 幸存区大到能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置要得当,让长时间存活对象尽快晋升

    • 因为晋升对象如果长时间存在于幸存区,每次垃圾回收进行复制其实都没必要。应该早点把待晋升对象晋升到幸存区。
    • -XX:MaxTenuringThreshold=threshold
    • -XX:+PrintTenuringDistribution

image-20210916155932490

老年代调优

以CMS为例

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有FullGC那么说明老年代空间比较富裕,运行状况还不错。及时出现了FullGC,也可以先尝试调优新生代
  • 观察发生FullGC时老年代内存占用,将老年代内存预设调大1/4~1/3
    • -XX:CMSInitiatingOccupancyFraction=percent
    • 待空间达到了老年代的多少进行垃圾回收,预留空间给浮动垃圾

案例

案例1 FullGC和MinorGC频繁

  • 说明空间紧张
  • 可能是新生代空间小,当高峰期时对象的频繁创建,导致频繁发生MinorGC
  • 由于新生代空间紧张,动态调整晋升寿命,导致存活时间较短的对象也会晋升到老年代,导致触发FullGC
  • 应尝试调节新生代的内存

案例2 请求高峰期发生FullGC,单次暂停时间特别长(CMS)

  • 通过查看GC日志,查看CMS哪个阶段耗时长
  • 当高峰期时,对象频繁创建。在CMS的重新标记阶段,就可能耗费大量时间。
  • 可以在重新标记之前,先进行一次垃圾回收
  • -XX:+CMSScavengeBeforeRemark

案例3 老年代充裕情况下,发生FullGC()

  • 1.7之前是永久代作为方法区的实现,可能会发生永久代不足。
  • 永久代不足会触发堆的FullGC
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值