JAVA垃圾收集

1.如何判断对象死亡

说道垃圾回收,那么首要问题就是jvm如何判断一个对象已经死亡呢

1.1 引用计数法

说白了,就是为每个对象设立一个引用计数器,每当有一个引用指向它,计数器加一,引用失效或是转移则减一,很容易想到,当计数器为0时认为该对象死亡。

但就是这样一个原理简单、判定效率高的算法却没有在主流的java虚拟机中得到使用,原因是他存在一个致命的问题——循环引用

即当两个"死亡"的对象互相引用着对方时,出现了类似"死锁"的情况,死亡的对象当然不会对他的指针进行修改,所以这两个对象会一直占用内存,造成内存泄漏。

![img]

1.2可达性分析

可达性分析算法是当前广为接受的算法,该算法采用了一系列称为"GC Roots"的根对象作为起始节点集,从该节点集开始向下搜索,形成一个"存活链",所有在存活链中的对象即为存活对象,否则判定为死亡

在Java中,可作为根对象的对象有以下几种:

  • 虚拟机栈中直接引用的对象(如线程方法栈中的参数、局部变量等)
  • 方法区中类静态属性引用的对象、常量引用的对象
  • 本地方法栈中JNI(native方法)引用的对象
  • 虚拟机内部的引用对象(如基本数据类型包装类等)
  • 被同步锁持有的对象
  • 等等

对象在第一次标记死亡后,并不会立即被回收,而是有一个自救的机会,即为java中的"析构函数"finalize(),可以在重写该方法中重新为该对象加上引用,但一般虚拟机等待的时间有限,即在二次标记时该对象还未完成自救,则立即回收该对象,所以这个自救是有概率的。

1.3 java的四大引用类型

传统的对象引用只单单指地址引用关系,对于绝大多数对象引用足矣,但对于某种"可有可无"对象的引用关系描述则显得乏力,所以在jdk1.2之后有了四大引用类型,分别是:

  • 强引用,最古老的引用方式,像String a = new String("abcd")就是一个强引用例子,对强引用的回收遵从判定死亡算法
  • 软引用,描述一些可以有,但不是必要的对象,用SoftReference类实现。系统在发生OOM异常之前,会首先将软引用的对象纳入回收对象中。
  • 弱引用,描述一些非必须对象,用WeakReference类实现,弱引用的对象会在下一次垃圾回收中被回收
  • 虚引用,最"没用"的引用,虚引用不会对对象的生存时间造成任何影响,也不能通过虚引用调用对象,他只是为了使回收该对象时系统给出一个通知

2.垃圾收集算法

2.1标记清除算法

最简单的回收算法,过程分为"标记"和"清除",顾名思义,先标记死亡的对象,在统一回收

缺点:1.标记清除的效率会随着对象的增多而骤减

​ 2.产生大量内存碎片,如下图

2.2标记复制算法

将内存空间分为两个子块,每次只使用其中一个子块,再一次垃圾回收中,将新分配的对象和上一次GC存活的对象统一移向另一个子块,然后对死亡对象进行回收。

缺点:1.存活对象的复制需要大量开销

​ 2.每次只使用一半空间,造成大量空间浪费![img]

2.3 标记整理算法

一种基于标记清除算法的改进算法,每次标记后,让存活的对象统一向内存的一端移动,然后再进行回收

2.4分代收集算法

一种集成了以上算法的收集理论,基于强分代假说(绝大多数对象都是朝生夕灭的)和弱分代假说(熬过越多次标记的对象就越难消亡)之上

分代收集顾名思义将内存区域划分为多个不同的区域:

2.4.1新生代

绝大多数对象的分配和回收都发生在新生代,所以新生代采用的是标记复制算法,在此基础上,新生代又分为Eden区和两个Survivor区(from区和to区),每次内存分配都发生在eden区,少部分情况也分配在from区.在发生一次内存回收时,将from区存放的对象(上次gc存活下来的对象)和eden区存活下来的对象移向to区。

2.4.2老年代

经历15次移动的对象将会被移动到老年代,老年代基本都是些存活时间比较久的对象,所以在老年代采用的是标记整理算法。

