JVM学习03:垃圾回收
   
    
    
    1、如何判断对象可以回收
   
    
    
    1.1、引用计数法
   
- 
     记录
 
 当前对象被引用的次数
 
 ,当引用次数为0时则进行垃圾回收。
- 缺点:当两个对象互相引用但并没有其他对象再引用它们时,他们的引用次数都为1,无法对其进行回收释放。如图所示:
     
   
- 早期的python使用这种方法,但是java而是采用可达性分析算法。
    
    
    1.2、可达性分析算法
   
- 
Java 虚拟机中的垃圾回收器采用 
 
 可达性分析
 
 来探索所有存活的对象。
- 
扫描堆中的对象,看是否能够沿着 
 
 GC Root对象
 
 为起点的引用链找到该对象,找不到,表示可以回收。
- 
哪些对象可以作为 GC Root ? - System Class:系统类,例如Object类、HashMap类等。
- Native Stack:本地方法栈中引用的java对象。
- Thread:活动线程中使用的对象,即栈帧中的局部变量等引用的对象。
- Busy Monitor:所有被同步锁(synchronized关键字)持有的对象。
 
    
    
    1.3、四种引用
   
     
   
注意:图中实现为强引用,虚线为其他引用。
强引用
- 
     只有所有
 
 GC Roots 对象都不通过强引用
 
 引用该对象,该对象才能被垃圾回收。
- 例如图中A1对象,当B、C对象都不引用它时才会被垃圾回收。
软引用(SoftReference)
- 
     仅有软引用引用该对象时,
 
 在垃圾回收后,内存仍不足时
 
 会回收软引用引用的对象,回收软引用对象可以配合
 
 引用队列
 
 来释放软引用自身占用的内存。
- 
     例如图中A2对象,当B对象不再引用A2对象,并且
 
 垃圾回收后内存不足
 
 时,软引用所引用的A2对象会被回收。但是软引用自身没有被清理,可以使用
 
 引用队列
 
 进行处理,释放软引用占用的内存。
测试代码1:
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class demo02 {
    private static final int _4MB = 4 * 1024 * 1024;
    public static void main(String[] args) throws IOException {
        soft();
    }
    // list --> SoftReference --> byte[]
    public static void soft() {
        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());
        }
    }
}
测试结果:
可以看出,软引用对象(4MB的byte数组)在垃圾回收后,内存不足时进行清理。
     
   
测试代码2:引用队列
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示软引用, 配合引用队列
 */
public class demo03 {
    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());
        }
    }
}
测试结果:
使用引用队列后,原来结果中的软引用对象(4个null)被去除掉了。
弱引用(WeakReference)
- 
     仅有弱引用引用该对象时,
 
 在垃圾回收时,无论内存是否充足
 
 ,都会回收弱引用引用的对象,可以配合
 
 引用队列
 
 来释放弱引用自身占用的内存。
- 
     例如图中A3对象,当B对象不再引用A3对象,
 
 只要进行垃圾回收
 
 ,A3对象就会被清理。和软应用一样,可以使用
 
 引用队列
 
 释放弱引用自身占用的内存。
测试代码:
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class demo04 {
    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());
    }
}
测试结果:
当垃圾回收时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
     
   
虚引用(PhantomReference)
- 
 必须配合引用队列
 
 使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。
- 
例如图中的ByteBuffer对象,当ByteBuffer对象实力创建时,会创建一个虚引用对象Cleaner来引用它,这时会分配一块直接内存,并且会把直接内存地址传递给Cleaner对象,当ByteBuffer对象被清理时,虚引用对象Cleaner会放入引用队列,当 ReferenceHandler 线程监测到有对象进入队列时,会调用相关方法释放直接内存。(上一节直接内存中讲过) 
     
   
     
   
终结器引用(FinalReference)
- 
     无需手动编码,但其内部配合
 
 引用队列
 
 使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由
 
 Finalizer 线程
 
 通过终结器引用找到被引用对象并调用它的
 
 finalize方法
 
 ,第二次 GC 时才能回收被引用对象。
- 例如图中的A4对象。所有的类都继承自Object类,Object类有一个finalize方法。当A4对象重写了finalize()方法后,此对象不再被其他的对象所强引用时,JVM会创建一个终结器引用,垃圾回收时会先将终结器引用对象放入引用队列中,然后Finalizer 线程会查看引用队列,若其中有终结器引用,则会通过终结器引用找到它所引用的对象并调用该对象的finalize方法。调用以后,再进行一次GC,该对象才会被垃圾回收。
- 
     
 不推荐
 
 使用finalize()方法进行内存释放,其条件太复杂了。
    
    
    2、垃圾回收算法
   
    
    
    2.1、标记清除
   
