java垃圾回收整理

如何判定对象为垃圾对象?有两个方法

一、引用计数法

在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器就+1,当引用失效的时候,计数器的值就-1

可以通过配置下面运行参数,查看垃圾回收的日志

-verbose:gc -XX:+PrintGCDetails

引用计数法速度快,简单灵活,但是存在缺陷,当堆中的对象相互引用的时候,有的对象会无法回收

                             

                            

当t1,t2置为null时,a,b被切断,t1和t2由于相互引用,引用计数都为1,所以不会被回收

运行结果

总结:

在对象头处维护一个counter,每增加一次对该对象的引用计数器自加,如果对该对象的引用失联,则计数器自减。当counter为0时,表明该对象已经被废弃,不处于存活状态。这种方式一方面无法区分软、虛、弱、强引用类别。另一方面,会造成死锁,假设两个对象相互引用始终无法释放counter,永远不能GC

 

 

 

二、可达性分析法(常用)

通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的。(出发后可不可以达到)

可以作为GCRoots的对象

虚拟机栈

方法区中类属性所引用的对象

方法区中常量所引用的对象

本地方法栈中引用的对象

                                       

 


 

如何回收?有哪些回收策略?

标记清除算法

顾名思义,标记-清除算法分为两个阶段,标记(mark)和清除(sweep).

在标记阶段,collector从mutator根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其标记为可达对象

而在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收

 

从上图我们可以看到,在Mark阶段,从根对象1可以访问到B对象,从B对象又可以访问到E对象,所以B,E对象都是可达的。同理,F,G,J,K也都是可达对象。到了Sweep阶段,所有非可达对象都会被collector回收。同时,Collector在进行标记和清除阶段时会将整个应用程序暂停(mutator),等待标记清除结束后才会恢复应用程序的运行,这也是Stop-The-World这个单词的来历。

 

缺点

标记-清除算法的比较大的缺点就是垃圾收集后有可能会造成大量的内存碎片,像上面的图片所示,垃圾收集后内存中存在三个内存碎片,假设一个方格代表1个单位的内存,如果有一个对象需要占用3个内存单位的话,那么就会导致Mutator一直处于暂停状态,而Collector一直在尝试进行垃圾收集,直到Out of Memory。

 

 

 

复制算法

解决标记清除算法效率的问题

一开始就将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着。当from域内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。

复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。

jvm将堆内存划分为新生代老年代,又将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区) ,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。

不过jvm在应用复制算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。一般的jvm都是8:1。也即是说,Eden区:From区:To区域的比例是8:1:1

始终有90%的空间是可以用来创建对象的,而剩下的10%用来存放回收后存活的对象。

1、当Eden区满的时候,会触发第一次young gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发young gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空

2、当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空

3、可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

注意:

  • 万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。

优点

在存活对象不多的情况下,性能高,能解决内存碎片和标记清除算法效率的问题。

缺点

  • 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;
  • 如果存活对象的数量比较大,复制算法的性能会变得很差。

 

标记整理算法

先标记,再整理,把它们集中起来再一起清除

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

优点
自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。

 

分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下。具体如下:
根据各个年代的特点采取最适当的收集算法

1、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
2、老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。


垃圾回收器有哪些?

Serial

  • 出现最早,历史最悠久的
  • 单线程垃圾收集器
  • 常用在客户端,桌面应用
  • 适合内存比较小的,单线程的程序

当开始垃圾回收时,其他线程暂停,垃圾回收后继续执行

 

Parnew

就是再Serial的基础上由单线程变为多线程处理

Parallel Scavenge

  • 复制算法(新生代收集器)
  • 多线程收集器
  • 达到可控制的吞吐量
    • 吞吐量:CPU用于运行代码的时间与CPU消耗的总时间的比值
    • 吞吐量 = (执行用户代码的时间)/ (执行用户代码的时间 + 垃圾回收占用的时间)
    •  例如:执行代码总用时100分钟,执行代码用99分钟,1分钟用来垃圾回收,99/99+1=99%,吞吐量为99%
  • -XX:MaxGCPauseMillis 垃圾收集器最大停顿时间
  • -XX:CGTimeRatio 吞吐量大小(0,100)

