java垃圾回收

java垃圾回收

​ 参考:https://blog.csdn.net/yrwan95/article/details/82829186

​ Java程序在运行过程中会产生大量的对象,但是内存大小是有限的,如果光用而不释放,那内存迟早被耗尽。如C、C++程序,需要程序猿手动释放内存,Java则不需要,是由垃圾回收器去自动回收。

​ 垃圾回收器回收内存至少需要做两件事情:标记垃圾、回收垃圾。

垃圾判断算法

​ 即判断JVM中的所有对象,哪些对象是存活的,哪些对象可回收的算法

引用计数算法

最简单的垃圾判断算法。

​ 在对象中添加一个属性用于标记对象被引用的次数,每多一个其他对象引用,计数+1,当引用失效时,计数-1,如果计数=0,表示没有其他对象引用,就可以被回收。这个算法无法解决循环依赖的问题。

image.png

可达性分析算法

​ 通过一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系链向下搜索,如果某个对象无法被搜索到,则说明该对象无引用执行,可回收。相反,则对象处于存活状态,不可回收。说白了,能做为GC Root的对象,目前肯定是要被使用的,所以要根据这些被使用的对象去找他们所引用的对象,这些被引用的对象肯定要是要使用的,那么没有被这些GC ROOT引用的对象的调用链找到,就是要被回收的对象。 常见的GC Root有如下:

  1. 虚拟机栈中局部变量表引用的对象
  2. 静态属性引用的对象
  3. JNI中引用的对象
  4. 所有被同步锁(synchronized关键字)持有的对象。

垃圾回收算法

标记-清除算法

​ 如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象

​ 缺点有两个:

  1. 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过
    程的执行效率都随对象数量增长而降低;
  2. 第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

img

标记-复制算法

​ 为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,有一种“半区复制”的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

img

​ 这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。这种算法主要是针对新生代的垃圾回收

