一、垃圾收集器
1.如何确定对象已死
1.1.引用计数法-Reference Counting
给对象添加一个引用计数器,当有新的地方引用它时,引用计数器加1,当引用失效时,计数器减1,任意时刻计数器为0的对象就是不可能被再使用了。这种方式实现简单且高效,但是很难解决循环引用的问题,例如有两个对象A、B,除了相互引用之外,并没有可达引用可以访问到它们中的任意一个,这种情况下其实它们已经是垃圾对象,但是它们的引用计数器都不为零。
1.2.可达性分析-Reachability Analysis
这个方法的基本思想是通过一系列的称为 “GC Root”的对象作为起点,从这些节点往下搜索,所经过的路径称为引用链,当一个对象到“GC Root”没有任何引用链相连时,则证明此对象是不可用的。这一些列GC Root对象是哪些对象呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法区中JNI(即一般说的Native方法)引用的对象
note: 关于Java中的4中引用可参考文章《Java中的4种引用类型》
2.生存还是死亡
- 当一个对象与GC Root没有任何引用链时,那么它会被第一次标记并且进一步筛选,筛选的条件是此对象有没有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被调虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。
- 若有必要执行,则将对象放置一个F-Queue的队列中,并在稍后有虚拟机自动建立的、优先级低的Finalizer线程去执行,但并不承诺等待它运行结束,因为不知道finalize()方法的的耗时,盲目等待有可能造成阻塞。
- finalize()方法是对象逃脱死亡命运的最后一次机会,如果在finalize()方法中成功拯救了自己,那就可以继续存活,如果没有逃脱,那么它就真的被回收了。
3.方法区回收
废弃常量回收,当一个常量没有被任意一个引用变量引用时,此常量就是废弃常量;
无用类卸载,无用类要满足下面3个条件:
- 该类的所有实例都被回收了,即Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过发射访问该类的方法。
4.垃圾回收算法
4.1.标记-清除(Mark-Sweep)
首先标记所有可回收的对象,在标记完成后,统一回收。
特点:标记清除这两个过程效率都不高,另外一点就是出现内存碎片化问题;
4.2.复制算法(Coping)
将可用内存分为大小相等的两块,只使用其中一块,当这一块用完时,将还存活的对象赋值到另外一块中,然后在把这一块上已使用的一次回收掉。
特点:实现简单、高效,但这使得可用内存变为原来的一半,代价有点儿高。
现代商业虚拟机在新生代中采用这用算法回收,将新生代按照 8:1 的比例分为 Eden 区和两个 Survivor 区,每次只使用Eden和其中一个Survivor,当使用完时,将Eden和Survivor上的存活对象复制到另外一个Survivor上,如果存活的对象超过Survivor的大小,则使用老年代。这样每次使用的内存就是原来的90%,浪费的只有10%。
4.3.标记-整理(Mark-Compact)
标记过程与4.1类似,但后续过程是让所有存活对象向一端移动,让后直接清理掉边界以外的内存。
4.4.分代回收算法
这并不是一种新的思想,而是根据具体的场景采用合适的回收算法。Java虚拟机一般把堆分为新生代和老年代,新生代的特点是每次垃圾收集时都发现大批对象死去,存活率低,适合采用复制算法;而老年代存活率很高,则采用“标记-清除”或者“标记-整理”算法来进行回收。
5.HotSpot实现
5.1根节点枚举
Stop the world,OopMap,不会为每条指令生成对应的OopMap,只在特定的位置记录这些信息,即安全点。
5.2.安全点
抢占试中断,在发生GC时,中断所用用户线程,如果发现某个线程不在安全点,则恢复此线程,让其跑到安全点。现在基本没有虚拟机这个干。
主动式中断,当发生GC时,只是设置标记位,让用户线程自己在安全点检查这个标记,如果是ture则自己中断挂起,另外一个检测的地方时需要分配内存的地方。
5.3.安全区域
SafePoint似乎能很好的解决进入GC的问题,但试想这样一个场景,当某些线程处于sleep或者blocked状态时,虚拟机很难说等待这些线程得到CUP资源并跑到附近的安全点上,这个时间是不确定的,这就引入了安全区域的概念,当一个线程进入安全区域时,标记位自己在安全区,发生GC时,就不用管在安全区域的线程了,当要离开安全区时它检查是否已经完成了GC,否则中断挂起等待。
6.垃圾收集器
图中的连线表示垃圾收集器可以配合工作
6.1.Serial
新生代收集器,单线程GC,采用复制算法,所用用户线程跑到SafePoint中断,然后执行GC,GC完恢复用户线程,这会导致Stop The World。
6.2.ParNew
多线程版的 Serial。
6.3.Parallel Scavenge
新生代收集器,复制算法,并行多线程收集器,看上去和ParNew一样,但它的关注点不一样,前两个收集器的关注点是尽量缩短GC导致用户线程的停顿时间,而这个收集器的关注点是吞吐量,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
6.4.Serial Old
老年代收集器,单线程,使用“标记-整理”算法。
6.5 Parallel Old
是Parallel Scavenge的老年代版本,使用“标记-整理”算法。
6.7 CMS
Concurrent Mark Sweep收集器,关注点是获得最短的回收停顿时间,从名字就看得出来是“标记-清除”算法,它运作过程分为四个步骤:
- 初始标记(CMS initial mark)- Stop The World
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)- Stop The World
- 并发清除(CMS concurrent sweep)
在四个步骤中,其中并发标记、并发清除时间是相对较长的,都是可以和用户线程并发执行的,所以Stop The World时间是很短的,总体上来看就是并发执行的,这对要求响应速度较快的应用场景比较适合。CMS还远达不到完美,它有一下几个缺点:
- 对CUP资源敏感,抢占CUP资源将导致用户线程的CUP资源减少而变得缓慢;
- 无法处理浮动垃圾,在并发回收垃圾时,用户线程会产生新的垃圾对象,这些垃圾要等下次回收;由于在并发回收的过程用户线程还在工作,这就需要预留一定的内存空间给用户线程,导致内存空间利用率下降;
- CMS采用的是标记-清除算法,这就导致内存碎片化。若出现内存空间还很多,但由于碎片化的情况,无法满足大对象的分配,当顶不住要触发Full GC时开启内存碎片合并整理过程,这个过程是不能并发的,会Stop The World。
6.8.G1
Garbage First,将内存分为多个Region,使用Remembered Set 避免全盘扫描,标记-整理算法。
- 初始标记(initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting Evacuation)
优点:
- 并行与并发,能充分利用CUP资源;
- 空间整理,与CMS的标记-清理相比,它采用的是标记-整理,从局部Region来看又是复制算法;
- 可预测停顿,可以让使用这指定在长度为M毫秒的是时间内,垃圾收集时间不能超过N毫秒;
二、内存分配策略
- 对象优先新生代Eden区分配
大多数情况下,对象在新生代Eden区分配,若Eden区无法分配,则虚拟机会触发一次Minor GC。
Minor GC-新生代; Major GC/Full GC-老年代
- 大对象直接直接进入老年代,大对象的界定可通过参数设定;
- 长期存活的对象进入老年代,对象的年龄,Minor GC一次且能被Survivor分区容纳则加1,默认是15岁进入老年代;
- 动态年龄判断,虚拟机并非永远要求对象的年龄到达了MaxTenuringThreshold才能晋升老年代,如果相同年龄的对象总时占据Survivor分区的一半及以上,年龄大于或者等于改年龄的对象就可以直接进入老年代,无须等到年龄到达MaxTenuringThreshold;
- 空间分配担保,处理Minor GC的风险问题,老年代为新生代担保,根据具体情况看是是否执行Full GC,比如老年代的剩余连续空间比新生代大,那就没有必要Full GC,这种情况下Minor GC是没有风险的。
参考:https://blogs.oracle.com/jonthecollector/our-collectors
上一篇:JVM(1)-运行时数据区
下一篇:JVM(3)-类加载机制