如图,如果设置最大停顿时间从100毫秒变成1毫秒,垃圾回收器为了满足能在1毫秒完成收集,肯定会减小划分的内存空间,来达到目的,这样做的话,垃圾回收的效率不见得会变高

高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;

当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;

例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;

Cms(Concurrent Mark Sweep)

老年代收集器

CMS(Concurrent Mark Sweep)收集器是一种以最短回收停顿时间为目标的收集器。目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,已给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

CMS收集器是基于“标记-清除”算法实现的,整个过程分为4步包括:

1、初始标记

2、并发标记

3、重新标记

4、并发清除

其中,初始标记、重新标记这两个步骤仍然需要其他线程暂停。初始标记仅仅只是标记一下GC roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长,但远比并发标记的时间短。由于整个过程耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从整体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。如下图

CMS是一款优秀的收集器,它的主要优点是并发收集、低停顿。但是还远达不到完美的成都,它的主要缺点如下:

1、CMS收集器对资源非常敏感。总的来说面向并发设计的程序都对CPU资源比较敏感。在并发阶段,他虽然不会导致应用程序线程停顿,但是会因为占用了一部分线程(或说CPU资源)而导致应用程序变慢,总吞吐量降低。

2、CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉他们,只好留待下一次GC在清理。这部分垃圾被称为浮动垃圾。

3、CMS收集器是基于“标记-清除”算法实现的收集器,着意味着收集结束时会有大量空间碎片产生。空间碎片多的情况可能会出现还有很大的空间但是无法找到足够大的连续空间来分配对象,不得不提前触发一次Full GC。为了解决这个问题CMS收集器会在顶不住了要进行Full GC时开启内存碎片合并整理过程,此过程无法并发,会导致停顿时间变长。

G1

集大成者,目前最好的垃圾收集器

由于篇幅较长:

直达链接 https://www.cnblogs.com/sidesky/p/10797382.html 感谢作者的详细论述

直达链接 https://www.cnblogs.com/aspirant/p/8663872.html 感谢作者的详细论述

G1 是什么?

  • G1 是一种面向服务端的垃圾收集器,应用在多核处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集器的暂停时间要求。
  • 像 CMS 收集器一样,能与应用程序线程并发执行, 整理空闲空间更快,需要更多的时间来预测 GC 停顿时间,不希望牺牲大量的吞吐性能,不需要更大的 JAVA Heap。
  • G1 收集器的设计目的是取代 CMS 收集器,同 CMS 相比,G1 垃圾收集器是一个有整理内存过程的垃圾收集器不会产生很多内存碎片。G1 的 Stop The World 更可控,G1 在停顿上添加了预测机制,用户可以指定期望的停顿时间
  • G1 是在 2012 年才在 jdk.1.7u4 中可以呀用,在 jdk9 中将 G1 变成默认垃圾收集器来代替 CMS。它是一款面向服务应用的收集器。主要改变是 Eden、Survivor 和 Tenured 等内存区域不再是连续的,而是变成了一个个大小一样的 region,每个 region 从 1M 到 32M 不等,一个 region 有可能属于 Eden、Survivor 或者 Tenured 内存区域。

特点:

  • G1 能充分利用多 CPU、多核环境硬件优势,尽量缩短 STW。
  • G1 整体采用标记-整理算法,局部是通过是通过复制算法,不会产生内存碎片
  • 宏观上看 G1 之中不再区分年轻代和老年代,被内存划分为多个独立的子区域。
  • G1 收集器里面讲整个的内存区域混合在一起,但其本身依然在小范围内要进行年轻代和老年代的区分。保留了新生代和老年代,但她们不在是物理隔离,而是一部分 Region 的集合且不需要 Region 是连续的,也就是说依然会采用不同的 GC 方式来处理不同的区域。
  • G1 虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的 Survivor to space 堆做复制准备。G1 只有逻辑上的分代概念,或者说每个分区都可能随 G1 的运行在不同代之间前后切换。

 

四步过程:

  • 初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短
  • 并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
  • 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
  • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

 

常用配置参数:

-XX:UseG1GC

-XX:G1HeapRegionSize=n:设置G1区域的大小,值是2的幂

-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

 

与CMS相比的优势:

  • 没有内存碎片
  • 可以精确控制停顿

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值