1. GC与GC Tuning

1 什么是垃圾
  1. 没有任何引用指向的一个对象或者多个对象(循环引用)
  2. 申请、释放内存
    1. C语言:malloc、free
    2. C++: new、delete
    3. Java:new、自动回收
  3. 自动回收与手动回收优缺点
    1. 自动回收:编程简单,系统不容易出错
    2. 手动回收:可能出现忘记回收(内存泄露)、多次回收(回收了有用的数据)
2 定位垃圾
  1. 引用计数(reference counting):对象上存放引用它的引用数量,当引用数量为0,认为是垃圾,但这种方案找不到循环引用
  2. 根可达(root searching):以静态变量、线程栈中变量、常量池(指向Class对象)、JNI指针(指向native方法用到的类或对象),作为根引用,顺着根引用能找到的对象都不是垃圾,其他都是垃圾,Hotspot就使用这种方式定位垃圾
3 垃圾回收算法
  1. 标记清除(Mark-Sweep)
    1. 两遍扫描:
      1. mark:将根对象可以访问到的对象都打上一个标识,表示可达
      2. sweep:遍历堆内存,将不可达对象清理
    2. 产生内存碎片
    3. 存活对象多时,效率高,因为需要清理的少
  2. 拷贝算法(Copying)
    1. 将堆内存对半分为两个半区,只用其中一个半区来进行对象内存的分配,如果在这个半区内存不够给新的对象分配了,那么就开始进行垃圾收集,将这个半区中的所有可达对象都拷贝到另外一个半区中去
    2. 需要移动和复制对象,因此对同一个对象,需修改引用指向
    3. 存活对象少,效率高,因为需要复制的对象少
  3. 标记压缩(Mark-Compact),无碎片,不浪费空间,但效率低
    1. 两遍扫描
      1. mark:
      2. compact:移动所有的可达对象到堆内存的同一个区域中,使他们紧凑的排列在一起,从而将所有非可达对象释放出来的空闲内存都集中在一起
    2. 无碎片,不会产生内存减半
    3. 效率低
      1. 算法复杂
      2. 多线程移动还需要时间进行同步
4 堆内存逻辑分区(也叫分代模型)

1. GC与GC Tuning_第1张图片

  1. 从分代GC的角度看,堆分为新生代与老年代
  2. 有些垃圾回收器逻辑上并不是如此划分的
    1. 除Epsilon、ZGC、Shenandoah之外的垃圾回收期都是使用逻辑分代模型
    2. G1是逻辑分代,物理不分代
    3. 除此之外,不仅逻辑分代,而且物理分代
  3. 新生代 = eden + 2个survivor区
    1. YGC(Minor GC):无法在新生代为对象分配空间时产生,对新生代的内容进行回收,由于新生代内容通常全能被回收掉,因此YGC采用拷贝算法进行回收,而拷贝算法需要额外空间,因此产生了survivor区
    2. 第1次YGC:大多数的对象会被回收,eden -> s0
    3. 第2次YGC:eden + s0 -> s1
    4. 第3次YGC:eden + s1 -> s0
    5. 年龄足够:老年代
      1. 年龄指对象复制的次数
      2. -XX:MaxTenuringThreshold:设置年龄阈值
      3. PS默认15,因为gc的age是4位,最大是15,CMS是6,G1是15
      4. 动态年龄判定:Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值
      5. 内存分配担保:YGC时,JVM会首先检查老年代最大的可用连续空间,是否大于新生代所有对象的总和,如果大于,那么这次YGC是安全的,如果不大于的话,JVM就需要判HandlePromotionFailure是否允许空间分配担保
        1. 允许空间分配担保:JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的所有对象总的平均大小,如果大于表示此处YGC相对安全,正常进行一次YGC,如果小于,进行FGC
        2. 不允许空间分配担保:FGC
  4. 老年代
    1. FGC(Major GC、Full GC):老年代满了会产生FGC,整个内存都回收,效率低,会暂停所有当前运行的线程,产生stw(stop-the-world)停顿,进行垃圾清理
    2. FGC本身采用标记压缩算法
  5. GC Tuning(Generation):尽量减少FGC
  6. -Xmn:指定新生代内存大小,-Xms:指定堆内存小值,-Xmx:指定堆内存最大值
    1. -X:非标准参数
    2. m:memory
