Java的垃圾回收机制
浅析Java的垃圾回收机制
1.引用的强弱(运用见后续io)
强引用:强引用不会被回收
软引用:用来描述一些有用但是非必须的对象,在将要发生溢出之前,会把这些对象列为第二次回收的对象,假如回收后空间依旧不足,那么将会抛出异常
弱引用:强度比软引用弱一些,垃圾时会作为垃圾被回收
虚引用:比弱引用弱;无法通过虚引用为对象获取实例
2.垃圾回收原则
2.1堆垃圾回收原则
假如这个对象被认为是可回收的对象,那么会判定是否需要执行finalize()方法,执行finalize方法的前提条件是有覆盖 finalize方法和没有执行finalize方法;假如没有覆盖和执行了那么认为是不需要执行finalize方法的,作为垃圾回收,所以作为垃圾回收那么至少需要被标记两次;假如认为是有必要执行finalize方法,那么会加该对象假如F-Queue队列中,用优先级很低的线程区执行;
2.2方法区回收原则:可回收的垃圾很少,可回收的部分包括两部分,废弃的常量和无用的类,常量的回收类似堆对象的回收,当没有其它地方引用这个字面量时,就会作为垃圾回收,类的回收,需要满足,该类所有的实例都已经被回收(java堆中不存在该类的任何的实例)及加载该类的classloader以及被回收,且该类的对应的class对象没有被其它任何地方引用,类的回收不是必然的,仅是可以,是否真正的被回收是虚拟机的参数进行控制的,java虚拟机规范中确实也说过可以不要求虚拟机在方法区实现垃圾的收集
3.垃圾回收算法
1> 引用回收算法
给对象添加一个引用计数器,当一个地方引用它时计数器加1,当引用失效时,计数器减1,当为0的时候被认为是可以被回收的,实现简单,效率较高,但是很难解决对象之间相互循环引用的问题
2> 根搜索算法
从GC Roots对象为起始点开始向下搜索(可作为GC Roots对象包括如下几种,虚拟机栈(栈桢中的本地变量表)中的引用的对象,方法区中的类静态属性引用的对象,方法区中的常量引用的对象,本地方法栈中JNI(Java Native Interface的缩写)的引用的对象),搜索所有路过的路径,当一个对象没有被相连时,那么认为为可回收对象进行标记,根搜索算法中被认为可以被回收的对象是需要至少经过两次被标记才可能会被回收(基本上主流的商业程序语言如java ,c等都是使用根搜索算法来实现的),根搜索的主要来源来至于局部变量表。
3> 标记-清除算法
算法分为标记和清除两个阶段,首先标记需要回收的对象,在标记完成后统一回收所有被标记的对象,他存在连个缺点,标记和清除的效率不高,空间不连续(空间碎片过多的原因)导致存储大对象的时候空间不足的问题
4> 复制算法
复制算法是基于标记清除算法之上的,当已定义的内存剩余的空间不足以存放新的实例的时候,就将还存活的对象复制到另一块内存上(这块空间可能是存在的(CMS等),也可能是新建的(G1),这块空间属于堆空间的一部分)。
4.CMS收集器的复制算法
在hotspot中的默认的比例为8:1(90%的空间)大小的一块eden和两块survivor;每次垃圾回收的时候都会对eden和survivor存活的对象一次性拷贝到另一块survivor空间上并清空。
5.标记整理算法
这是针对老年代回收的一种算法,标记过程和标记清除算法一样,不同的是后续不是直接对可回收对象进行清理而是让存活的对象都移向一端,然后清除边界以外的内存
6.分代收集算法
商业虚拟机的垃圾回收都是采用的分代收集算法,一般会把java分为年轻代和老年代,对于年轻代采用复制算法,对于老年代采用标记整理算法,对巨型区域也是采用标记整理算法。巨型区域也可以认为属于老年代的一部分,防止复制移动。
7.堆的分代
年轻代young:存放新创建的对象,采用复制算法。
老年代tenured:存放着经历多次垃圾清理仍然存活的对象,采用的标记整理算法(CMS是个例外,CMS采用的是标记清除算法),
Humongous Region(巨型区域):在G1收集器之中新增了Humongous Region(巨型区域),Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,他们被存储在一系列的连续区域内未使用的区域内。
8.元空间Metaspace(jdk1.8):方法区中的数据,存储位置不再占用堆空间空间而是在物理内存,之所以需要这么做的原因是:例如在字符串存在较多的情况下导致内存溢,且类及方法的信息等比较难确定其大小,因此对于永久代的大小指定也比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出,所有将其改为放置在元空间(本地内存),取消PermGen(永久代),注意:年轻代和老年代在堆中,元空间在本地内存中
9.CMS年轻代进入老年代的过程
在eden内存中生成的年轻代在每次进行gc之后,那么年龄就会加一,当增加到一定年龄后进入老年代(默认是15次),对于大对象在年轻代垃圾清除后仍然空间不足,那么会该对象全部直接的进入老年代(但是需要保证老年代有足够的保存空间,一共会存活多少对象在回收之前是无法确定的,所以只能用平均数,存在空间不足时候会导致担保失败,如果担保失败会进一步的full gc(空间分配担保)),在survivor空间中相同年龄对象大小的总和大于survivor空间的一半,那么大于或等于该年龄的对象就可以直接的进入老年代,无须等到-XX:MaxTenuringThreshold中设置的年龄,老年代的垃圾回收比年轻代的垃圾回收慢10倍以上,老年代进行垃圾回收不一定会年轻代会进行垃圾回收(Parallel Scavenge收集器的收集策略里就有直接进行老年代垃圾回收的策略选择过程)
10.CMS空间分配担保
在晋升到老年代之前会检查每次晋升到老年代的平均大小是否大于老年代所剩余的空间,如果大于,则会依次full gc ,如果小于,则会查看HandlePromotionFailure设置是否允许担保失败,如果允许,那么只会进行年轻代垃圾清理,如果不允许那么会进行full gc(年轻代和老年代垃圾清理)。
11.G1收集器年轻代进入老年代的过程
G1把整个Java堆划分为若干个不连续的区间。每个区间大小为2的倍数,范围在1MB-32MB之间,可能为1,2,4,8,16,32MB。所有的区间有一样的大小,JVM生命周期内不会改变。在经过yong gc之后,存活的对象会被转移(拷贝或移动)到1个或者多个survivor区域。如果达到了年龄阈值,一些对象会被晋级到老年代的区域,当然对一些超过50%标准region大小的大对象,会被存储在一系列的连续区域内未使用的区域内(这块区域可以认为是老年代的一部分)。
12.G1年轻代垃圾回收GC (young)
年轻代垃圾收集(Young GC)会暂停其他线程(STW),采用多线程并行收集,存活的对象被拷贝到新的survivor区域或者达到年龄的阈值进入老年代
13.G1老年代垃圾回收GC (一般情况下是mix gc(年轻代和一部分老年代) full gc开销很大尽量避免)
初始标记:标记从根节点直接可达对象,伴随一次新生代GC,产生暂停服务
根区域扫描:由于初始标记存在一次新生代GC,所以在初始化标记后,Eden被清空,并一入幸存区
并发标记阶段
如果找到空区域会被标记,它们在重新标记阶段立刻被删除,此外还会判断是否存活的统计信息会被相应的计算。G1没有像CMS一样的清理阶段
重新标记阶段
使用Snapshot-at-the-Beginning (SATB)算法使得重新标记比CMS更快,空区域会被删除和回收,所有区域的存活信息会被统计。
复制/清理阶段
年轻代和老年代同时被回收,老年代收集的区域的优先级是根据他们的存活信息来选择的。
14.G1的暂停时间和优先级
G1可以设定目标的暂停时间,但是G1不是一个实时垃圾收集器。它尽可能的符合设定的目标暂停时间,但是不能绝对实现。G1在执行并发标记和重新标记之后来确定整个堆中对象的存活信息。根据存活率来判断垃圾回收的优先级,在后台维护一个优先的列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
15.G1与CMS的对比
G1相对于CMS对比空间连续,时间可控。但是不论CMS和G1都是线程交替的过程,在线程特别繁忙的场合会出现内存不充足的情况,当遇到这种情况会进行fullGC,在新老区域都不能容纳幸存对象的时候会进行fullGC。
16.实例:
16.1引用计数算法的缺陷(相互引用)
public class Test2 {
public Object instance =null;
public int[] arr=new int[1024*1024*2];
public static void main(String[] args) {
Test2 test2 = new Test2();
Test2 test3 = new Test2();
test2.instance=test3;
test3.instance=test2;
test2=null;
test3=null;
System.gc();
}
}
16.2被认为是可回收对象的自我拯救
public class FinalizeTest1 {
public static FinalizeTest1 save_hook = null;
public void islive() {
System.out.println("yes:i am alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method execute!");
FinalizeTest1.save_hook = this;
}
public static void main(String[] args) throws Throwable {
FinalizeTest1 collection = new FinalizeTest1 ();
// 不写上述一行的结果是no:i am dead no:i am dead null
/*原因:首先是FinalizeTest1e重写的finalize方法,所以需要FinalizeTest1e对象为null的时候才会调用finalize方法进行自救*/
collection=null;
System.gc();
Thread.sleep(1000);
if (save_hook != null) {
save_hook.islive();
} else {
System.out.println("no:i am dead");
}
save_hook = null;
System.gc();
Thread.sleep(1000);
if (save_hook != null) {
save_hook.islive();
} else {
System.out.println("no:i am dead");
}
System.gc();
System.out.println(save_hook);
}
}// finalize method execute! yes:i am alive no:i am dead null