垃圾回收机制

Java GC算法 垃圾收集器

资源泄漏异常

1.1内存溢出

程序申请内存的时候,没有足够大的连续内存空间,导致异常

1.2内存泄漏

程序申请到内存后,使用完成没有释放掉内存空间,导致GC的可达性可以被监测到,所以不会被GC回收
内存泄漏最终会导致内存溢出

  1. 内存泄漏原因通过跟踪GC Roots的引用链,找到泄漏对象如果与GC Roots关联却不被释放。
  2. 是否对象生命存在周期过长

2. 线程栈和本地方法栈溢出

线程数量 = 分配给线程栈的内存是扣除堆内存剩下的内存 / 每个栈容量
如果每个线程过大,扩展线程时会导致溢出,这时候解决方案是"减少堆内存"

  1. 栈深度大于虚拟机所允许的最大深度
  2. 扩展栈时无法申请到足够内存

3. 运行常量池溢出

String.intern()导致,此方法是Native方法。若池中存在此String对象,则返回池中的String对象;否则,将此对象字符串添加到常量池中。


回收条件

判断一个对象是否死亡,有两次标记

  1. 是否与GC Roots有引用链,是否有finalize()方法被覆盖,或者被执行,会执行此方法(此方法可以让对象逃脱被回收),将对象放入F-Queue队列中
  2. 如果F-Queue队列中还存在此对象,则回收

finalize()方法只会被系统调用一次
请忘记这个方法,这是C++的析构函数,已经没人用了

Thread Stack
PC Register
Native Thread Stack

都是使用StackFrame棧帧随着出入栈实现自动内存清理

Heap
Method Area

分配是动态的,内存的回收需要使用GC来实现


Method Area 讨论

主要回收的是废弃常量无用类

  1. 废弃常量是当前系统没有任何一处引用该常量
  2. 无用类需要满足3个条件:
    • 该类所有实例都已经被回收,也就是说Java堆中不存在该类的任何实例
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  3. 为了避免内存溢出,在大量使用反射、动态代理的场景都需要虚拟机具备类卸载功能

Heap GC讨论