IBM公司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。`

​ 所以目前虚拟机的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1 : 1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。

标记-整理算法

​ 针对老年代对象的存亡特征,提出了另外一种有针对性的“标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

img

优点是自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。缺点是回收速度很慢,所以jvm调优就是减少老年代的垃圾回收。

三色标记与读写屏障

把遍历对象过程中遇到的对象,按照“是否访问过”这个条件标记成三种颜色:

  • 白色:尚未访问过。
  • 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
  • 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。

如果是串行或者并行垃圾收集器,三色标记没有任何问题。但是如果是并发垃圾回收器,就会出现问题。因为用户线程和gc线程同时工作,那么再标记阶段会出现两种问题:多标、少标、漏标

  1. 多标 浮动垃圾

    GC线程已经标记了B,此时用户代码中A断开了对B的引用,但此时B已经被标记成了灰色,本轮GC不会被回收,这就是所谓的多标,多标的对象即成为浮动垃圾,躲过了本次GC。多标对程序逻辑是没有影响的,唯一的影响是该回收的对象躲过了一次GC,造成了些许的内存浪费。

    image.png

    2.少标 浮动垃圾

    ​ 并发标记开始后创建的对象,都视为黑色,本轮GC不清除。

    ​ 这里面有的对象用完就变成垃圾了,就可以销毁了,这部分对象即少标环境中的浮动垃圾。

    image.png

    3.漏标 程序出错

    ​ 漏标是如何产生的呢?GC把B标记完,准备标记B引用的对象,这时用户线程执行代码,代码中断开了B对D的引用,改为A对D的引用。但是A已经被标记成黑色,不会再次扫描A,而D还是白色,执行垃圾回收逻辑的时候D会被回收,程序就会出错了。

    image.png
    代码示例:

    public class Test {
    
    public static void main(String[] args) {
        A a = new A();
        //初始标记 STW
        D d = a.b.d;//并发标记 此时还没标记到B对象
        a.b.d = null;//并发标记 a对象标记完了,置为黑色。标记B对象,发现D引用是空的,d为白色
        a.d = d;//a引用了d,表示黑色引用了白色,则D不会被标记到了
    
        }
    }
    
    class A {
        B b = new B();
        D d = null;
    }
    
    class B{
        D d = new D();
    }
    
    class C{
    
    }
    
    
    class D{
    
    }
    	
    
    如何解决漏标问题

    ​ 先分析下漏标问题是如何产生的:

    ​ 条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。

    ​ 条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。

    ​ 知道了问题所在就知道如何解决了

    ​ 将漏标的的对象放入一个集合当中,并发标记完再去重新标记。会stw

    1、读屏障 + 重新标记
    读屏障:读取成员变量前后做一些操作
    

    ​ 在建立A对D的引用时将D作为白色或灰色对象记录下来,并发标记结束后STW,然后重新标记由D类似的对象组成的集合。

    ​ 重新标记环节一定要STW,不然标记就没完没了了。

    2、写屏障 + 增量更新(IU)
    写屏障:在给对象成员变量赋值的前后做一些操作
    

    ​ 这种方式解决的是条件二,即通过写屏障记录下更新,具体做法如下:

    ​ 对象A对D的引用关系建立时,将A加入带扫描的集合中,A对象置为灰色,在重新标记阶段重新扫描A

    3、写屏障 + 原始快照(SATB)

    ​ 这种方式解决的是条件一,带来的结果是依然能够标记到D,具体做法如下:

    ​ 当B断开对D的引用时,将D放入到待扫描的集合中,D对象置为灰色,在重新标记阶段重新扫描D。此方式可能造成浮动垃圾,因为D不一定会被别的对象引用。

    4、实际应用

    ​ CMS:写屏障 + 增量更新

    ​ G1:写屏障 + SATB

垃圾收集器

​ HotSpot虚拟机中的7种垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

​ jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

​ jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

​ jdk1.9 默认垃圾收集器G1

​ java -XX:+PrintCommandLineFlags -version参数可查看默认设置收集器类型

​ -XX:+PrintGCDetails 亦可通过打印的GC日志的新生代、老年代名称判断

​ 垃圾收集器中的串行、并行、并发

​ 串行:一个GC线程运行

​ 并行:多个GC线程同时运行

​ 并发:多个GC线程与多个用户线程同时运行

Serial收集器

​ 是一款串行垃圾回收器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。它进行垃圾收集时,必须暂停其他所有工作线程(STW,Stop The World),直到它收集结束。STW是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。采用的是标记-复制算法

​ Serial/Serial Old收集器运行示意图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W0JdKa9D-1599443165069)(C:%5CUsers%5CA%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200830164355044.png)]

​ 虽然这款垃圾收集器缺点很明显,但是在HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的。

ParNew收集器

​ ParNew收集器实质上是Serial收集器的多线程并行版本,也是对新生代内存的回收。采用的是标记-复制算法

​ 相关参数:

​ -XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器

​ -XX:+UseParNewGC:强制指定使用ParNew

​ -XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同

​ ParNew/Serial Old收集器运行示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lafHo5Nq-1599443165070)(C:%5CUsers%5CA%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200830164745529.png)]

​ ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge收集器

​ Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。

​ Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。 吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)

相关参数:

  • -XX:MaxGCPauseMillis:是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
  • -XX:GCTimeRatio:一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。
  • -XX:+UseAdaptiveSizePolicy:一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

​ Parallel Scavenge/Parallel Old收集器运行示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQuBDzJL-1599443165071)(C:%5CUsers%5CA%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200831155225275.png)]

Serial Old收集器

​ Serial收集器的老年代版本。基于标记-整理算法实现,

​ 有两个用途:

​ 1、与Serial收集器、Parallel收集器搭配使用

​ 2、作为CMS收集器的后备方案

Parallel Old收集器

​ Parallel收集器的老年代版本。基于标记-整理算法实现。

CMS收集器

​ 聚焦低延迟。

​ 针对老年代的回收,基于标记-清除算法实现。CMS收集器是并发收集器,即在运行阶段用户线程依然在运行,会产生对象,所以CMS收集器不能等到老年代满了才触发,而是要提前触发,这个阈值是92%。这个阈值可以通过参数-XX:CMSInitiatingOccupancyFraction设置

​ Concurrent Mark Sweep收集器运行示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pPvwktoo-1599443165071)(C:%5CUsers%5CA%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200905172846566.png)]

CMS收集器工作分四个步骤:

​ 1、初始标记 会STW。只标记GC Roots直接关联的对象。

​ 2、并发标记 不会STW。GC线程与用户线程并发运行。会沿着GC Roots直接关联的对象链遍历整个对象图。可想而知需要的时间较长,但因为是与用户线程并发运行的,除了能感知到CPU飙升,不会出现卡顿现象。

​ 3、重新标记 会STW。CMS垃圾收集器通过写屏障+增量更新记录了并发标记阶段新建立的引用关系,重新标记就是去遍历这个记录。

​ 4、并发清除 GC线程与用户线程并发运行,清理未被标记到的对象

G1收集器

​ 暂不讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java垃圾回收算法主要基于可达性分析和标记-清除两种算法。下面是对这两种算法的简要说明: 1. 可达性分析 (Reachability Analysis):这是Java垃圾回收的基础算法。它通过判断对象是否可以从根对象(如线程栈、静态变量等)访问到来确定对象的存活状态。如果一个对象不可达,则认为它是垃圾,可以被回收。 2. 标记-清除 (Mark and Sweep):这是最基本的垃圾回收算法之一。在标记阶段,垃圾回收器从根对象开始遍历所有可达对象,并将其标记为“存活”。在清除阶段,垃圾回收器清除所有未被标记的对象,并回收它们所占用的内存空间。 除了标记-清除算法,Java还使用了其他一些高级的垃圾回收算法,包括: 1. 复制算法 (Copying Algorithm):将堆内存分为两个区域,每次只使用其中一个区域。当一个区域满了之后,将存活的对象复制到另一个区域中,并清除当前区域中的所有对象。 2. 标记-整理 (Mark and Compact):类似于标记-清除算法,但在清除阶段之后,它会将存活的对象移动到内存的一端,以便于分配连续的内存空间。 3. 分代算法 (Generational Algorithm):根据对象的存活时间将堆内存划分为不同的代。通常情况下,新创建的对象会被分配到年轻代,而存活时间较长的对象则会被转移到老年代。不同代使用不同的垃圾回收算法进行回收。 这些算法的选择取决于具体的应用场景和性能需求,Java垃圾回收器通常会根据当前堆内存的使用情况和对象的存活特性来选择合适的回收算法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值