为什么说基本呢,这里存在一个叫做分配担保的问题,如果即将要分配的对象大于eden和from区的剩余空间的话,这些对象便通过分配担保直接分配到老年代。

2.4.3永久代

JDK1.8之后改为元空间,元空间物理上不在JVM堆内存中,而在计算机内存中,方法区便在其中

3.java垃圾回收的算法细节(以HotSpot虚拟机为例)

3.1根节点枚举

随着虚拟机技术的不断发展,查找存活链的过程已经可以和用户线程并发了,但根节点枚举必须保证在一个能保障一致性的快照中进行,即暂时性的暂停用户进程。

但根节点的扫描需要扫描所有的对象引用,并计算他们的类型,这就需要大量的时间,导致用户进程的"Stop The World"。但事实果真如此吗,其实不然,虚拟机自然有方法知道引用中的对象类型。在hotspot虚拟机中,是一组被称之为OopMap的数据结构,一旦类加载完成后,hotspot就会把对象的引用中的数据类型计算出来,并在某些特定的位置记录下哪些位置存放着引用。这样根节点枚举时只需要去扫描这些引用的位置就可以。

3.2 安全点

如果每个需要修改引用关系的指令都要为其维护OopMap,无疑是一个巨大的开销,所以前面说过,虚拟机只是在某些特定的位置记录下引用信息,而这些特定的位置被称之为安全点。同时也规定,只有当所有正在运行的用户线程进行到安全点时,才能进行垃圾回收,这也需要安全点的选取一般是方法调用、循环等这些序列复用的长时间指令。

同时也迎来了一个问题,如何保证进行垃圾回收时,所有线程都运行到了安全点呢,有两种方案:

  • 抢占式中断

    由系统控制,在需要垃圾回收时先中断所有进程,如果发现有进程没有到达安全点,则让它继续运行一会,直到安全点。

  • 主动式中断

    当系统需要垃圾回收时,将一个标志位置为真。每个用户线程都需要不断地去轮询这个标志位,如果标志位为真,则运行到最近的安全点挂起。

3.3 安全区域

前面讲到,通过线程与虚拟机之间的交互保证到达安全点,那么如果该线程正处于阻塞状态或者睡眠状态呢,在安全点的基础上,虚拟机又针对这种情况设置了一个安全区域(在安全区域内的指令不会修改引用关系)

  • 当一个线程运行安全区域内的代码时,他标注自己进入了安全区,这样即使它被阻塞,依然可以进行垃圾回收。
  • 当一个线程即将离开安全区域时,他先确认是不是完成了根节点枚举,如果没有完成,则等待。
3.4 记忆集与卡表

分代收集理论带来了一个很现实的问题,就是跨代引用(例如一个老年代的对象指向了一个新生代对象),为了考虑这种情况,每次新生代的标记过程都要扫描一边老年代,又是一个大开销,为了解决这个问题,在新生代设立了一种数据结构——记忆集(记录由非收集区指向收集区的引用)。

卡表就是记忆集的一种实现方式,他将老年代分为一个一个的内存块,并标注了哪些内存块存在跨代引用,这样在标记时就只需要扫描这些内存块就可以。

3.5 写屏障

卡表有效地解决了跨代引用的问题,那么卡表应该如何维护的,如何保证在引用关系更新后能立即更新卡表。虚拟机引入了一个机器码层面的控制手段——aop切面,将类型赋值的指令视作切入点,将维护卡表的操作作为aop增量服务即可有效维护卡表。

3.6 并发的可达性分析

为了方便理解,这里引入一个三色标记算法来模拟查找存活链的过程

  • 白色:未被收集器访问过的对象
  • 黑色:自身被扫描且所有子引用被扫描
  • 灰色:自身被扫描,但至少有一个未被扫描的子引用

则很容易想象到该过程是一个以灰色对象为波峰的蔓延,如下图:

但是如果在并发的标记过程中,用户线程修改了某些引用关系,则会出现两种情况:

  • 将原本存活的对象标记为死亡(致命的错误):将对象已经标记为死亡,用户线程加了引用,复活
  • 将原本死亡的对象标记为存活:将对象已经标记为存活(黑色节点),用户线程删除了所以对它的引用,对象死亡