5 栈上分配、TLAB

1. GC与GC Tuning_第2张图片

  1. 一些小的、无逃逸的、线程私有的对象,会使用标量替换,以标量形式将该对象存放于栈内存中,而不是在堆内存中
    1. 无逃逸:某个对象的引用不会传递给其他线程或方法拿到
    2. 线程私有:不是线程私有,就意味着该对象一定会被其他线程访问到,也就一定是逃逸对象了
    3. 标量替换:例如User会被替换为一个int和一个String
  2. 栈上分配比堆上分配更快,栈中的内存不需要垃圾回收,因为变量出栈,就没了
  3. TLAB(Thread Local Allocation Buffer)线程本地分配
    1. 为防止各线程对堆内存的征用导致效率降低
    2. 提前为每个线程分配独有的一块空间,eden区的1%,线程分配空间时,先往这块空间里分配
//-XX:-DoEscapeAnalysis -XX:-EliminateAllocations -XX:-UseTLAB -Xlog:c5_gc*
//去掉逃逸分析、标量替换、线程专有对象分配
public class TestTLAB {
    //User u;
    class User {
    	//user只有两个属性,都是基本类型变量,因此是支持标量替换的
        int id;
        String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    void alloc(int i) {
    	//如果该方法会将新的User对象返回给main方法,就属于逃逸
        new User(i, "name " + i);
    }

