JVM垃圾回收

垃圾的定义

  • 没有任何引用指向的对象或Class,因为在Java1.8之后可以对Meta Space中的class进行回收(FGC)

为什么要分代

  • 分代目的其实为了将长期被其他对象指向的对象与短暂被使用的对象进行分离,当对象被判定为长期被使用的对象,就会进入老年代,从而降低了这些长期被使用的对象的回收频率;

吞吐量和停顿时间

  1. 吞吐量:用户代码时间 /(用户代码执行时间 + 垃圾回收时间)
  2. 响应时间:STW越短,响应时间越好

线程和纤程

  1. 进程 cpu资源分配的基本单位;
  2. 线程 cpu执行或调度的基本单位;(进程里最小的一个执行单元)(程序里不同的执行路径); 线程是需要在操作系统内核态下进行启动和上下文切换的,所以启动线程和上下文切换是比较耗时的;
  3. 纤程:它和线程的主要差别是,它可以在用户态下进行启动和上下文切换,所以相对与线程来说在某些场景下可能更合适;(例如处理内存中计算密集型的业务)

GC的基本概念:

  • Card Table:
    为什么会有card的设计:因为YGC时通过GC root来找到可达对象(通过对象地址查找),但是有些可达对象可能已经进入老年代了,并且它还指向了新生代对象,这样当查找对象时,需要扫描整个old区,效率非常低,所以JVM将内存分为一个个的card,如果old区有对象指向了新生代,就将其比标记为dirty,下次扫描时,只需要扫描dirty card, 哪些card为dirty记录在card table中,card table的数据结构是一个bitMap;

如何定位到垃圾(垃圾标记阶段)

  1. 引用计数算法(无法解决循环引用的问题)
  2. 可达性分析算法(或称为根搜索算法、追踪垃圾收集),分析工作必须在一个可以保证一致性的快照中进行,如果不满足准确性将无法得到满足,这也是进行GC时必须Stop the World的一个重要原因

GC Root

  • GC Root包括那些类别的元素:
    1.线程栈中的变量:各个线程调用的方法的参数,局部变量等;准确来说就是栈中所有栈帧局部变量表中的变量;
    2.静态变量:静态属性引用的变量
    3.常量池中对象、
    4.本地方法栈中引用的对象
    5.所有被同步锁Synchronized持有的对象
    6.Java虚拟机内部的引用:基本数据类型对应的class对象,系统类加载器,以及一些常驻的异常对象(如:NullPointerException、OutOfMemoryError)

标记-清除(Mark-Sweep)算法(垃圾清除阶段)

  1. 执行过程:
    标记阶段:垃圾收集器会从GC Root开始遍历,会在可达对象的对象头中记录标记
    清除阶段:垃圾收集器对堆内存整体进行遍历,对没有标记为可达对象的对象进行回收
  2. 缺点:
    1.标记阶段和清除阶段分别进行一次扫描,效率不高;
    2.容易产生内存碎片化;需要维护一个空闲列表;
    3.执行过程中需要Stop the World;
     注意:为什么要维护一个空闲列表?
     因为为对象分配内存的时候,会有两种情况:
     1.内存规整:指针碰撞,直接根据偏移量为对象分配内存;
     2.内存不规整:需要去空闲列表查找可以分配内存的空间;
     由于这里会导致内存碎片化,所以需要维护空闲列表来记录内存的使用情况;
    
  3. 优点:算法相对简单,在存活对象比较多的时候效率比较高;
  4. 所谓的清除:这里的清除并不是将需要清除的对象内存空间置空,而是把需要清除对象的内存地址维护在空闲列表里。在下次有新的对象需要分配内存空间时,会根据对象大小及空闲列表中记录来为新对象分配内存;

复制(Copying)算法(针对新生代的垃圾收集)

  1. 核心思想:将内存空间分成两块,每次使用其中的一块,在垃圾回收时,将正在使用的内存中的存活对象复制到另一块未被使用的内存块中,然后清除正在被使用的内存块中的所有对象,交换两个内存块的角色,完成垃圾收集
  2. 优点:
    1.直接复制存活对象,没有标记和清除过程,实现简单,运行高效;
    2.复制对象时候,直接使用指针碰撞的方式来分配内存,不会出现内存碎片化;
  3. 缺点:
    1.需要两倍的内存空间;
    2.复制是需要移动存活对象,HotSpot使用的是直接引用的方式将对象引用直接指向对象内存地址,而复制操作改变了内存地址,此时也需要变更对象引用指向的内存地址,带来了额外的开销;
  4. 适合的应用场景:存活对象较少的场景,因为存活对象比较多,需要复制的对象就比较多,效率就比较低;而这与垃圾收集的Survivor区比较契合,由于Java中百分之八十以上对象都是朝生夕死的,所以复制算法很契合Survivor区进行垃圾回收;