第二种情况对程序难以造成影响,所以一般都选择处理第一种情况,又称为"对象消失",以三色标记算法举例,对象消失当且仅当以下两个情况同时满足时发生:

  • 用户线程插入了一条或多条从黑色对象到白色对象的引用

    解决办法:增量更新

    黑色对象如果有插入到白色对象的引用,则将其记录下来,等待并发扫描结束后,再重新扫描这些黑色对象

  • 用户线程删除了所有由灰色对象到该白色对象的直接饮用或间接引用

    解决办法:原始快照

    当灰色对象要删除指向白色对象的引用时,就将该引用关系记录下来,等并发扫描结束后再将这些灰色对象重新扫描一次

4.经典的垃圾收集器

新生代收集器

4.1 Serial收集器
  • 进行垃圾收集时,必须暂停替他所有用户线程,基于标记复制算法
  • 占用额外内存最少,单线程收集效率最高的收集器
  • 对运行在客户端模式下的虚拟机是个非常好的选择
4.2 ParNew收集器
  • 与Serial收集器相似,但是支持多线程的垃圾收集,基于标记复制算法
  • ParNew和Serial是唯二可以与CMS搭配使用的收集器
4.3 Parallel Scavenge 收集器
  • 支持多线程垃圾收集,基于标记复制算法
  • 提供可控制的吞吐量、自适应吞吐量调节

老年代收集器

4.4 Serial Old收集器
  • 顾名思义,Serial的老年代版本,单线程收集,基于标记整理算法
4.5 Parallel Old收集器
  • Parallel的老年代版本,多线程并发收集,基于标记整理算法
  • 可与Parallel组成吞吐量优先的收集器组合
4.6 CMS(Concurrent Mark Sweep并发标记清除)收集器
  • 以获取最短回收停顿时间为目标,基于标记清除

  • 步骤:

    初始标记:stw(stop the world)标记Gc Roots和其关联对象

    并发标记:查找存活链,可与用户线程并发

    重新标记:stw,增量更新实现

    并发清除:回收死亡对象

  • 与用户线程的并发导致用户线程变慢,为此提供了一个i-CMS变种,使并发变为并行(到jdk9被废弃)

  • 为了提供并发的内存空间,CMS在老年代溢出之前就会进行一次FullGC,一般会有一个阈值,如果预留的空间无法满足并发需要,则会产生一次stw的垃圾回收

  • 标记清除会产生大量内存碎片,可用虚拟机参数调节

整堆收集器

4.7 G1(Garbage First)收集器
  • 一款面向服务端的收集器,开创了后几期面向局部收集和基于Region的内存布局
  • 将内存布局成数个大小相等相互独立的区域(Region),每个region都可以扮演eden、survivor等空间
  • 对于那些大小大于region区的对象,则由几个相连的Humongous Region存储
  • Garbage First,计算每个Region区的垃圾回收效益,维护一个优先级列表,每次根据用户指定的回收时间(停顿时间模型),选择优先级最高的region区回收
  • 双向卡表解决跨Region引用、原始快照解决对象消失、写前屏障跟踪原始快照
  • G1的垃圾回收过程涉及存活对象的移动,所以要stw

