【六】垃圾回收
一、如何判定对象为垃圾对象?
1. 引用计数法
在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效时,计数器的值就-1。(但是目前GC没有用这种算法的)
判定是否有被回收,需要打印垃圾回收的日志信息。
package com.everjiankang.gc; public class Test { Object instance; public Test() { byte[] m = new byte[20 * 1024 * 1024]; } public static void main(String[] args) { Test t1 = new Test(); Test t2 = new Test(); t1.instance = t2; t2.instance = t1; t1 = null; t2 = null; System.out.println("hello world"); System.gc(); //手动进行垃圾回收 } }
虚拟机参数:-verbose:gc -XX:+PrintGCDetails
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java -verbose:gc -XX:+PrintGCDetails 。。。。。。 hello world //由下面红色数值可见,确实是被回收了,而若是使用引用计数法的话是不可能被回收的,因为t1和t2对象之间依然有引用,由此证明不是使用引用计数法 [GC (System.gc()) [PSYoungGen: 23142K->512K(38400K)] 43622K->21000K(125952K), 0.0011692 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 512K->0K(38400K)] [ParOldGen: 20488K->437K(87552K)] 21000K->437K(125952K), [Metaspace: 3307K->3307K(1056768K)], 0.0055045 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] Heap PSYoungGen total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000) eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000) from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000) to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000) ParOldGen total 87552K, used 437K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000) object space 87552K, 0% used [0x0000000740000000,0x000000074006d6e8,0x0000000745580000) Metaspace used 3313K, capacity 4496K, committed 4864K, reserved 1056768K class space used 365K, capacity 388K, committed 512K, reserved 1048576K Process finished with exit code 0
2. 可达性分析法
顺着栈中的GCRoot开始找,找不到的就被回收
哪些对方可以作为GCRoot进行向下查找呢?
1、虚拟机栈(栈帧中的局部变量表)
2、方法区的类属性所引用的对象
3、方法区中的常量所引用的对象
4、本地方法栈中引用的对象
现在主流的垃圾回收器所采用的判定垃圾对象的算法基本上都是可达性分析法。
将对象的引用置为空,
二、如何回收?
1. 回收策略
1)标记-清除法(效率不高)
标记:由可达性分析法 查找到的对象被标记
红色:正在使用的
黄色:被标记的可以清除的 图二被清除掉了
带来的问题:
(1)空间问题 : 内存区域会产生越来越多的不连续的内存空间
(2)效率问题 : 分配一个大对象的时候,由于不连续空间太多,寻址的过程效率低下。若是一直都找不到,会再次触发垃圾回收动作,然后再寻址。
2)复制算法(新生代)
用以解决标记-清除算法的效率问题
原理:
复制算法将内存分为2块区域,只在其中一块区域存储,另一块区域不用。图中红色的代表被标记要被回收。当回收时,会将所有没有标记回收依然要使用的对象,复制到另一块未使用的区域并按顺序排列,这样可以保证未使用的区域是连续空间。待将所有尚使用的对象复制完成后,再将之前的区域清空。
然后循环这个过程。
带来的问题:
内存区域只使用了一半,造成了内存区域极大的浪费。
解决办法:
将内存分成了3块区域
堆
(1)新生代
1))Eden区
2)Survivor存活区
3)Tenured Gen
(2) 老年代
新生代内存的垃圾回收
新生成的对象都会扔到Eden区,Eden区满了后会还会用其中的一个Suvivor区S0,垃圾回收发生时,Eden区的一般都会清空,存活的对象大概在10%左右,就都扔到S1区了,并顺序排列。且清掉s0和Eden区。下次创建对象的时候继续往Eden区存,当Eden区的内存达到一定阈值时,垃圾回收又开始工作了,会把Eden存活的对象继续往S1区域放,并且检测S1里的对象是否依然存活。如果都存活就可能进入Tenured Gen 区域。然后将S1区的对象在移到S0区。这样在使用的过程中,内存并不会浪费太多。仅仅是浪费了10%,这是可接受的。
如果Eden区经过垃圾回收,存活的对象占内存10%+,那么Survivor区域就放不下了,强行放会产生内存溢出,这时候怎么办呢?这时候就需要内存担保。就跟银行贷款一样,有个中间担保人,你若换不上,就找担保人要。 解决办法是扔到老年代里去。
3)标记-整理算法(老年代)
可以理解为:标记-整理-清除算法
上面的算法对新生代内存回收比较高效,但是对老年代内存回收效率则很低,因为老年代内的对象存活几率大, 可能有90%的存活,若是按照上面的方式回收,则每次都需要内存担保,反而会导致过程变慢。
让没有被定位垃圾对象的对象向内存的一端去移动,被标记为垃圾对象的对象移到另一端,这样就分开了,然后把所有的垃圾对象清除,且没有散列的空间,留下的是连续空间。
4)分代收集算法 = 标记-整理算法 + 复制算法
不同的代分别用不同的垃圾回收算法,新生代用复制算法,老年代用标记整理算法
2.垃圾回收器
1、Serial(新生代)
在1.3之前,唯一的一个回收新生代内存的垃圾回收器。最基本,发展最悠久的。单线程。桌面应用。
2、Parnew(新生代)
多线程收集器在一些比如客户端情况下,性能还是不如Serial的。
3、CMS(老年代)Concurrent Mark Sweep (标记清除)
在jvm1.5的时候,Sun公司发明了一个非常🐂的垃圾回收器:CMS,它是用于回收老年代内存的,使用CMS收集老年代内存的同时要想回收新生代内存,必须要用Serial是Parnew。
无法使用parelell回收新内存。
一般CMS + parnew 组合使用。至于其他的收集器,跟Serial大部分代码都是相同的,实现的原理都是复制算法原理。
了解CMS(Concurrent Mark-Sweep)垃圾回收器
工作过程:
初始标记
并发标记 1、标记就是收集
重新标记 2个动过并发执行
并发清理 2、收集后再清理
优点:
并发收集
低停顿
缺点:
占用大量CPU资源
无法处理浮动垃圾
出现Concurrent Model Failure
空间碎片
4、Parallel Scavenge(新生代)
[ˈpærəlel] [ˈskævɪndʒ]
并行的; vt.清除污物,打扫;
多线程收集器,复制算法。
和Parnew很像,但是最初设计的关注点就不一样。
设计目标:达到可控制的吞吐量。
-XX:MaxGCPauseMillis 垃圾收集器最大停顿时间(即上图中中间位置)
-XX:GCTimeRatio 吞吐量大小
(0,100)默认值是99,代表垃圾回收的时间比例只能占到1%,执行用户代码的时间是99%。
停顿时间越短,越适合于用户交互,有良好的响应速度,提升用户体验。
但是对于服务器来说,注重的的高并发,整体的性能,也就是所谓的吞吐量。所以在后台运算,不需要太多交互的情况下,Parallel更适合。
4、G1 (Garbage First)
G1赶说老二,没人敢说老大,jdk9 默认。集中了前面所讲的所有的垃圾收集器的优势。2004年的时候SUN公司实验室就发表了第一篇G1的论文,
优势:
1)并行并发
2)分代收集
3)空间的整合
4)可预测的停顿
如果是提高吞吐量的话,G1并不比GMS强到哪儿去,减少停顿的情况,G1比GMS强很多。
步骤
1)初始标记
2)并发标记
3)最终标记
4)筛选回收
特点
堆内存并没有特别清晰地分出新生代和老年代,没有一个非常强的空间划分,即使有这概念,也是逻辑上的。它是将内存空间分出一块儿一块的区域(Region),然后可能每个区域可能在逻辑上被分到新生代和老年代,而不是物理上把空间分成2块儿区域。
Remember Set
当虚拟机发现程序对引用类型进行写操作的时候,就会产生一个叫rihtByrry 暂时中断的写操作,检查引用对象是否处于不同的Region中,如果确实是在不同的Region中,那么对象处于哪个Region中就会被记录到Remember Set 中。然后当进行垃圾回收的时候,在GC根节点的枚举范围中,加入Remember Set ,就可以保证不全被扫描。
三、何时回收?