标记压缩(或标记整理、Mark - Compact)算法(针对老年代代的垃圾收集)

  1. 背景:复制算法在存活对象比较多的时候效率比较低,而基于老年代垃圾回收的特性,显然复制算法是不合适的,标记-清除算法的确可以使用在老年代垃圾收集,但是该算法不仅执行效率低下且会产生内存碎片化,显然是不合适的;
  2. 执行过程:
    1.第一阶段:和标记清除算法一样,从GC Root出发,对所有可达对象进行标记;
    2.第二阶段:将所有对象压缩到内存的一端,按顺序排放; —>(整理碎片化);
    3.第三阶段:清除存活对象边界外的所有空间;
  3. 优点:
    1.解决内存碎片化的问题;
    2.消除了复制算法中,两倍内存的高额代价;
  4. 缺点:
    1.从效率上来说,标记-整理算法效率要低于复制算法;
    2.整理对象会移动对象,会改变对象的内存地址,需要维护对象引用所指向的对象地址;
    3.整理对象过程中,需要Stop the World;

JVM内存分代模型(用于分代垃圾回收算法)

各个区比例

  1. 新生代各部分默认占比:eden : s1 : s2 = 8 : 1 : 1
  2. 新生代与老年代默认比例:new/young : old = 1 : 2;new/young = eden + s1 + s2

对象各阶段的GC流程

  1. 尝试栈上分配;
  2. 进入Eden区;
  3. s0 - s1之间的复制年龄超过限制时,进入old区,可以通过参数来设置年龄:-XX:MaxTenuringThreshold配置
  4. Minor GC/YGC:年轻代空间耗尽时触发;
  5. Major GC/Full GC:在老年代无法继续分配空间时触发,新生代老年代同时进行;

栈上分配

  1. 线程的私有小对象;
  2. 没有逃逸出方法(对象的使用范围就在方法内部,没有外部引用指向它)
  3. 支持标量替换;

线程本地分配TLAB(Thread Local Allocation Buffer)

  • 理解:Eden为了避免多线程分配对象时需要线程同步,线程同步的话就会带来影响分配效率,所以引入了TLAB,为每个线程分配自己的空间
    1.占用eden,默认为1%;
    2.多线程的时候不用竞争eden就可以申请空间,提高效率;
    3.小对象;

对象何时进入老年代

  1. 超过XX:MaxTenuringThreshold指定次数(YGC)
    1.Parallel Scavenge 15
    2.CMS 6
    3.G1 15
  2. 动态年龄
    1.年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域TargetSurvivorRatio的时候,就从这个年龄段往上的年龄的对象进行晋升。
    -XX:TargetSurvivorRatio目标存活率,默认为50%
  3. 大对象直接进入老年区;
  4. Minor GC后,survivor区容量不够,直接进入老年区;

安全点

  • 程序执行时并非所有地方都能停下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为”安全点(Safepoint)”

常见的垃圾收集器

  1. Serial回收器(串行回收):
    (1).特点:单线程垃圾回收,Serial作用于新生代垃圾收集,使用的是复制算法;Serial Old作用于老年代垃圾收集,使用的是标记压缩算法
    (2).使用:-XX:+UseSerialGC
    (3).使用的算法:年轻代使用的是复制算法
    (4).垃圾回收器组合:Serial Old
  2. ParNew回收器(并行回收):
    (1).特点:并行回收,与Serial主要区别采用了并行回收
    (2).使用:-XX:+UseParNewGC
    (3).使用的算法:年轻代使用的是复制算法
    (4).垃圾回收器组合:CMS
    (5).核心参数:-XX:ParallelGCThreads限制线程数量,默认开启与CPU核数相同的数量
  3. Parallel Scavenge回收器(吞吐量优先):
    (1).特点:和ParNew不同,Parallel Scavenge的目标是达到一个可控制的吞吐量,它被成为吞吐量优先的垃圾回收器;自适应策略也是Parallel与ParNew的一个重要区别,JDK8默认的垃圾回收器
    (2).使用:-XX:+UseParNewGC
    (3).使用的算法:年轻代使用的是复制算法
    (4).垃圾回收器组合:Parallel Old
    (5).核心参数:-XX:MaxGCPauseMillis;(设置垃圾回收器的最大停顿时间,即STW时间,单位毫秒);-XX:GCTimeRatio;(垃圾收集时间占总时间的比例,取值范围(0,100),默认值99);-XX:+UseAdaptiveSizePolicy;(设置Parallel Scavenge收集器具有自适应调节策略)