Java基础篇——垃圾收集详解的更多相关文章

  1. java基础篇---枚举详解

    在JDK1.5之前,JAVA可以有两种方式定义新类型:类和接口,对于大部分面向对象编程,有这两种似乎就足够了,但是在一些特殊情况就不合适.例如:想要定义一个Color类,它只能有Red,Green,B ...

  2. Java基础之 数组详解

    前言:Java内功心法之数组详解,看完这篇你向Java大神的路上又迈出了一步(有什么问题或者需要资料可以联系我的扣扣:734999078) 数组概念 同一种类型数据的集合.其实数组就是一个容器. 数组 ...

  3. java提高篇(十)-----详解匿名内部类

    在java提高篇-----详解内部类中对匿名内部类做了一个简单的介绍,但是内部类还存在很多其他细节问题,所以就衍生出这篇博客.在这篇博客中你可以了解到匿名内部类的使用.匿名内部类要注意的事项.如何初始 ...

  4. java提高篇(八)----详解内部类

    可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 第一次见面 内部类我们从外面 ...

  5. java基础之:详解内部类(转载)

    可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 第一次见面 内部类我们从外面 ...

  6. 【转】java提高篇(十)-----详解匿名内部类

    原文网址:http://www.cnblogs.com/chenssy/p/3390871.html 在java提高篇-----详解内部类中对匿名内部类做了一个简单的介绍,但是内部类还存在很多其他细节 ...

  7. java提高篇之详解内部类

    可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 第一次见面 内部类我们从外面 ...

  8. java提高篇(十)-----详解匿名内部类 ,形参为什么要用final

    在java提高篇-----详解内部类中对匿名内部类做了一个简单的介绍,但是内部类还存在很多其他细节问题,所以就衍生出这篇博客.在这篇博客中你可以了解到匿名内部类的使用.匿名内部类要注意的事项.如何初始 ...

  9. 基础篇:详解JAVA对象实例化过程

    目录 1 对象的实例化过程 2 类的加载过程 3 触发类加载的条件 4 对象的实例化过程 5 类加载器和双亲委派规则,如何打破双亲委派规则 欢迎指正文中错误 关注公众号,一起交流 参考文章 1 对象的 ...

  10. Java基础知识面试题详解(2019年)

    文章目录 1. 面向对象和面向过程的区别 2. Java 语言有哪些特点? 3. 关于 JVM JDK 和 JRE 最详细通俗的解答 JVM JDK 和 JRE 4. Oracle JDK 和 Ope ...

随机推荐

  1. 在mybatis中#{}和${}的区别

    文章目录 1.第一个#{} 2.第二个${} 3.区别 1.第一个#{} 解释: 使用#{}格式的语法在mybatis中使用preparement语句来安全的设置值 PreparedStatement ...

  2. 深入浅出redis缓存应用

    0.1.索引 https://blog.waterflow.link/articles/1663169309611 1.只读缓存 只读缓存的流程是这样的: 当查询请求过来时,先从redis中查询数据, ...

  3. 21.drf视图系统组成及继承关系

    APIView REST framework提供了一个 APIView 类,它是Django的 View 类的子类. APIView 类和Django原生的类视图的 View 类有以下不同: 传入的请 ...

  4. 论文笔记 - RETRIEVE: Coreset Selection for Efficient and Robust Semi-Supervised Learning

    Motivation 虽然半监督学习减少了大量数据标注的成本,但是对计算资源的要求依然很高(无论是在训练中还是超参搜索过程中),因此提出想法:由于计算量主要集中在大量未标注的数据上,能否从未标注的数据 ...

  5. 删除redis对应key的缓存

    [root@zhyly-pre-002 ~]# /usr/local/redis/bin/redis-cli -p 6379 #登录redis 127.0.0.1:6379> auth 'Red ...

  6. Java-(array)数组的基本概念 及 Java内存划分

    (array)数组的基本概念 数组的概念:是一种容器,可同时存放多个数据值 数组的特点: 1.数组是一种引用数据类型 2.数组当中的多个数据,类型必须统一 3.数组的长度在程序运行期间不可改变 数组的 ...

  7. 文件服务器 — File Browser

    前言 一直想部署一套文件服务器,供队友之间相互传输文件.平时用微信发送文件真的太烦了,每发送或者接收一次都会有一个新的文件,造成重复文件太多了.文件服务器统一管理,自己需要什么文件再下载. 前面也安装 ...

  8. MybatisPlus多表连接查询一对多分页查询数据

    一.序言 在日常一线开发过程中,多表连接查询不可或缺,基于MybatisPlus多表连接查询究竟该如何实现,本文将带你找到答案. 在多表连接查询中,既有查询单条记录的情况,又有列表查询,还有分页查询, ...

  9. [.NET学习] EFCore学习之旅 -1

    1.创建项目 这里我们先新建一个控制台项目:"jyq.EFCore.Learn",框架基于.NET6 2.安装 Neget包 Install-Package Microsoft.E ...

  10. 【开源库推荐】#4 Poi-办公文档处理库

    原文:[开源库推荐] #4 Poi-办公文档处理库 - Stars-One的杂货小窝 github仓库apache/poi Apache POI是Apache软件基金会的开放源码函式库,POI提供AP ...