定义: Mark Sweep
- 
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。 
- 
注意:这里的清理出来的内存空间并不是将内存空间的字节清0,而是记录下这段内存的 
 
 起始和结束地址
 
 ,下次分配内存的时候,会直接覆盖这段内存。
- 
优点:速度较快。 
- 
缺点:会造成内存碎片。 
     
   
    
    
    2.2、标记整理
   
定义:Mark Compact
- 
先采用标记算法确定可回收对象,然后将让所有存活的对象都向内存空间一端移动,最后直接清理掉边界以外的内存。 
- 
优点:没有内存碎片。 
- 
缺点:速度慢。 
     
   
    
    
    2.3、复制
   
定义:Copy
- 
它将可用内存按容量划分为大小相等的两个区域:from区和to区。当from内存满了的时候,首先标记存活的对象,然后把存活的对象从from区复制到to区,然后将from区清空,最后交换from区和to区。 
- 
优点:不会有内存碎片。 
- 
缺点:需要占用双倍内存空间。 
     
   
    
    
    3、分代垃圾回收
   
     
   
- 
对象首先分配在伊甸园区域。 
- 
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1,并且交换 from to。 
- 
minor gc 会引发 
 
 stop the world
 
 ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
- 
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。 
- 
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长。 
- 
新生代一般采用复制算法,老年代一般使用标记整理算法。 
- 
如果放入一个大对象,新生代放不进去时,会直接晋升到老年代,不需要引发GC了。 
    
    
    3.1、相关 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 | 
测试代码:
package com.jvm.lesson03;
import java.util.ArrayList;
/**
 *  演示内存的分配策略
 */
public class demo05 {
    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 {
        /*ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);*/
        //当一个线程抛出OOM异常后,不会影响其他线程的运行。
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);//如果放入一个大对象,新生代放不进去时,会直接晋升到老年代,不需要引发GC了。
            list.add(new byte[_8MB]);
        }).start();
        System.out.println("sleep....");
        Thread.sleep(1000L);
    }
}
结果分析:
- 如果放入一个大对象,新生代放不进去时,会直接晋升到老年代,不需要引发GC了。
- 当一个线程抛出OOM异常后,不会影响其他线程的运行。
    
    
    4、垃圾回收器
   
这部分结合书《深入理解java虚拟机第三版》看。
     
   
    
    
    4.1、串行
   
- 单线程
- 堆内存较小,适合个人电脑
- 
     参数:
- 
       
 -XX:+UseSerialGC
 
 :开启 Serial + SerialOld 收集器。
 
- 
       
     
   
- 
     串行的收集器比如
 
 Serial
 
 和
 
 SerialOld
 
 收集器。Serial收集器工作在
 
 新生代
 
 ,采用
 
 复制
 
 算法;SerialOld收集器工作在
 
 老年代
 
 ,采用
 
 标记整理
 
 算法。
    
    
    4.2、吞吐量优先
   
- 多线程
- 堆内存较大,多核 cpu
- 
     让
 
 单位时间内,STW 的时间最短
 
 ,垃圾回收时间占比最低,这样就称吞吐量高。
- 
     参数:
- 
       
 -XX:+UseParallelGC
 
 ~
 
 -XX:+UseParallelOldGC
 
 :分别为开启新生代和老年代的并行垃圾回收器,开启一个另一个自动开启。
- 
       
 -XX:+UseAdaptiveSizePolicy
 
 :使用自适应的调整策略,来调整新生代的大小、伊甸园区和幸存去的比例、晋升老年代的阈值等。
- 
       
 XX:GCTimeRatio=ratio
 
 :根据 1/(1+ratio)(垃圾收集时间占总时间的比率) 尝试调整堆的大小,进而调整吞吐量的大小。例如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19))。
- 
       
 -XX:MaxGCPauseMillis=ms
 
 :最大垃圾回收暂停的毫秒数,默认为200ms。
- 
       
 -XX:ParallelGCThreads=n
 
 :指定并行垃圾回收线程数,一般与CPU核数相同。
 
- 
       
     
   
- 
吞吐量:就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。 
 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾回收时间)
 
- 
吞吐量优先的收集器比如 
 
 Parallel Scavenge
 
 和
 
 Parallel Old
 
 收集器。Parallel Scavenge收集器工作在
 
 新生代
 
 ,采用
 
 复制
 
 算法;Parallel Old收集器工作在
 
 老年代
 
 ,采用
 
 标记整理
 
 算法。
    
    
    4.3、响应时间优先
   
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让单次 STW 的时间最短。
- 
     参数:
- 
       
 -XX:+UseConcMarkSweepGC
 
 :开启并发的使用标记清除算法的垃圾回收器。
- 
       
 -XX:ParallelGCThreads=n
 
 :设置
 
 并行
 
 的垃圾回收线程数,一般与CPU核数相同。
- 
       
 -XX:ConcGCThreads=threads
 
 :设置
 
 并发
 
 的垃圾回收线程数,一般设置为并行垃圾回收线程数的1/4。
- 
       
 -XX:CMSInitiatingOccupancyFraction=percent
 
 :设置CMS垃圾回收触发的百分比。当老年代使用的内存占比到了这个数值后才会触发立即回收,预留出一定的空间给其他用户线程和浮动垃圾(并发清理时新产生的垃圾)。
- 
       
 -XX:+CMSScavengeBeforeRemark
 
 :在重新标记阶段前对新生代进行一次垃圾回收,减少时间浪费。因为在重现标记阶段会扫描整个堆,从新生代查找其引用老年代的对象做可达性分析,但新生代的对象非常多且其中包含很多并发标记阶段产生的垃圾,这就造成浪费大量的时间查找无用的引用。
 
- 
       
     
   
- 
     并行和并发的区别:
- 并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 
       并发的关键是你有处理多个任务的能力,不一定要
 
 同时
 
 。并行的关键是你有
 
 同时
 
 处理多个任务的能力。
 
- 
     响应时间优先的收集器例如
 
 CMS收集器
 
 ,它工作在
 
 老年代
 
 ,使用
 
 标记清除
 
 算法实现。一般在新生代配合ParNew(
 
 -XX:+UseParNewGC
 
 )收集器使用。由于CMS收集器在老年代使用的标记清除算法,会产生大量的内存碎片,当预留的内存无法满足程序分配新对象的需要时,导致
 
 并发失败
 
 ,这时会使用 SerialOld 收集器作为补救措施。
    
    
    4.4、G1
   
定义:Garbage First
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景:
- 
     同时
 
 注重吞吐量
 
 (Throughput)
 
 和低延迟
 
 (Low latency),默认的暂停目标是 200 ms。
- 超大堆内存,会将堆划分为多个大小相等的Region,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
- 
     整体上是
 
 标记+整理
 
 算法,两个区域之间是
 
 复制
 
 算法。
相关 JVM 参数:
    
     -XX:+UseG1GC
    
    :开启G1收集器。
   
    
     -XX:G1HeapRegionSize=size
    
    :每个Region的大小。
   
    
     -XX:MaxGCPauseMillis=time
    
    :设定允许的收集停顿时间,默认为200ms。
   
    
    
    4.1.1、G1垃圾回收阶段
   
     
   
    
    
    4.2.2、Young Collection
   
- 会 STW
- 对象首先放到伊甸园区中,当伊甸园(E)的空间占满时,会触发Young Collection,将伊甸园的幸存对象以复制算法放入幸存区(S)。当幸存区的空间快要占满或幸存区对象年龄达到一定阈值,会再次触发Young Collection,幸存区的部分对象会晋升至老年代,年龄不够的会复制到另一块幸存区。
     
   
     
   
     
   
    
    
    4.2.3、Young Collection + CM
   
- 
在 Young GC 时会进行 GC Root 的 
 
 初始标记(会 STW)
 
 。
- 
老年代占用堆空间比例达到阈值时,进行 
 
 并发标记(不会 STW)
 
 ,由下面的 JVM 参数决定:
 -XX:InitiatingHeapOccupancyPercent=percent
 
 (默认45%)
     
   
    
    
    4.2.4、Mixed Collection
   
会对 E、S、O 进行全面垃圾回收。
- 
 最终标记(Remark)会 STW
 
- 
 拷贝存活(Evacuation)会 STW
 
    
     -XX:MaxGCPauseMillis=ms
    
    :设定允许的收集停顿时间,默认为200ms。使得在有限的时间里优先回收老年代中回收价值较高的(垃圾多的)。
   
     
   
    
    
    4.2.5、Full GC
   
- 
SerialGC - 新生代内存不足发生的垃圾收集 – minor gc
- 老年代内存不足发生的垃圾收集 – full gc
 
- 
ParallelGC - 新生代内存不足发生的垃圾收集 – minor gc
- 老年代内存不足发生的垃圾收集 – full gc
 
- 
CMS - 新生代内存不足发生的垃圾收集 – minor gc
- 
       老年代内存不足 –
 
 当老年代使用的内存占比到了设定的百分比会进行并发收集。当内存碎片足够多时导致新的对象放入时内存不足,会造成并发失败,并发收集失败时退化为串行收集,此时才会进行full gc
 
 。
 