CMS回收器(低延迟)

  1. 特点:并发标记阶段,不会STW,垃圾回收线程与用户线程并行执行
  2. 工作环节:
    1.初始标记阶段(initial mark) — 标记所有的GC root — 当前阶段是STW的 — 虽然此阶段是STW的,但是只是标记GC root, GC root相对于内存中的对象来说是比较少的,所以STW的时间非常的短;
    2.并发标记(concurrent mark):根据GC root标记GC root的所有可达对象 — 该阶段需要标记的对象比较多,比较耗时,但是此阶段是和用户线程是并发执行的,不产生STW, 对于用户的响应就会比较及时,但是这会造成一个问题,标记过程,有的对象原来是垃圾,现在又有新的对象指向了,有的对象变成新垃圾,
    3.重新标记(remark):此阶段用于标记并发阶段新指向的对象; — 此阶段是STW的,但是remark标记的对象不会很多,
    4.并发清理:用户线程也在同时进行,此时任然会产生新的浮动垃圾,这些浮动会在下一次的GC过程中回收所以该阶段的时间也很短
  3. 弊端分析:会产生内存碎片化

G1垃圾回收器

  1. 核心做法及思维模式亮点:彻底改变了以前垃圾回收器的内存布局,前代垃圾回收器都是分代但是各代都是一整块内存,带来的问题就是整块内存过大,导致回收时间过长,而G1将老年代和新生代分成一个个小的region,每次回收部分region中的对象,从而提高响应时间;充分利用了分而治之的思想;
  2. 内存分区:G1在逻辑依然是分代的,依然有Eden,Survivor,Old,添加了Humongous区域来分配大对象;而且G1的各个区域分配不是固定的,比如Eden区的对象进行了回收后,他可能下次会被分配为old区;
  3. 特点:
    1.并发收集
    2.压缩空闲空间不会延长GC的暂停时间
    3.更易预测的GC暂停时间
    4.适用不需要实现很高吞吐量的场景,G1相对与Parallel再吞吐量下降低了10%,但在响应时间上提升了很多;
    5.追求响应时间:-XX:MaxGCPauseMillis 200,对STW进行控制
    6.灵活:分Region回收,优先回收花费时间少,垃圾比例高的Region
  4. 新老年代比例:
    5% - 60%
    一般不用手工指定,也不要手工指定,因为这是G1预测停顿时间的基准;
    G1会对每次垃圾回收的时间进行回收,如果STW超过了预测时间,则会动态的调整新生代比例,来减少STW的时间
  5. GC何时触发
    YGC:Eden空间不足,多线程并行执行;
    FGC:Old区空间不足,System.gc();
    G1是否分代?G1垃圾回收器会产生FGC吗? – G1逻辑上还是分代的,会产生FullGC
    G1会产生FGC吗? – 选择G1要尽可能的避免FGC

G1如果G1产生FGC,应该怎么做

  1. 扩内存
  2. 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
  3. 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)MixedGC在堆内存达到设置的阈值时,就会进行,它的回收过程和CMS几乎一致,只是最后的回收阶段,是采用并行筛选回收,会有一个筛选过程,优先回收花费时间少,垃圾比例高的Region,没有满的Region能够合并会合并到一个Region,且回收后会进行内存的压缩,也不会产生内存碎片化;

Collection Set

  • 一组可被回收的分区的集合,这里的分区指的是card;

RememberedSet

  • 在数据结构上它是一个HashSet,记录其他Region中对象对本Region中对象的引用;
  • RSet的价值在于:它可以让垃圾回收器在不需要扫描整个堆就能够找到谁引用了当前分区中的对象,只需要扫描RSet即可;

并发标记算法

  1. 难点:在标记过程中,对象引用关系正在发生改变;
  2. 三色标记:
    黑色:自身和成员变量均已标记完成的;
    灰色:自身被标记,成员变量未被标记;
    白色:未被标记的对象;
  3. 漏标:
    达成漏标的充分必要条件:1.黑对象指向了白色对象;2.删除了所有灰色到白色对象的引用;
    上述条件想要达到不漏标:需要重新扫描黑色对象;
    避免漏标的做法:
    1.incremental update – 增量更新(关注引用的增加),把黑色对象重新标记为黑色,下次重新扫描属性; —CMS
    2.SATB snapshot at the beginning – 关注引用的删除,当B->D消失时,要指向白色对象的这个引用推到GC的堆栈,保证D还能被GC扫描到; —G1
  4. 为什们G1用SATB?
    灰色对象指向白色对象引用消失时,指向白色对像的引用会被push到堆栈,下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高;