1.垃圾收集算法
(1)判断对象是否存活
①引用计数法(JVM中不使用)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数值就加1,;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。主流的JVM中没有选用引用算法来进行内存管理,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
class TestA{
public TestB b;
}
class TestB{
public TestA a;
}
public class Main{
public static void main(String[] args){
A a = new A();
B b = new B();
a.b=b;
b.a=a;
a = null;
b = null;
//假设在这行发生GC,a和b能否被回收?
System.gc();
}
}
最后面两句将a和b赋值为null,也就是说a和b指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。
对象a和对象b之间存在循环引用,但在执行到System.gc()时,可以发现两个对象仍被回收,故JVM没有采用引用计数算法来判断对象的存活性。
②可达性分析
在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程(第二次标记时执行对象的finalize()方法,若在此方法内对象能和GC roots重新连接则对象“复活”,否则被清理。但注意finalize()方法仅能被执行一次,且不提倡使用),如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。
此算法的核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。以下图为例:
对象Object5 —Object7之间虽然彼此还有联系,但是它们到 GC Roots 是不可达的,因此它们会被判定为可回收对象。
在Java语言中,可作为GC Roots的对象包含以下几种:
1 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象,方法中定义的对象)
2 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象,static对象)
3 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象,static final对象)
4 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)
(2)回收方法区(永久代)
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。废弃常量回收比较简单,就是看是否仍有其他地方引用了此字面量,若无引用,则被回收。而判断一个类是否需要被回收条件比较苛刻,需要同时满足下面三个条件才能算是“无用的类”:
1 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
2 加载该类的ClassLoader已经被回收
3 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
(3)垃圾收集算法
①标记-清除算法
首先标记出需要回收的对象,在标记完成后统一回收掉所有的被标记对象。
缺点:效率问题(标记和清除两个过程的效率都不高)和空间问题(标记清除后会产生大量的不连续内存碎片,内存碎片过多可能会导致程序需要分配较大对象时找不到足够大的连续内存空间而不得不提前触发另一次垃圾回收动作)
②复制算法
“复制”(Copying)的收集算法,它将可用内存按需要分成两块(实际情况不一定2块,块的大小比例不一定),每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:每次只对其中一块进行GC,不用考虑内存碎片的问题,并且实现简单,运行高效。(适用于新生代,效率高)
缺点:内存缩小了一半。复制的目标空间需要依赖其他空间进行分配担保(3中详细阐述)。效率随对象存活率升高而降低:当对象存活率较高时,需要进行较多复制操作,效率将会变低。
③标记-整理算法
因为老年代没有额外的分配担保空间,故不能采用复制算法。使用“标记-整理”算法:先标记,再把所有存活的对象向一端移动,然后直接清理端边界意外的内存。
优点:不会像复制算法、效率随对象存活率升高而变低。不会像标记-清除算法,产生内存碎片(因为清除前,进行了整理,存活对象都集中到空间一侧)。
缺点:主要是效率问题:除像标记-清除算法的标记过程外,还多了需要整理的过程,效率更低。
2.垃圾收集器
(1)Serial及Serial Old收集器
Serial收集器对新生代进行垃圾收集,采用标记—复制算法,是一个单线程收集器(进行垃圾收集时需要暂停所有用户线程,即“stop the World”)。对于运行在Client模式下的虚拟机来说是一个很好的选择。
Serial Old收集器时Serial收集器的老年代版本,采用标记—整理算法,也是单线程收集器。
(2)ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,使用多个垃圾收集线并行程对新生代进行垃圾回收。是运行在Server模式下的首选收集器,因为只有它能与CMS收集器配合工作。
(3)Parallel Scavenge/Parallel Old收集器
Parallel Scavenge是一个新生代垃圾收集器,使用标记—复制算法,多线程垃圾收集器。使用Parallel Scavenge收集器的目的是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),例如虚拟机总共运行了100分钟,其中垃圾收集花费1分钟,吞吐量就是99%)。
Parallel Old是Parallel Scavenge的老年代版本,使用多线程和标记—整理算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器与Parallel Old收集器的组合。
(4)CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的老年代收集器。基于标记—清除算法实现,整体分为4个步骤。
CMS收集器工作步骤:
1.初始标记:暂停所有用户线程(stop the world),标记直接与GC Roots关联的对象。
2.并发标记:单个垃圾收集线程与用户线程并发执行,通过GC Roots引用链追踪标注所有存活对象。占用绝大部分GC时间。
3.重新标记:暂停所有用户线程(stop the world),修正并发标记期间因用户线程运作而变动的对象标记。
4.并发清除:单个清理线程与用户线程并发执行,进行垃圾清除操作。
CMS收集器的缺点:
1.对CPU资源敏感(并发标记及并发清除阶段与用户线程同时执行,需要占用较多的CPU资源,造成用户程序执行速度下降)。
2.无法处理“浮动垃圾”。“浮动垃圾”就是在并发清理阶段产生的垃圾,这些垃圾出现在标记过程之后,CMS无法在档次收集中处理他们,只好留在下一次GC时清理。(详细介绍见《深入理解JVM》P83)
3.由于CMS是一款基于标记清理算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,会给在老年代大对象的分配带来很大麻烦。
3.内存分配策略
JVM中将对象年代划分为:新生代、老年代以及永久代。对象的内存分配,主要是在堆上进行分配,而大多数对象都分配在新生代的Eden区上,少部分大对象直接分配在老年代上,但这也取决于垃圾收集器组合及虚拟机相关参数设置。
1.JVM堆结构分析
HotSpot JVM将新生代划分为三个区域:一个Eden区 和两个Survivor区(分别叫from(S1)和to(S2))(Eden和一块Survivor存储垃圾回收前的所有对象,另一块Survivor存储垃圾回收时复制的存活对象)。由于新生代采用“标记—复制”算法进行垃圾回收,而新生代中的对象90%都是“朝生夕死”的,所以将Eden和Survival的分配比例默认设置为8:1。当进行垃圾回收时,将Eden和Survivor中存活的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。(详细来说新生代的“复制—清理”算法执行过程中还牵扯到老年代的分配担保机制及新生代对象年龄判定,下面会详细阐述)具体可参考下面的JVM内存体系图。
一个较详细的新生代“复制—清理”过程描述:
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
2.内存分配策略
先论及两个概念
Minor GC 和 Full GC 的区别:
新生代GC (Minor GC):指发生在新生代的垃圾收集动作,因为Java时象大多都具备朝生夕灭的特性,所以 Minor GC非常倾繁,一般回收速度也比较快。
老年代GC (Major GC / Full GC ):指发生在老年代的GC,出现了 Major GC,经常会伴随至少一次的 Minor GC Major GC的速度一般会比Minor GC慢10倍以上。
JVM在内存分配时主要遵从以下几条策略(总述):
1.大多数新生代对象都在Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。(分配担保机制:以老年代内存空间为新生代Survivor空间(“TO”)作担保,若Survivor空间不够存放Minor GC后的对象,就将这些对象提前转移到老年代中。在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于的话,那么这个GC就可以保证安全,如果不成立的,那么可能会造成晋升老年代的时候内存不足。在这样的情况下,虚拟机会先检查HandlePromotionFailure设置值是否允许担保失败,如果是允许的,那么说明虚拟机允许这样的风险存在并坚持运行,然后检查老年代的最大连续可用空间是否大于历次晋升老年代对象的平均大小,如果大于的话,就执行Minor GC,如果小于,或者HandlePromotionFailure设置不允许冒险,那么就会先进行一次Full GC将老年代的内存清理出来,然后再判断。)若在Minor GC期间,虚拟机发现存活的对象无法全部容纳在Survivor空间中,便提前将这些对象通过分配担保机制
参考:
1.《深入理解java虚拟机》:周志明
2.JAVA垃圾回收-可达性分析算法:https://blog.csdn.net/luzhensmart/article/details/81431212
3.JAVA垃圾回收器与垃圾回收算法:https://blog.csdn.net/newchenxf/article/details/78071804
4.java垃圾收集算法:https://blog.csdn.net/maybe423/article/details/80405032
5.JVM(九)内存分配略: https://blog.csdn.net/liupeifeng3514/article/details/79183734