Garbage Collect(GC)在动态的回收内存是通过对象是否存活来判断:

  1. 引用计数:每个对象有一个引用计数器,新增引用+1,释放-1,计数器为0可以回收(无法解决循环引用问题)
  2. 可达性分析/根搜索算法:从GC Roots往下查询,所走过的路径为引用链。如果一个对象到GC Roots没有任何引用链接,则为不可达,可回收(进行GC Roots枚举时,必须stop the world来保证引用关系不再改变。所以这也要求GC Root的枚举不能太耗时
HotSpot
快速查找到GC Roots
使用一组叫做OopMap的数据结构。
(1) 记录每个类的每个数据位置(通过偏移量来确定)的类型(即是否是引用)。这一步在类加载后即可确定。
(2) 在特定的位置上记录栈和寄存器中哪些位置是引用。
这样我们在GC Root枚举时,可以快速找到所有引用。

设置安全点(只有在安全点上才有全部的OopMap信息,安全点由程序结构决定)
抢占式:在GC发生时,中断所有线程,如果发现有的线程不在安全点,
    则重新唤醒知道跑到最近的安全点。--这种方式几乎没人使用
主动式:线程主动发起。在GC发生时,在安全点位置上设置标记位,
    线程执行到安全点时轮询这个标志位,如果发现为true,则中断。

安全区(解决就绪线程态无限等待CPU导致无法GC进入安全点)
一段代码内无引用关系改变。
线程进入安全区代码后,标记自己“安全区”标记,GC发生时就不用管标记“安全区”的线程了。
当线程想要离开安全区时,判断GC是否结束,如果没结束要等GC结束后才能继续执行

GC Roots的对象

  • Thread Stack(StackFrame局部变量表)中 引用的对象
  • Method Area 类静态属性引用的对象
  • Method Area 常量引用的对象
  • Native Method Stack JNI中引用的对象

垃圾收集算法/方法论

并行(Parallel):用户工作线程停止,多条GC线程并行工作,可以同时处理多件事情(A B同时开始处理从0%->100% )
并发(Concurrent):用户工作线程与GC线程并发工作,GC集中在另一个CPU上,可以处理多个事情,不需要同时(先处理A 50%,再处理B 50%,再处理A 50%,再处理B 50%)

1. 标记-清除(Mark-Sweep)算法

标记出需要回收的对象,在标记完成后统一回收掉所有的被标记对象。
issues:

  1. 标记和清除的效率不高
  2. 标记清除后的会产生大量的不连续内存,碎片太多导致需要连续的大对象无法找到连续内存,再次提前促发垃圾回收

2. 复制(Copy)算法

将内存空间分成大小相等的两块,每次只使用一块。当一块用完类,将存活的对象copy到另一块上面,然后把使用的内存一次清空,在对象存活率低时回收的效率高
issues:

  1. 内存只有一半
  2. 长生存期的对象被持续复制效率低

3. 标记-整理(Mark-Compact)算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,在对象存活率高时回收效率高

4. 分代收集算法

基本假设是:绝大多数的对象生命周期都非常短暂,频繁回收,但是有部分对象生命周期比较长
将Java堆分为
Young Generation:每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
Old Generation:对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”“标记-整理”算法来进行回收

垃圾收集器/方法论实现

1. Serial收集器

串行收集器,只使用一个线程去回收,当回收的时候会Stop The World。
缺点:工作时会停止用户线程
算法:新生代,复制算法
关注点:响应时间优先

2. ParNew收集器

多线程串行收集器,当新生代占用达到一定比例时开始收集
缺点:工作时会停止用户线程
算法:新生代,复制算法
关注点:响应时间优先

3. Parallel Scavenge

关注点:吞吐量优先,各代比例自适应调整
算法:复制算法

4. Parallel Old收集器

5. CMS(Concurrent Mark Sweep)收集器

采用标记-清除算法,使用并发的方式
分为四个阶段:初始标记(stop-the-world)->并发标记->重新标记(stop-the-world)->并发清除
存在浮动垃圾,需要预留内存空间(默认68%),如果内存不足,会使用Serial Old收集器来执行。
优点:并发收集、停顿低,可以与用户线程并发工作最短用户线程停顿的时间
缺点:会产生大量的空间碎片,导致提前促发full gc(预留给标记中用户修改的空间),并发阶段会降低吞吐量(解决方案是在N次full gc后提供碎片整理
关注点:响应时间优先

分为四个步骤:三标记一清除

  1. 初始标记mark GC Root直接关联的对象
  2. 并发标记:进行GC Root的trace过程,进行可达性分析
  3. 重新标记:修正(2)时,由于用户线程也在运行导致的引用关系变化
  4. 并发清除:就是清除的过程,并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾(浮动垃圾),新的垃圾在此次GC无法清除,只能等到下次清理

(1)和(3)需要stop the world,但(1)和(3)的消耗时间非常短。
(2)和(4)为并发,即可以和用户线程同时运行。消耗时间相较于1和3较长
但由于是并发,所以整个收集器看来就是和用户线程并行运行。

垃圾回收机制_第1张图片
CMS流程.png

6. G1收集器

Young/Old Generation 不再是物理隔离,将Heap 划分成N个相等大小独立的Region,Young/Old Generation是一部分Region的合集,后台维护一个优先列表优先回收垃圾最多的区域
region和跨代之间的堆引用全局扫描问题,采用Remembered Set记录每次赋值引用时是否引用其它region,是则记录。(保证不会全堆扫描)
关注点吞吐量优先
优点:

  1. 整体来看采用标记-整理算法,局部(region)来看采用复制算法,不会产生大量碎片而导致无法分配大对象
  2. 可预测停顿,在指定N毫秒内的时间片段内,GC时间不超过N毫秒
  3. GC线程与用户线程可以并发执行

分为四个步骤:三标记一清除

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收:在运行的GC停顿时间内,选择回收价值最高的Region回收。这个阶段虽然可以和用户线程并行,但实际上由于所花费时间短,且停顿用户线程这个时间会更短,所以采用停顿用户线程的方式

常用的收集器组合

YoungGeneration: ParNew
OldGenration: CMS


YoungGeneration: G1
OldGenration: G1

Minor GC和Full GC

  1. Minor GC
    新生代的垃圾收集动作,由于新生代的Java对象存活率都很低,Minor GC会非常频繁,并且回收速度快
  2. Full GC
    老年代的垃圾回收动作,出现Full GC的时候通常会执行一次Minor GC。并且时间较长,会产生STW(stop the world)

GC促发条件

垃圾回收机制_第2张图片
整体分配策略.png
  • 对象优先分配在Eden区 如果Eden区没有足够的空间时,虚拟机执行一次Minor GC
  • 大对象直接进入老年代 (大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置(这个意思为是否担保失败),如果true则只进行Monitor GC,如果false则进行Full GC
    空间担保.png
  • 手动调用System.gc()方法,通常这样会触发一次的Full GC以及至少一次的Minor GC

你可能感兴趣的:(垃圾回收机制)