学习脉路
1.垃圾回收器回收的是什么?
对象
2.回收哪些对象?
已死的对象
3.如何判断对象已死?
3.1引用计数法
给对象添加一个计数器:有引用这个对象的地方就加一,反之减一,任何时刻计数器为零的对象就是死的。
为啥不用引用计数法判断呢?原因就是无法解决循环引用的问题(a引用b,b引用a)
3.2可达性分析
定义了一些GC roots的对象,任何对象如果可以通过各种引用关联到某个GC roots对象,那它就是活的。
哪些对象可以做为GC roots?
活对象
1.虚拟机栈中引用的对象
分析:就是说如果一个对象的引用是存在的,那么这个对象就是活的。
2.方法区中类静态属性引用的对象
分析:首先书上说:方法区的垃圾回收主要是针对常量池和对类型的卸载没有提及到对静态属性的回收,
那么我们可以理解成类的静态属性永久存活,换句话说这个对象的引用一直存在,所以这个对象一直是活的。
3.方法区中常量所引用的对象
分析:方法区中常量引用的对象,比如
final Student s = new Student();
student对象中有一个学校对象,因为s存在方法区的常量池且为强引用,所以student不会被垃圾回收,也就不会死
4.本地方法栈中JNI引用的对象。
分析:本地方法栈不受垃圾回收控制,引用一直存在(在本地方法调用期间)
再谈引用
1.强引用
程序中普遍存在的,类似Object obj = new Object();强引用永远不会被回收。
2.软引用
描述一些有用但非必须的对象,内存不够了(溢出之前)就会先回收它们
3.弱引用
描述非必须对象,无论内存够不够用,下一次垃圾回收的时候都会回收它。
4.虚引用
对象在被回收的时候会受到系统通知
生存还是死亡?
接下来讨论:是不是一个对象没有关联到GC roots就一定会被回收呢?其实不是这样的
总结:如果对象不可达会先对它进行一次标记,若该对象覆盖了finalize()方法,则改对象会被放入一个队列中,有一个低优先级的线程来执行这些对象的finalize方法,在执行此方法时,对象有最后一次机会拯救自己,比如:把自己赋给一个全局的静态变量。他就活了。
回收方法区(永久带):
回收永久带的内容为:常量和无用的类,但是性价比很低
回收常量:比如字符串“abc”,在代码中没有任何String类型的引用指向常量池中的这个常量,那么它就会被回收。
回收无用的类:
1.内存中不存在该类的实例
2.加载该类的ClassLoader对象也被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用
我们上面讨论的如果一个对象没有关联到GC roots那么就会宣判这个对象死亡吗?
不是这样的,如果一个对象没有关联到GC roots那么它会被标记一次,如果该对象覆盖了finalize方法,那么会将它放到F-queue中,由一个低优先级的线程来执行这个队列里对象的finalize方法,在执行对象此方法时,对象有最后一次机会自我拯救,比如:把自己赋给了一个类的经常变量。全局有一个变量:private static Student s = null;而在finalize方法里:s = this;那么它就活了。
需要注意的是:任何对象的finalize方法只会被执行一次。
垃圾回收算法:
1.标记-清除
标记:从GC roots对象开始遍历,所有与其相关联的对象都是可达的
清除:遍历堆,清除所有未被标记的对象
缺点:容易产生内存碎片,在分配大对象时容易导致连续的内存空间不足而提前触发Full GC,而且标记清除两个阶段效率都不高。
2.复制算法
将内存分成两块,每次只使用其中的一块,当进行垃圾回收时,先将存活的对象copy到另一块内存区域,然后清空已使用的内存区域。代价就是内存减少为原来的一半了。
备注:新生代采用这种回收算法,研究表明新生代的对象存活率很低(98%是死的),所以并没有按照 1:1的比例进行划分,而是将新生代分成了一个Eden和两个Survivor,比例是 8:1:1,每次使用Eden和其中一个Survivor,当需要垃圾回收的时候
为啥是8:1呢比例?
分析:根据上面说的新生代中98%对象都是死的,0.2% 乘以 10 小于1,所以1个单位的survevior已经够用通常来说,
但是如果回收的垃圾大于剩下的survevor空间,那就需要依赖老年代(分配担保),
3.标记-整理
标记:与标记清除的标记阶段一样
整理:将存活的对象往一边移,然后清除垃圾对象
4.分代收集
指的是:根据新生代和老年代对象的存活特点,而采用不同的回收算法,在新生代中,存活的对象很少,所以只需要用很少的额外内存就可以完成回收,而在老年代中对象存活率较高,没有额外的内存进行分配担保,所以需要采用标记-清除或者标记-整理
垃圾回收器
1.Serial收集器 一个单线程的垃圾收集器,在进行垃圾回收的时候,所有用户线程需要等待
在桌面应用中,系统分配给虚拟机的内存一般都不大,用Serial收集器进行垃圾回收。
2.ParNew收集器 是Serial收集器的多线程版本,在进行垃圾回收时,会有多条线程同时执行回收操作
如上图所示,目前只有serrial和parNew收集器能和cms收集器配合使用。
3.Parallel Scanvage收集器 控制吞吐量的收集器
-xx:maxgcpausemillis来控制垃圾回收的时间
4.Serial Old收集器 Serial收集器的老年代版本,采用的是标记-整理算法。主要用于给Client模式下的虚拟机的老年代使用
5.Parallel Old收集器 Parallel Scanvage的老年代版本,使用多线程的标记-整理算法
6.CMS收集器 它的目的是使垃圾回收时间最短,整个过程分为四个步骤
(1)初始标记、标记GC roots直接能关联到的对象
(2)并发标记、GC roots 追踪的过程
(3)重新标记、修复由于并发标记期间变动的那一部分对象(并发标记期间对象的死活状态可能发生改变)
(4)并发清除、
备注:初始标记和重新标记阶段用户线程是需要等待的,但最耗时的两个阶段:并发标记和并发清除都是与用户线程一起执行的,所以cms收集器是真正能能达到并发执行的。
缺点:
(1)cms垃圾收集器线程的数量:(cpu数量+3)/4,由此看来会导致cpu吞吐量降低(垃圾线程跟用户线程争cpu)
(2)基于标记-清除的算法容易产生内存碎片(如前面说的)
(3)在进行回收时,由于垃圾回收线程和用户线程是交替执行的,用户线程在执行时产生的垃圾只能在下一次GC时回收
G1收集器
(1)G1利用多cpu的环境,使回收线程与用户线程并发工作
(2)与cms不一样,G1采用的是“标记-整理”算法,避免了内存碎片的产生
(3)可以控制停顿时间
G1收集器将java堆的新生代和老年代分割成了好多个“region”
当新建的对象大小超过region大小的一半时,直接分配一个连续空间给它,并标记为H
回收大致分为四步:
(1)初始标记
(2)并发标记
(3)最终标记
(4)回收清除