文章目录
如何判断对象已死?
- 引用计数法:效率高,但无法解决对象之间相互引用的问题。如 s.baby=q,q.baby=s;
- 可达性分析法:将GC Roots作为根对象向下搜索,形成引用链,如果不在引用链就证明该对象不会被使用。
在Java技术体系中,固定可以作为GC Roots的对象包括以下几种:
1)在虚拟机栈中引用的对象,如使用到的参数、局部变量等。
2)在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,还有系统类加载器等。
4)所有被synchronized持有的对象。
判定不可达的对象也不一定会死亡:
发现不在引用链,它会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。如果有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。
这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己一只要重新与引用链上的任何一一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第次标记时它将被移出“即将回收”的集合如果对象这时候还没有逃脱, 那基本上它就真的要被回收了。
强软弱虚引用
方法区的常量、类型是否被应该被回收
常量没有被引用就可被回收。
判断类型是否可回收:
- 该类所有实例都已经被回收,也就是Java堆中不存在该类及其任何派生类的实例。
- 加载该类的加载器被回收。
- 该类对应的java.lang.Class对象没被任何地方引用。
在大量使用反射、动态代理、CGLib等字节码框架时,都需要JVM有具备类型卸载的能力,以保证不会对方法区造成过大的压力。
垃圾收集算法
-
分代收集理论:为了针对不同的区域安排与里面存储对象死亡特征相匹配的垃圾收集算法.因而发展出了2、3、4。(下u他仅作参考,young和old比例当然不是如下图所示、且现在无永久代)
-
标记-清除算法:
1)执行效率不稳定
2)内存碎片多 -
标记-复制算法:将内存分成两块,每次只使用其中的一块,垃圾回收时将存活对象转移到另一块。适用于存活对象较少的区域,如新生代。
1)每次移动堆顶指针按顺序分配无内存碎片,实现简单高效
2)内存缩小了原来的一半(假如按半分的话)。
举例:Serial、ParNew等新生代收集器均采用了 Apple式 设计新生代布局。
-
标记-整理:与标记-清楚算法的差异是前者是移动式的。
1)移动时内存回收更复杂、但是不移动内存回收很复杂。
2)对象移动操作必须全程暂停用户程序才能进行。
HotSpot对象判活算法
枚举GC Roots:必须暂停用户线程。
- 必须让所有程序到达安全点,垃圾收集才可开始。如何到达安全呢:垃圾收集开始时,对所有用户线程发起中断,如果不在安全点就恢复线程的执行再重新中断,直到跑到安全点上。
- 但是一些用户线程处于Sleep状态或Blocked状态,无法响应中断请求,不能走到安全点去挂起自己,虚拟机显然也不能等这些线程重新被激活。对于这种情况,就必须引入安全区域来解决。
- 当用户线程执行到安全区域里面的代码时,首先会标识自己已经进人了安全内区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
记忆集与卡表:解决跨代引用所带来的问题,用以避免把整个老年代加进GC Roots扫描范围。
然鹅我不会!我不会!我不想学习了。
Serial 收集器(新生代)
对应的老年代收集器是 Serial Old收集器。
读音:美[ˈsɪriəl]
1)额外内存消耗最少
2)对于单处理器或者处理器核心较少的环境来说,Serial收集器由于没有线程交互的开销,可获得最高的单线程收集效率。
ParNew 收集器(新生代)
ParNew收集器实质上是Serial收集器的多线程并行版本。
只有ParNew能和CMS配合工作。
Concurrent Mark Sweep (老年代收集器)
- 初始标记: 暂停所有的其他线程,仅标记下直接与 GC Roots 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
优点:并发收集低停顿
缺点:
- CMS默认启动回收线程数是(处理器核心数量+3)/4,也就是说当处理器核心数量不足4时,CMS对用户程序的影响变大。
- 在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够的内存给用户线程用,因此CMS不能等老年代几乎快满了菜收集。要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次并发失败,虚拟机这时采用后备方案:临时启用Serial Old收集器来重新进行老年代的垃圾收集。所以参数-XX:CMSInitiatingOccupancyFraction设置的太高容易导致大量并发失败,性能反而降低。
- CMS基于“标记-清除”算法实现,收集结束后会产生大量碎片。**当碎片过多分配不了大对象会提前触发一次Full GC。**有一个参数是 CMS执行若干次收集后下次执行收集先进行碎片整理,但JDK9开始废弃。
Garbage First(Mixed GC)
概念:将连续的Java堆划分为多个大小相等的独立区域,每一个Region都可以根据需要,扮演Eden、Survivor或者老年代。收集器能够对扮演不同角色的Region采用不同的策略去处理。哪块内存的垃圾最多、回收就收益最大。 G1认为超过Region容量一半的对象为大对象,这些大对象放在连续的Humongrous Region中。G1仍保留新生代和老年代的概念,但是新生代和老年代都不是固定的了。Region是单次回收的最小单元。
参数:
-XX:G1HeapRegionSize设定每个Region的大小。
-XX:MaxGCPauseMillis:指定用户设定允许的收集停顿时间,默认200ms。(这个参数调的很低的话,每次选出的回收集只占堆内存最小的一部分,收集器的速度逐渐跟不上分配器分配的速度,最终占满堆引发Full GC反而降低性能。)
运作过程:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来指定回收计划,可以自由选择多个Region构成回收集,把存活对象复制到空的Region,在清理到整个旧Region全部空间。由于涉及到存活对象的移动,必须暂停用户线程。由多条收集器线程并行完成。
优点:G1运行期间不会产生空间碎片
缺点:G1为每个Region维护一些信息占用大量内存,所以G1适用于内存多的机子上。
GC触发条件
RednaxelaFX的回答
young GC:新生代eden区满时候触发,触发后部分存活对象可能会晋升到老年代。
full GC:当触发一次young GC时老年代连续空间 不大于新生代对象中大小 也不大于 历次晋升平均大小,就会触发Full GC。System.gc()会进行Full GC。除了CMS,收集老年代的时候都会同时收集整个GC堆,不用提前触发Full GC。当CMS碎片过多分配不了大对象也会触发Full GC.