    public static void main(String[] args) {
        TestTLAB t = new TestTLAB();
        long start = System.currentTimeMillis();
        for(int i=0; i<1000_0000; i++) t.alloc(i);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

6 常见的垃圾回收器

  1. Serial:串行回收,YGC时,所有线程停止工作STW,启动一个单线程的垃圾回收器开始执行,回收完垃圾后,其他线程重新执行
    1. safe point:线程不是马上就能停止,会到一个安全的点,才能停
    2. 早期内存小的时候,一个线程做清理很快能清理完,对现在大内存的系统效率太低了,因此用的极少
  2. Parallel Scavenge(清理):简称PS,并行回收,所谓并行可能是说,垃圾清理的线程处于不同cpu中,是并行的,也是所有线程停止,只不过启动多个垃圾回收线程同时进行垃圾回收
  3. ParNew:与PS功能相同,但可以与CMS配合,而PS不行。
  4. Serial Old
  5. Parallel Old
  6. CMS(Concurrent Mark Sweep):并发回收,所谓并发是指垃圾回收线程可与工作线程同时进行,这些线程可能处于同一cpu,所以是并发的,降低STW的时间至200ms以内,并发是因为无法忍受stw,10g内存,ps+po,一次stw需要11s。使用cms,之后的fgc,用了几天或几个小时时间
    1. 诞生了一个里程碑一样的东西,毛病大,没有一个版本的jdk使用cms
    2. 算法:三色标记+Increment Update
    3. cms几个阶段:
      1. 初始标记:stw,单线程,找根引用指向的对象,并标记,很快
      2. 并发标记:一边标记可达对象,一边产生垃圾,80%gc时间,都浪费在这,这块和用户线程同时运行
      3. 重新标记:stw,多线程,并发标记同时产生新的可达对象,这些新对象需要被重新标记进行标记,也不长
      4. 并发清理:不需要移动存活对象,因此可以与工作线程同时进行,清理过程中产生的垃圾,叫浮动垃圾,浮动垃圾等下一个周期进行清理
    4. CMS缺点:
      1. 内存的碎片化:由于采用标记清理算法,因此CMS在指定参数次(默认每次)FGC后,下一次FGC前会,会启动单线程Serial Old对内存进行合并整理,浪费时间
      2. 浮动垃圾:
        1. 由于CMS与工作线程同时执行,因此不能等内存满了才进行CMS,老年代使用内存占老年代总内存的一定百分比就开始cms,jdk5默认68%,jdk6默认92%
        2. 如果预留的这8%无法满足工作线程分配内存的需要,会引起并发失败(垃圾回收与工作线程同时处理失败Concurrent Mode Failure)
        3. 此时系统不得不启用后备方案,stw,然后启用Serial Old进行清理
        4. 可以降低触发CMS的阈值,-XX:CMSInitiatingOccupancyFraction
  7. G1:stw10ms,算法:三色标记+SATB
  8. ZGC:stw1ms,可以与C++回收速度抗衡,算法:coloredpointer+写屏障
  9. Shenandoah:算法:Colored Pointers+读屏障
  10. Epsilon:调试jdk用的
  11. 其他
    1. 没有不会产生stw的垃圾回收器
    2. 垃圾收集器与内存大小的关系
      1. Serial:几十兆
      2. Ps:上百兆-几个G
      3. CMS:20G
      4. G1:上百G
      5. ZGC:最高4T
7 JVM调优第一步,了解生产环境下的垃圾回收器组合
  1. JVM的命令行参数参考
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
  1. JVM参数分类
    1. 标准:- 开头,所有的HotSpot都支持,输入java查看
    2. 非标准:-X 开头,特定版本HotSpot支持特定命令,输入java -X查看
    3. 不稳定:-XX 开头,下个版本可能取消
  2. -XX相关参数查看
    1. -XX:+PrintCommandLineFlags:打印启动时真正使用的命令行参数
    2. -XX:+PrintFlagsFinal 最终参数值,可以查看java所有参数
    3. -XX:+PrintFlagsInitial 默认参数值
8 GC常用参数
8.1 常用垃圾回收器组合设定参数(1.8)
  1. -XX:+UseSerialGC = Serial New(DefNew)+Serial Old
    1. 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
  2. -XX:+UseParNewGC = ParNew + SerialOld
    1. 这个组合已经很少用(在某些版本中已经废弃)
  3. -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
  4. -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
  5. -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
  6. -XX:+UseG1GC = G1
    1. Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
  7. java -XX:+PrintCommandLineFlags -version
    1. 通过GC的日志来分辨
  8. Linux下1.8版本默认的垃圾回收器到底是什么?
    1. 1.8.0_181 默认(看不出来)Copy MarkCompact
    2. 1.8.0_222 默认 PS + PO
8.2 GC通用参数
  1. -Xmn -Xms -Xmx -Xss 年轻代 最小堆 最大堆 栈空间
  2. -XX:+UseTLAB 使用TLAB,默认打开
  3. -XX:+PrintTLAB 打印TLAB的使用情况
  4. -XX:TLABSize 设置TLAB大小
  5. -XX:+DisableExplictGC System.gc()不管用(FGC),线上系统都启用
  6. -XX:+PrintGC
  7. -XX:+PrintGCDetails
  8. -XX:+PrintHeapAtGC
  9. -XX:+PrintGCTimeStamps
  10. -XX:+PrintGCApplicationConcurrentTime (低) 打印应用程序时间
  11. -XX:+PrintGCApplicationStoppedTime (低) 打印暂停时长
  12. -XX:+PrintReferenceGC (重要性低) 记录回收了多少种不同引用类型的引用
  13. -verbose:class 类加载详细过程
  14. -XX:+PrintVMOptions 打印JVM启动时参数
  15. -XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 必须会用
  16. -Xloggc:opt/log/gc.log
  17. -XX:MaxTenuringThreshold 升代年龄,最大值15
  18. -XX:PreBlockSpin 锁自旋多久升级为重量级锁(不建议设置)
  19. -XX:CompileThreshold 热点代码检测参数多少次后编译为热点代码,逃逸分析 标量替换(不建议设置)
8.3 Parallel常用参数
  1. -XX:SurvivorRatio survivor区比例
  2. -XX:PreTenureSizeThreshold 有些大对象会直接分配到old区,大对象到底多大
  3. -XX:MaxTenuringThreshold
  4. -XX:+ParallelGCThreads 并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
  5. -XX:+UseAdaptiveSizePolicy 自动选择各区大小比例
8.4 CMS常用参数
  1. -XX:+UseConcMarkSweepGC
  2. -XX:ParallelCMSThreads CMS线程数量,默认是cpu核数的一半,因为CMS与应用程序线程并行,需要给应用程序线程留位置
  3. -XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
  4. -XX:+UseCMSCompactAtFullCollection 在FGC时进行压缩,因为CMS实际上是不进行压缩的,解决CMS产生内存碎片的问题
  5. -XX:CMSFullGCsBeforeCompaction 多少次FGC之后进行压缩
  6. -XX:+CMSClassUnloadingEnabled 回收永久代/元数据区
  7. -XX:CMSInitiatingPermOccupancyFraction 达到什么比例时进行Perm回收
  8. GCTimeRatio 设置GC时间占用程序运行时间的百分比
  9. -XX:MaxGCPauseMillis 停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
8.5 G1常用参数
  1. -XX:+UseG1GC
  2. -XX:MaxGCPauseMillis 建议值,G1会尝试调整Young区的块数来达到这个值
  3. -XX:GCPauseIntervalMillis ?GC的间隔时间
  4. -XX:+G1HeapRegionSize 每个分区块大小,建议逐渐增大该值,观测效果,1 2 4 8 16 32。 随着size增加,region越不容易满,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长。ZGC做了改进(动态区块大小,根据之前几次垃圾回收时间,安排region大小)
  5. G1NewSizePercent 新生代最小比例,默认为5%
  6. G1MaxNewSizePercent 新生代最大比例,默认为60%
  7. GCTimeRatio GC时间建议比例,G1会根据这个值调整堆空间。与-XX:MaxGCPauseMillis设其中一个即可
  8. ConcGCThreads 线程数量
  9. InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例
9 G1详解
  1. G1将内存划分为多个(默认2000多个)大小相同的内存块称为Region,每个Region是逻辑连续的一段内存,在被使用时都充当一种角色
    1. GC与GC Tuning_第3张图片
  2. Region可充当old、survivor、eden、humongos,当新建对象大小超过Region的50%,直接在新的一个或多个连续Region中分配,并标记为humongos区,某Region的角色不是固定的edian或old,随时会变化
  3. 当垃圾回收时,优先收集存活对象最少的,也就是垃圾最多的Region,所以叫garbage first
  4. 特点
    1. 并发收集,与CMS差不多,采用三色标记算法
    2. 压缩空闲空间,不会延长GC的暂停时间
    3. 每次收集指定的几个Region,因此更容易预测GC暂停时间
    4. 适用于不需要实现很高吞吐量的场景,使用硬件弥补吞吐量的问题
  5. 当YGC时,正常来讲,只需要先找到Young区中的所有root对象,再顺着这些root对象继续找,找到的所有对象就是可达对象,剩余对象就全可以清理,但有一种情况,如果老年代中的根对象A,有成员变量,指向了年轻代中一个对象B,此时B对象无法被Young区中的根对象找到,为了将B也正确标记为可达,就需要遍历所有Old区中的对象,看是否有指向B的引用,为了避免这种情况的产生,引入了RSet
  6. RSet:是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,Card Table是G1和CMS对RSet的一种具体实现
  7. Card Table:将堆空间划分为一系列2的n次幂大小的空间,称为card(卡页),对于HotSpot JVM,卡页大小为512字节,将所有卡页的起始地址右移9位,得到的数值,作为一个byte[]的索引,这个byte[]就是所谓的Card Table(卡表),也就是根据byte[]的索引就能找到指定的卡页,同时该byte[]中的元素值,代表对应的卡页的状态
  8. CSet:Collection Set,表示本次要被收集的Card的集合
  9. CMS处理跨代引用时对Card Table的使用
    1. 这个Card Table的索引,包含所有老年代中的卡页
    2. 老年代中的某个卡页中的对象,指向了年轻代中的对象,就将以该卡页的地址作为索引的byte[]处元素值,设置为0表示dirty
    3. YGC时,只需要顺着Young区所有Root对象进行扫描,并加上顺着dirty的card中的引用进行扫描即可找到所有Young区的可达对象
  10. G1处理跨代(Region)引用时对Card Table的使用
    1. 在上面介绍的Card Table使用方法的基础上,加入了一个存放于当前Region的Card Table
    2. 这个Card Table包含所有指向存放它的Region的其他Region中的卡页
    3. 这样当想扫描Region时,对于跨代的引用,只需要扫描Region中的RSet所代表的卡页中,是否有可达对象指向自身即可
    4. RSet会需要堆的10%-20%的内存用来维持垃圾收集器工作
  11. 可达性分析算法:三色标记
    1. 难点:标记对象过程中,对象引用关系正在发生改变
    2. 做法
      1. 白色:尚未被标记,最后白色对象被回收
      2. 灰色:自身被标记,成员变量指向的对象未标记
      3. 黑色:自身和成员变量指向的对象均标记完成
    3. 漏标
      1. 工作线程将B到D的引用切断,由A中成员引用D
      2. 此时由于垃圾回收线程已经扫描过A,不会再对其成员引用的D对象进行标记,而扫描B的属性时,又无法通过该属性的引用找到D
      3. 此时D无法正确被标记为可达对象
        1. GC与GC Tuning_第4张图片
        1. GC与GC Tuning_第5张图片
      4. 解决方案
        1. 跟踪A指向D的增加(incremental update):CMS引入了另一个数据结构mod union table,这里一个bit对应一个Card,young gc在将Card Table设置为clean的时候会将对应的mod union table置为dirty,重新标记的时候会将Card Table或者mod union table是dirty的Card也作为root去扫描
        2. 跟踪B指向D的消失(SATB,snapshot at the beginning):将B到D的引用放入一个特殊的栈中,重新标记时,会继续对栈中的对象进行标记
  12. G1的新生代大小默认占堆的5%-60%,收集过程目标时间默认为200ms,不要手工指定G1的年轻代和老年代的大小,由于G1会造成STW,所以G1会根据收集过程目标时间,自动优化新生代大小,比如本次YGC较慢,下一次就会将年轻代比例调小
  13. G1的GC模式
    1. young gc:stw
      1. eden region被耗尽无法申请内存时触发
      2. 活跃对象会被拷贝到survivor region或者晋升到old region中
    2. mixed gc
      1. 由-XX:InitiatingHeapOccupancyPercent参数设定,默认45%,即当老年代占整个堆的45%以上时触发mixed gc
      2. 过程相当于一套完整的CMS
        1. 初始标记
        2. 并发标记
        3. 最终标记
        4. 筛选回收:与CMS不同
          1. 会多了筛选的步骤,优先选择垃圾最多的Region进行回收
          2. 会将Region中存活的对象复制到空的Region中,并清理旧的Region空间
          3. 由于涉及存货对象的移动,因此无法与用户线程并行执行
    3. full gc
      1. 对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满时触发
      2. G1的full gc,在java10以前为单线程的Serial Old,java10以后才是并行回收,效率非常低
      3. 应尽量避免full gc
        1. 扩内存
        2. 提高cpu性能(加快回收速度)
        3. 降低mixed gc的阈值,让mixed gc提早发生
10 参考资料
https://blogs.oracle.com/ jonthecollector/our-collectors
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp
11 补颜色指针的算法

你可能感兴趣的:(JVM)