- 
G1 - 新生代内存不足发生的垃圾收集 – minor gc
- 
       老年代内存不足 –
 
 老年代占用堆空间比例达到阈值(默认45%)时,进行并发标记(不会 STW)与混合收集,当回收速度快于垃圾产生速度时会进行并发收集。只有当垃圾产生速度快于回收速度时并发收集失败,退化为串行收集,此时才会进行full gc。
 
 
    
    
    4.2.6、Young Collection 跨代引用
   
新生代回收的跨代引用(老年代引用新生代)问题:
    在进行Young Collection时,首先要找到GC Root根对象,根据其进行可达性分析找到存活的对象复制入幸存区。但根对象一部分是来自老年代的,且老年代的存活对象较多,对老年代进行遍历查找效率很低。因此采用
    
     卡表技术
    
    对老年代进行分区,每个card大小约为512k。若此card中有对象引用了新生代的对象,则将其标记为
    
     脏卡
    
    。这时新生代会有一个
    
     Remembered Set
    
    来存放被标记的脏卡,因此在Young Collection查找根对象时只需要根据
    
     Remembered Set
    
    来查找脏卡即可,提高效率。
   
    在引用变更时通过
    
     post-write barrier + dirty card queue
    
    的方法来重新标记脏卡,这是一个异步操作,
    
     Remembered Set
    
    可以通过
    
     concurrent refinement threads
    
    来更新。
   
     
   
     
   
    
    
    4.2.7、Remark(重新标记)
   
- 
     
 pre-write barrier
 
 +
 
 satb_mark_queue
 
     
   
图中:黑色为已被处理的;灰色为正在处理中的;白色为还未处理的。
    如果对象的引用发生改变,JVM就会给其加入写屏障
    
     pre-write barrier
    
    ,并执行写屏障指令,将C加入队列中
    
     satb_mark_queue
    
    ,并把C标记为灰色。并发标记结束后,进入重新标记阶段,此时会STW,并将
    
     satb_mark_queue
    
    中的对象一个个取出来进行检查,如此对象有强引用引用时,将其修改为黑色,进行对象保留,否则被修改成白色进行清理。
    
     
   
    
    
    4.2.8、JDK 8u20 字符串去重
   
- 
优点:节省大量内存。 
- 
缺点:略微多占用了 cpu 时间,新生代回收时间略微增加。 
- 
参数: 
 
 -XX:+UseStringDeduplication
 
 (默认是打开的)
//连个字符串对象引用同一个字符数组
String s1 = new String("hello"); // char[]{'h','e','l','l','o'} 
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
- 
将所有新分配的字符串放入一个队列。 
- 
当新生代回收时,G1并发检查是否有字符串重复。 
- 
如果它们值一样,让它们引用同一个 char[]。 
- 
注意,与 
 
 String.intern()
 
 不一样。- 
       
 String.intern()
 
 关注的是
 
 字符串对象
 
 。
- 
       而字符串去重关注的是
 
 字符串对象引用的 char[]
 
 。
 
- 
       
- 
在 JVM 内部,使用了不同的字符串表。 
    
    
    4.2.9、JDK 8u40 并发标记类卸载
   
- 
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器(一般为自定义加载器)的所有类都不再使用,则卸载它所加载的所有类。 
- 
参数: 
 
 -XX:+ClassUnloadingWithConcurrentMark
 
 (默认启用)
    
    
    4.2.10、JDK 8u60 回收巨型对象
   
- 
一个对象大于 region 的一半时,称之为巨型对象。 
- 
G1 不会对巨型对象进行拷贝。 
- 
回收时被优先考虑。 
- 
G1 会跟踪老年代所有 incoming 引用(脏卡引用巨型对象的次数),这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉。 
     
   
    
    
    4.2.11、JDK 9 并发标记起始时间的调整
   
- 
并发标记必须在堆空间占满前完成,否则退化为 FullGC。 
- 
JDK 9 之前需要使用: 
 
 -XX:InitiatingHeapOccupancyPercent
 
 (默认45%)
- 
JDK 9 可以动态调整: - 
       
 -XX:InitiatingHeapOccupancyPercent
 
 用来设置初始值。
- 进行数据采样并动态调整。
- 总会添加一个安全的空档空间。
 
- 
       
    
    
    4.2.12、JDK 9 更高效的回收
   
- 
250+增强 
- 
180+bug修复 
- 
https://docs.oracle.com/en/java/javase/12/gctuning 
    
    
    5、垃圾回收调优
   
