1.如何判断对象可以回收
1.1引用计数法
只要一个对象被其他对象所引用,就要让该对象的技术加1,某个对象不再引用其,则让它计数减1。当计数变为0时就可以作为垃圾被回收。
有一个弊端叫做循环引用,两个的引用计数都是1,导致不能作为垃圾回收,会造成内存泄露。
java虚拟机没有采用该算法。
1.2可达性分析算法
该算法需要先确定根对象,根对象的定义就是那些肯定不能当成垃圾被回收的对象。
在垃圾回收之前会先对堆中的所有对象进行扫描,看每一个对象是否被根对象直接或者间接的引用。是的话则该对象不能被回收,否则的话该对象可以被作为垃圾将来被回收。
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为 GC Root ?
使用eclipse提供的一个工具Memory Analyzer(MAT)
一个快速且多功能的java堆分析工具。
MAT MemoryAnalyzer中文使用指南_mat 汉化_Louis.No1的博客-CSDN博客
案例
先使用jps查看进程id,然后用jmap抓取进程快照,但是要使用MAT还要将快照转换成文件,使用下面命令
jmap -dump:format=b,live,file=1.bin 21384
format是指定转储文件的格式,b是二进制格式,live是抓取快照时只抓取存活类型,不管被回收的,并且live参数会在抓取前进行一次垃圾回收。file参数决定要把内存快照存为哪个文件。最后跟上进程id
在代码中抓取了两次,一次在置集合为空之前,一次在置集合为空之后。
置为空之前
有系统类
诸如Object,HashMap,String,等虚拟机运行过程中核心的类对象都可以作为GD root对象
第二类是java虚拟机在执行时调用的操作系统方法,操作系统执行时应用的java对象也是可以作为根对象。
第四类是Busy Monitor
synchronized锁住的对象,不会被当做垃圾
第三类就是活动线程中使用的一些对象
线程运行时产生的栈帧说产生的东西可以作为根对象
这里用到的局部变量所引用的对象都可以作为根对象。
引用变量和对象是两个东西 list1是一个引用,后面引用的对象是存在堆里面的,根对象也是指后面堆中的对象。而不是list1这个局部变量引用。
下图中的ArrayList 就是当前活动线程执行过程中局部变量所引用的对象可以作为根对象。
以及下面的那个方法参数引用的字符串数组对象String[0]也是根对象。
虚拟机栈中引用对象,方法区中类静态属性引用对象,方法区常量引用对象,本地方法栈引用对象
置为空之后
再次查找已经没有ArrayList了,因为list1置为空,ArrayList不再被引用,已经被回收了。
1.3四种引用
常见的应该有5种引用。
图中的实线箭头表示强应用,虚线这是软弱虚终结器引用。
没有使用xxxReference这个类包一层创建的实例对象,都是强引用类型的对象,因为后面会看到,非强引用对象的创建,是带有xxxReference类先包一层再创建的
我们平时学习的就是四种的第一级的强引用,说白了,就Object o = new Object(),四个引用分别是强引用、软引用、弱引用和虚引用,请记住它们的顺序!因为这是它们对内存的敏感程度!
等级: 强 > 软 > 弱 > 虚
1.强引用
Object o = new Object() new一个对象通过=复制给一个变量,则变量强引用了该对象。
特点:
沿着GCRoot的引用链可以去找到它就不会被垃圾回收。
在上面图中沿着C对象实线可以找到A1对象,则A1对象无法被回收。
当如下图所示没有GCRoot直接或者间接应用了A1时,A1才能被回收。
2.软引用
3.弱引用
上图的A2,A3两个对象,只要没有被直接的强引用所引用,当垃圾回收发生时都可能被回收。
上面图中A2,A3就是间接的被C对象间接的引用了,通过一个软引用对象和一个弱引用对象,这是间接的途径,并且又被B对象强引用了,这时不会被回收。
当B对象不再引用A2,A3时,A2,A3就可以被回收了。
A2回收时刻:当垃圾回收发生后,内存依旧不足时回收
A3回收时刻: 当垃圾回收发生时,不管内存充足与否,都会把弱引用的对象回收。
引用队列
软弱引用还可以配合一种叫做引用队列的一起工作。
当软引用的对象被回收后,软引用自身也是一个对象。如果创建时给它分配了一个引用队列,那么现在软引用会进入该队列。弱引用也是一样,会进入弱引用的引用队列。
原因: 软引用和弱引用自身都要占用一定的内存,如果要对它们的内存做进一步释放,需要使用应用队列将它们释放。
4.虚引用
与软弱引用不同,虚终应用必须配合引用队列使用。它们创建时就会关联一个引用队列。
工作: 在直接内存部分创建一个ByteBuffer实现类对象时就会创建一个名为Cleaner的虚引用对象。
ByteBuffer分配到一片直接内存之后会将直接内存地址传递给虚应用对象。
当ByteBuffer不再被强引用所引用后会被垃圾回收。
但是直接内存不归垃圾回收管,虚引用对象进入引用队列后,会定时由一个线程检查是否有虚引用入队,有的话会调用Cleaner中的freeMeory方法将直接内存释放。
5.终结器引用
所有的java对象都会继承自Object父类,都会有一个finallize()终结方法。
当这个A4对象重写了终结方法,并且没有被强引用所引用时就可以被当成垃圾回收。
在A4被回收前会先将终结器引用对象放入引用队列,再由一个优先级很低的线程在某些时刻过来检查并找到要作为垃圾回收的A4对象并且调用A4的finallize方法,调用完后的下一次垃圾回收就会回收掉A4.
由于线程优先级很低会导致A4对象迟迟无法被释放。不推荐使用finallize释放资源。
软引用------应用
上面代码是一个listh集合,然后不断往里面添加byte数组,用-Xmx20m设置了堆内存大小20m.
报错如下。
在业务场景中那4mb资源可能不是核心资源,当其过多时使用强引用来引用会到值内存溢出。
对于不重要的资源需要在内存紧张时将其占用资源释放,以后再用时再读取。
这里就要用到软引用。
在上面的这段代码中list不再直接引用byte数组,而是在中间加了个软引用对象。
现在list对SoftReference是强引用, SoftReference对byte是软引用。
这次就不会出现堆内存溢出。
在循环时调用软引用的get去取时还会有,等到循环结束后就已经取不到了 ,前四个元素都变成了null,只剩最后一个。
这里通过打印垃圾回收的详细参数演示。
-XX:+PrinyGCDetails -verbose:gc
在第三次循环时就已经出现内存不足发生了一次minor GC回收,回收了新生代,第四次之后又来了一次 minor GC,但是发生效率不高,4696k->4696k,于是来了一次Full GC,把老年代也回收了。
但是发现效率还是低,于是触发软引用的垃圾回收。
然后新生代4549k->0k,老年代12500k->639k.这里把前四个软引用的byte数组都回收了。
软引用------引用队列
在上面最后一次循环,前四个都是null了,已经没有保留的必要。
所以这里要把软引用本身也清理掉,配合引用队列使用。
这里的poll!=null是指这个软引用本身不为空,而不是引用的值不为空。
只有byte数组被回收的软引用才会进这个队列,所以只有4个需要被回收的软引用在这里。
最后再次循环就只剩一个在list里面了。
弱引用------应用
弱引用的使用和软引用类似。这里弱引用的byte对象在每次GC是都会被回收。
导致最后只剩四个byte数组。
加大循环次数,我们可以得知前三个对象没有被垃圾回收掉是因为晋升到了老年代。
加到6个对象时,第四个和第五个因为是新生代所以被回收了。
增大到10个时,最后一次直接回收了前面9个byte数组,因为弱引用本身也有内存,导致放不下引发了一次fullGC,去回收了老年代的byte数组。
因为年轻代已经放不下其他对象了,后续对象都是放到老年代。???
这里有点乱,后续再来看。
2.垃圾回收算法
2.1标记清除
第一个阶段是标记没有被根对象引用的对象。
第二阶段要将垃圾所占的空间释放,这里是将垃圾的起始结束地址放入一个空闲地址列表里面,下次分配新对象时会到空闲列表中找有没有一块足够的空间容纳新对象。这里和OS的内存管理很像。
优点:
速度快
缺点:
容易产生空间不连续的内存碎片。
2.2标记整理
标记过程和上面一样,但是第二个阶段会使用紧凑技术整理。
优点:
没有碎片
缺点:
移动过程效率低
2.3复制
将内存区划分成大小相同的两块区域,其中TO始终空闲。
第一个阶段也是标记,第二阶段如下图所示,将存活对象移动到TO区域并清理from区域
第三阶段交换from和to区域,是To总是保持空闲。
优点:
不会产生碎片
缺点:
会占用双倍内存空间。
3.分代垃圾回收
java虚拟机不会采用单独的一种算法,而是三种协同工作,具体实现就是分代的垃圾回收机制。
划分如下,有新生代和老年代。
老年代放的是长时间使用的对象。老年代的垃圾可以等到内存不足时调用fullGC清理。
新生代放的是用完就可以丢弃的对象。
永久代就是jvm进程在运行中永远不会删除的内存区域,也就是方法区。
针对生命周期不同采用不同的策略,老年代的垃圾回收很久才有一次,新生代的GC较为频繁。
分代垃圾回收机制工作
新创建的对象会被分配到伊甸园。
当伊甸园放不下后会出发一次垃圾回收叫做MinorGC,用可达性分析算法去标记。
标记完后会把存活对象放到幸存区TO中,并且让其寿命+1。伊甸园剩下的都会回收。
然后幸存区from和to就会交换位置。
然后放着放着伊甸园又满了,这时出发第二次垃圾回收。同时会去幸存区From找是否有需要回收的。
然后把伊甸园和From中存活的对象放入TO中再次加1,然后调换From和To.并把新对象放入伊甸园。
当幸存区的对象寿命超过预值后会晋升到老年代当中。
当出现下面的情况,一个新对象在新生代和From和老年代都放不下的时候会触发一次full GC.
fullGC会出发新生代和老年代的清理。
minor gc引发的 stop the world会把其他用户的线程都暂停,因为垃圾回收时会发生对象地址的改变,其他线程在根据原来的地址是找不到的。
新生代触发的STW时间较短。
老年代触发的STW时间更长。
- 如果full gc之后还是空间不足就会报内存溢出错误。
Minor GC 与 Full GC 的触发条件
Minor GC
Eden 区没有足够的空间分配给新创建的对象.
Full GC
- 老年代空间不足,这个很简单,就是字面上的不足,例如:大对象不停的直接进入老年代,最终造成空间不足。
- 方法区空间不足。
- Minor GC 引发 Full GC
年轻代的对象在经历Minor GC 过后,部分对象存活对象或全部存活对象会进入老年代。
3.1相关VM参数
幸存区比例默认是8,有10mb时,8mb是伊甸园,剩下的2mb是幸存区。
分析
参数中设置垃圾回收器为SerialGC,虚拟存储器比例不会动态调整。
运行后
new generation新生代 tenured generation老年代 Metaspace老年代
可以看见新生代里面有eden from to区域按照8:1:1
按照上面代码,往新生代放入7mb的byte数组,但是eden只有8mb,并且已经用了28%,则一定会触发垃圾回收。下面只显示GC表示是新生代的GC,Full GC则是老年代的GC.
如下所示,因为触发了垃圾回收,from区已经有东西了。
在7mb的基础上再一次放两个512kb,放一次没满,放两次就溢出了,所以又触发了一次垃圾回收。
并且这里很多对象晋升到了老年代。因为内存紧张所以没到15次就让一部分对象及晋升到老年代,这里直接把7mb的放入了老年代。
大对象_oom
这里直接一个大对象超过了新生代的容量,不会触发垃圾回收,会直接进入老年代。
再放一个8mb的大对象,这次没有地方能放得下了,就会直接内存溢出。
再内存溢出前还会尝试进行垃圾回收 ,先是minor GC不行,然后Full GC也不行就直接报错了。
误区:
在一个线程中内存溢出了并不会导致整个线程结束。
当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,但是这里释放不了,老年代里的8mb字节对象被list引用着。
子线程里放入大对象时,发现继续放入会造成内存泄漏,所以最后这个8mb对象就没有放入,但还是会给程序反馈报异常。主线程依然可以使用堆内存。
4.垃圾回收器
后两种需要多核CPU才能充分发挥性能。区别在,响应时间优先是让每次时间最短,发生5次,每次0.1,。吞吐量是单位时间内最短,一个小时发生两次,每次0.2,最终单位时间内0.4秒,少于0.5,单位时间内垃圾回收时间占比越低,吞吐量占比越高。
4.1串行
Serial运行在新生代,使用复制算法,SerialOld生活在老年代,使用标记+整理算法。
如上图所示多核CPU在运行时发生内存不足了,要让所有线程在一个安全点停下。
因为Serial和SerialOld都是单线程的垃圾回收器,所以只有一个线程在进行垃圾回收。
4.2吞吐量优先
在jdk1.8中,默认使用的就是ParallelGC,并行的垃圾回收器,后面那个加了个Old是老年代用的,回收算法上和串行的使用一样。开启其中一个另一个也会自动开启。
这里会开启多个垃圾回收线程进行垃圾回收,默认与CPU核数相关。
每次发生垃圾回收时CPU占用率如下所示,因为是动用了所有核去进行垃圾回收。
这里垃圾回收线程数可以通过参数设置修改
相关参数
采用自适应的大小调整策略,调整新生代中的大小,伊甸园和幸存区的比例,晋升预值等。
可以根据设定目标尝试调整堆的大小来达到期望目标。
1是吊证吞吐量的目标,垃圾回收时间和总时间的占比。公式:1/(1+ratio).ratio默认值是99,有
1/(100)= 0.01,则说的是垃圾回收时间不能超过总时间的百分之一。超过的话一般会把堆的大小增大,使垃圾回收发生次数减少,使的总时间下降。
2是最大暂停毫秒数,默认是200mx,2和1冲突,1会增大垃圾回收的总时间,使其可能超过200ms。而且2会将堆的大小减小,防止因为堆过大导致垃圾回收时间过长。
4.3 响应时间优先(CMS)
要开启该垃圾回收器,虚拟机参数为UseConcMarkSweepGC
Conc=Concurrent并发 Mark标记 Sweep清除。
基于标记清除算法的并发垃圾回收器。垃圾回收器在工作的同时其他用户线程也能进行,在垃圾回收的部分阶段不需要Stop the World. 这是工作在老年代的垃圾回收器。
与其对应的是ParNewGC,工作在新生代的垃圾回收器。
有时CMS并发失败的时候会切换到SerialOld垃圾回收器。
如上图所示,运行过程中老年代发生内存不足,CMS开始执行初始标记(标记根对象,暂停时间短)的工作,需要stop the world并会阻塞用户线程。
并发标记阶段用户线程恢复运行,并且CMS可以把剩余的垃圾找出来.
重新标记阶段需要stop the world,因为并发标记阶段用户线程可能会产生新对象,改变对象的引用,对垃圾回收有干扰。
最后并发清理阶段就又可以并发运行了。
细节:
CMS受到两个参数影响,一个是并行的垃圾回收线程数,第二个是并发的垃圾线程数,一般要设置为并行线程总数的四分之一。并行是4,并发是1时,有4个CPU会工作,有一个用来执行垃圾回收线程。
CMS对CPU的占用没有上面的高,因为这里只有四分之一的CPU用去垃圾回收,但用户工作线程只能占用原本3/4的线程数量,对应用程序吞吐量有影响。
在其他用户线程运行的时候其他用户线程可能会产生新的垃圾,称为"浮动垃圾",这些要等到下一次才能回收。要预留空间保存垃圾,上面的参数就是用于控制什么时候执行CMS垃圾回收。
等于80时,只要老年代的内存占用达到80%时就进行垃圾回收。
用于在重新标记阶段的特殊场景:
新生代的对象引用老年代的对象,在重新标记阶段做可达性分析时对性能影响很大。
原因:
新生代的对象创建个数较多,并且很多都是要作为垃圾的,就算去找老年代也是要被回收,多做了无用的查找功。
用上面的参数在重新标记前对新生代用UseParNewGC做垃圾回收,可以减少存活对象,减轻重新标记阶段的压力。
特点:
CMS采用标记清除算法,可能产生较多的内存碎片。导致将来分配对象时新生代内存不足,结果老年代也不足,造成并发失败。使得垃圾回收器退化为SerialOld,做一次整理,使得碎片少了才能继续工作。
一旦有垃圾回收失败的问题,垃圾回收时间也会大幅上升。
未完待续........