目录
1.如何判断对象可以回收
1.1引用计数法
引用计数法指一个对象被其他对象所引用,那么就让该对象的计数加一,如果被引用了两次就将计数记为二,如果某一个变量不再引用该变量,那么改变量的计数减一,当该变量的计数为零时,则被视为可垃圾回收的对象。
弊端:当两个变量对象不断互相引用且没有其他变量对象进行调用时,该变量不能被垃圾回收(循环引用),最终造成内存泄漏。
1.2可达性分析算法
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
可达性分析算法:扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,则表示可以回收。
哪些对象可以作为GC Root?
工具https://www.eclipse.org/mat/
一个对象可以属于多个root,GC root有几下种:
- Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
- Thread - 活着的线程
- Native Stack - Java方法的本地变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Busy Monitor - 用于同步的监控对象
- Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。
使用该程序来测试那些对象为GC Root对象
使用该命令可以将该程序的内存快照抓取成一个dump文件
当清除list1之后,再抓取一遍
然后使用mat工具进行分析
查看文件1的GC Roots对象
1.3四种引用
1.3.1强引用
只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
1.3.2软引用
仅由软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身。
以上程序造成堆内存溢出。
与前者不同的是,软引用使用的是以软引用为载体,ArrayList对象的元素类型是软引用,而创建一个软引用对象去接收数据,最终添加到list里面。
原理
执行到第三次循环的时候,堆内存已满,触发垃圾回收机制,新生代被回收,此时可以执行第四次循环;
第四次循环,堆内存已满,新生代被回收,但还是没有解决堆内存溢出的问题,故触发了Full GC对老年代也进行回收,但也没有什么效果,此时为第二次垃圾回收,触发软引用的垃圾回收,回收了新生代,但没有什么效果,需要进行一次Full GC 对新生代和老年代进行回收。
以上程序可以将被垃圾回收的对象的软引用也移除,释放内存。
1.3.3弱引用
仅有弱引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
当进行到第三次循环之后,想要执行第四次循环,但由于弱引用也需要一定的内存,故不足第四次,此时需要执行一次垃圾回收,将新生代回收一遍可以进行第四次循环,但要进行第五次循环的时候,需要清除一个Byte对象,来防止内存的溢出,原因是弱引用在每一次垃圾回收的时候,都会对弱引用的对象进行清除。
当有十次循环的时候,弱引用的前九个对象都被清除,只保留最后一个弱引用对象,原因是弱引用本身也占内存。
1.3.4虚引用
必须配合引用队列,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,有Reference Handler线程调用虚引用相关方法释放直接内存。
1.3.5终结器引用
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象。
2.垃圾回收算法
2.1标记清除
原理:根据GC Root的引用链去判断堆对象中没有被GC Root引用的对象,然后进行标记,最后将标记对象占用内存的起始地址记录到一个空闲的地址列表,等待下个对象前往地址列表然后进行分配替换。
优点:清除速度快
缺点:会造成内存碎片
2.2标记整理
相比于标记清除算法,该算法需要对堆内存进行整理。
优点:没有内存碎片
缺点:速度慢
2.3复制
原理:复制一个空间大小相等的区域,然后先通过标记筛选出被GC Root引用的对象,然后迁移到新的区域,最后将原区域和新区域的地址交换。
优点:不会有内存碎片
缺点:需要占用双倍的内存空间
3.分代垃圾回收
新生代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。
年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。
*对象首先分配在伊甸园区域
*新生代空间不足时,触发minor GC,伊甸园和from存活的对象使用copy复制到to中(先根据可达性分析算法进行标记),存活的对象年龄加1并且交换from和to的内存地址
*minor GC会引发stop the wrold ,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
*当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
*当老年代空间不足时,会先尝试触发minor GC ,如果之后的空间仍然不足,那么触发full GC,stop the wrold的时间更长
3.1相关VM参数
3.2大对象![](https://i-blog.csdnimg.cn/blog_migrate/fd9061c7de79d9c6f4922dee4b663a91.png)
以上程序是增加一个8MB的对象到内存中,由于新生代的内存是10MB,使用的是UseSerialGC回收器,故伊甸园区域是8MB,from和to区各1MB,且在存在引用变量的内存,故新生代是不能够存放一个8MB的对象的,故直接提交到老年代中,老年代是10MB,当有两个8MB提交到内存时,Java虚拟机会先进行一次minor GC 和full GC 但结果还是会出现堆内存溢出。
4.垃圾回收器
1.串行
*单线程
*堆内存较小,适合个人电脑
2.吞吐量优先
*多线程
*堆内存较大,多核CPU
*让单位时间内,STW的事件最短
3.响应时间优先
*多线程
*堆内存较大,多核CPU
*尽可能让单词STW的时间最短
4.1串行垃圾回收器
在JDK1.3.1之前,单线程回收器是唯一的选择。它的单线程意义不仅仅是说它只会使用一个CPU或一个收集线程去完成垃圾收集工作。而且它进行垃圾回收的时候,必须暂停其他所有的工作线程(Stop The World,STW),直到它收集完成。它适合Client模式的应用,在单CPU环境下,它简单高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。
串行的垃圾收集器有两种,Serial与Serial Old,一般两者搭配使用。新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。Client应用或者命令行程序可以,通过-XX:+UseSerialGC可以开启上述回收模式。
4.2吞吐量优先垃圾回收器
-XX:UseParellelGC ~ -XX:+UseParallelOldGC
前者表示并行的垃圾回收器且工作在新生代使用的是复制算法,
后者也表示并行的垃圾回收器且公仔老年代使用的是标记整理算法。
工作模式:图中的四个CPU在运行时,需要进行一次垃圾回收,此时会在一个安全点将所有线程停止下来,之后会有与CPU核数相关的垃圾回收线程数来进行回收,最后恢复程序的运行。
-XX:ParallelGCThreads=n,表示手动的设置垃圾回收线程数。
-XX:+UseAdaptiveSizePolicy,表示自适应的新生代大小调整策略,例如:晋升阈值也会受到影响。
-XX:GCTimeRatio=ratio,表示调整吞吐量的参数,吞吐量 = 1/(1+ratio) = 代码运行时间/(代码运行时间+垃圾收集时间)
-XX:MaxGCPauseMillis=ms,表示最大停顿时间。
4.3响应时间优先垃圾回收器
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道其是给予标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:
初始标记:标记一下GC Roots能直接关联到的对象,会“Stop The World”。
并发标记:GC Roots Tracing,可以和用户线程并发执行。
重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
并发清除:清除对象,可以和用户线程并发执行。
工作流程:多个CPU并行运行,当老年代发生的内存不足的情况,多个线程都在安全点停止下来,CMS收集器会执行一个初始标记的动作并且会发送STW(时间很短),当初始标记完成的时候,到了下一个安全点用户线程恢复运行,与此同时CMS收集器执行并发标记,将剩余的垃圾进行标记,之后还要执行一个重新标记(防止并发标记中的错误而进行)并且会发生STW,最后进行并发清理。
由于在并发标记清理收集器的作用下,用户线程和CMS线程可以同时工作,故在CMS回收垃圾的时候,用户线程可能产生新的垃圾,此时需要预留一部分的内存给这些新的垃圾,即给浮动垃圾预留内存。
由于CMS收集器采用的是并发标记清理算法,故会产生内存碎片,当内存碎片到达一定程度,会造成新生代和老年代并发失败,此时CMS收集器会退化为SerialOld老年代的标记整理算法(此时的STW会很长),然后再恢复为CMS。
开启响应事件优先垃圾回收器需要配置一下虚拟机参数:
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
表示启动CMS垃圾回收器,同时会启动新生代垃圾回收器和老年代垃圾回收器。
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
表示设置并行线程数和并发线程数,并发线程数建议设置为并行线程数的四分之一 。
-XX:CMSInitiatingOccupancyFraction=parcent
表示执行CMS垃圾回收的内存占比,例如将parcent设置为80%,那么当内存达到80%进行一次垃圾回收,预留一点内存给浮动垃圾。
-XX:CMSScavengeBeforeRemark
表示CMS在重新标记之前对新生代进行一次垃圾回收,以提高性能。
4.4G1
定义:Garbage First
*2004论文发布
*2009jdk 6u14体验
*2012 jdk 7u4官方支持
*2017 jdk 9默认
使用场景
*同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
*超大堆内存,会将对划分为多个大小相等的Region
*整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
4.4.1G1垃圾回收阶段
年轻代GC(Young GC)
老年代并发标记过程(Concurrent Marking)
混合回收(Mixed GC)
过程:
Young Collection:当新生代的Eden区用尽时开始新生代回收过程,G1的新生代收集阶段是一个并行(多个垃圾线程)的独占式收集器。在新生代回收期,G1 GC暂停所用应用程序线程,启动多线程执行新生代回收。然后从新生代区间移动存活对象到Survivor区间或者老年代区间,也有可能是两个区间都涉及;
Young Collection + Concurrent Mark:
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程;
Mixed GC:
会对 E 、 S 、 O 进行全面垃圾回收最终标记( Remark )会 STW拷贝存活( Evacuation )会 STW- XX:MaxGCPauseMillis=ms
![](https://i-blog.csdnimg.cn/blog_migrate/3c7fb41971c4fb3930a2750e4bebf0c6.png)
标记完成马上开始 混合回收过程。对于一个混合回收期,G1 GC从老年代区间移动存活对象到空闲区间,这些空闲区间也就成为老年代的一部分,和新生代不同,老年代的G1回收期和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分的老年代的Region就可以了(选择一个回收价值高的老年代)。同时,这个老年代Region是和新生代一起被回收的。
4.4.2Full GC
1.SerialGC
*新生代内存不足发生的垃圾收集 -minor gc
*老年代内存不足发生的垃圾收集 -full gc
2.ParallelGC
*新生代内存不足发生的垃圾收集 -minor gc
*老年代内存不足发生的垃圾收集 -full gc
3.CMS
*新生代内存不足发生的垃圾收集 -minor gc
*老年代内存不足时,类似于G1的情况,当并发失败之后才会进行一次full gc。
4.G1
*新生代内存不足发生的垃圾收集 -minor gc
*老年代内存不足分为:
当堆内存达到一定值时(45%),那么在并发标记和混合收集的时候,垃圾回收的速度小于垃圾产生的速度,退化为串行垃圾回收,那么的就会发生一次full gc;当垃圾回收的速度大于垃圾产生的速度,那么会进行一次垃圾回收,但此垃圾回收并不是full gc
4.4.3Young Collection跨代引用
新生代垃圾回收需要先找到根对象,但根对象有一部分是来自于老年代,老年代的存活对象很多,遍历起来计算量很大,此时我们使用卡表的技术。
将老年代对象进行细分,以card为单位,一个card是512KB,如果一个card引用了新生代对象,那么我们就将该card标记为dirty(脏卡),此我们去寻找老年代根对象时,只需要关注脏卡。
卡表与Remembered Set
在新生代区中会有一个Remembered Set记录外部对该新生代对象的引用,当我们需要回收新生代对象时,我们可以通过Remember Set去老年代的卡表查找根对象。
在引用变更时通过 post-write barrier +dirty card queue
我们通过写屏障将对象引用发生变更的数据与卡表中的脏卡数据进行同步(异步操作),
由concurrent refinement threads 去更新Remembered Set。
4.4.4Remark
*pre-write barrier + satb_mark_queue
在重新标记时,将引用对象发生改变的对象使用写屏障并且放入重新标记队列,若该队列的对象被重新强引用,变为黑色根对象。
并发标记,垃圾回收线程标记的时候,可能用户线程也在使用这个对象,导致该对象的引用发生变化,例如:
原先是对象B强引用着对象C,当对象C的引用发生改变时,会发生写屏障,将对象C加入到队列中并且将对象C的状态该为待处理状态,此时发生重新标记,若队列中对象被根对象引用那么就改为强引用对象。
4.4.6JDK 8u20字符串去重
优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
开启字符串去重
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
new String的底层是使用字符数组
*将所有新分配的字符串放入一个队列
*当新生代回收时,G1并发检查是否有字符串重复
*如果它们值一样,让它们引用同一个char[]
*注意,这与String.intern()不一样:String.intern()关注的字符串对象,而字符串去重关注的是char[],在JVM内部,使用不同的字符串表
4.4.7JDK 8u40并发标记类卸载
在一些框架的类加载器被使用之后,很大概率不会再次被使用,此时我们可以将其的类加载器和类加载器的所有类进行卸载,所有对象都经过并发标记后,就能知道那些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark 默认开启
4.4.8JDK8u60回收巨型对象
*一个对象大于region的一半时,称之为巨型对象
*G1不会对巨型对象进行拷贝
*回收时被优先考虑
*G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的的巨型对象就可以在新生代垃圾回收时处理掉