预备知识:
- 
掌握 GC 相关的 VM 参数,会基本的空间调整。 查看虚拟机的运行参数: 
 
 "java的路径" -XX:+PrintFlagsFinal -version | findstr "GC"
 
     
   
- 
掌握相关工具:jmap、jconsole、VisualVM等。 
- 
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则。 
    
    
    5.1、调优领域
   
- 
内存 
- 
锁竞争 
- 
cpu 占用 
- 
io 
    
    
    5.2、确定目标
   
- 
【低延迟】还是【高吞吐量】,选择合适的回收器。 
- 
低延迟的虚拟机:CMS,G1,ZGC 
- 
高吞吐量的虚拟机:ParallelGC 
- 
其他的的虚拟机:Zing 
    
    
    5.3、最快的 GC
   
答案是不发生 GC。
查看 FullGC 前后的内存占用,考虑下面几个问题:
- 
数据是不是太多? - 
       数据库里读取数据:
 
 resultSet = statement.executeQuery("select * from 大表 limit n")
 
 。可以后面加上
 
 limit n
 
 。
 
- 
       数据库里读取数据:
- 
数据表示是否太臃肿? - 对象图
- 对象大小 :比如Integer占24个字节,int占4个字节,尽量使用基本类型。
 
- 
是否存在内存泄漏? - 把数据全放入一个map中,垃圾得不到即使清理,会导致内存泄漏。
- 可以使用软引用、弱引用或者第三方缓存实现,比如redis。
 
    
    
    5.4、新生代调优
   
- 
新生代的特点: - 
所有的 new 操作的内存分配非常廉价。 - TLAB(thread-local allocation buffer):线程分布局部缓冲区,作用是让每个线程用自己私有的内存进行对象分配,因此多个线程同时创建对象时不会产生内存冲突。
 
- 
死亡对象的回收代价是零(一般使用复制算法)。 
- 
大部分对象用过即死。 
- 
Minor GC 的时间远远低于 Full GC。 
 
- 
- 
新生代内存越大越好吗? 
-Xmn参数:
设置新生代的初始和最大内存。在这个区域执行GC的频率要高于其他区域。如果新生代的内存太小,那么就会执行大量的minor GC。如果内存太大,那么就会执行full GC,full GC可能需要很长的时间来完成。Oracle建议你将年轻一代的大小保持在整个堆大小的25%以上,50%以下。
补充:吞吐量随着新生代内存增大先增大再减小,一开始内存增大后,垃圾回收频率变低,吞吐量增加;到了一定阶段,内存太大时,垃圾回收占的时间也增加,此时吞吐量开始减小。
- 
新生代内存估算: 
 
 【并发量 * (请求-响应)】
 
 (每个请求相应占用的内存量*并发数)。
- 
幸存区大到能保留**【当前活跃对象+需要晋升对象】**。幸存区太小会导致一部分存活时间较短的对象晋升至老年代,老年代对象full gc时才会回收,进而造成内存浪费。 
- 
晋升阈值配置得当,让长时间存活对象尽快晋升,否则就会在幸存区反复被复制。 
 -XX:MaxTenuringThreshold=threshold
 
 :设置最大晋升阈值。
 -XX:+PrintTenuringDistribution
 
 :打印晋升区存活对象的详情。
Desired survivor size 48286924 bytes, new threshold 10 (max 10) 
- age 1: 28992024 bytes, 28992024 total 
- age 2: 1366864 bytes, 30358888 total 
- age 3: 1425912 bytes, 31784800 total 
...
    
    
    5.5、老年代调优
   
以 CMS 为例:
- 
CMS 的老年代内存越大越好。预留更多的空间,避免浮动垃圾造成并发失败。 
- 
先尝试不做调优,如果没有 Full GC ,那先尝试调优新生代。 
- 
观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3。 
- 
 -XX:CMSInitiatingOccupancyFraction=percent
 
 :设置CMS垃圾回收触发的百分比。
    
    
    5.6、案例
   
案例1:Full GC 和 Minor GC 频繁。
可能是新生代内存太小引发的Minor GC 频繁,可以尝试增大新生代的内存大小;然后可以适当的增大晋升老年代的阈值,防止Full GC频繁。
案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)。
    CMS垃圾回收器在初始标记和并发标记消耗的时间较短,但是重新标记消耗的时间较长,因为他需要重新扫描老年代和新生代。可以使用
    
     -XX:+CMSScavengeBeforeRemark
    
    参数在重新标记阶段前对新生代进行一次垃圾回收,减少时间浪费。
   
案例3:老年代充裕情况下,发生 Full GC(CMS jdk1.7)。
可能是永久代内存不足导致的Full GC。在 JDK 1.7 及以前,JVM中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
 
