2.1 概述

垃圾收集需要考虑三件事:哪些内存需要回收;什么时候回收;如何回收;经过半个世纪的发展,今天的内存动态分配和内存回收技术都已经十分成熟,但我们为什么要了解垃圾收集和内存分配呢?答案呼之欲出,当需要排查各种内存溢出,内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
其中,程序计数器,虚拟机栈,本地方法栈都随着线程而生,线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的入栈和出栈,每一个栈帧中分配多少内存基本上在类结构确定下来就已知的,因此这几个区域内存的分配和回收都是确定的,当方法结束或者线程结束,这三个区域内存自然就跟着回收了。
但是,堆和方法区有着显著的不确定性,一个接口的多个实现类需要的内存可能不同,一个方法所执行的把不同条件分支所需要的内存也可能不一样,只有在运行期间,我们才知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收都是动态的。

2.2 对象已死

2.2.1 引用计数法

内容:

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加一;当引用失效时,计数器就减一;在任何时刻计数器为零的对象是不可能再被使用的。

问题:

虽然这个算法实现简单,判定效率很高,但是存在着循环引用的问题。当A->B,B->A,除此之外没有任何引用,实际上这两个对象已经不可能再被访问了,但是因为它们互相引用着对方,导致它们的引用计数器不为零,无法回收。

2.2.2 可达性分析算法

内容:

通过GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能在被使用的。

GC Roots对象包括:

在虚拟机栈中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量等;
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量;
在方法区中常量引用的对象,譬如字符串常量池里的引用;
在本地方法栈中Native方法引用的对象;
虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器;
所有被同步锁持有的对象;

2.2.3 再谈引用

将引用分为强引用,软引用,弱引用,虚引用。
强引用:Object obj = new Object(),无论任何情况,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象;
软引用:在系统中将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收;
弱引用:当垃圾收集器开始工作,无论当前内存是否充足,都会回收掉只被弱引用关联的对象;
虚引用:一个对象是否有虚引用的存在,完全不影响其生存时间。

2.2.4 生存还是死亡

即使在可达性分析算法中判定为不可达的对象,但它还有一次自我拯救的机会,如果此对象有必要执行finalize()方法,那么会将它放置在F-Queue的队列之中,并在稍后由Finalizer线程去执行finalize()方法,如果它成功逃脱,则将它移除“即将回收”的集合,如果它没有逃脱,那么它真的要被回收了。

2.2.5 回收方法区

方法区的垃圾收集只要回收两部分内容:废弃的常量和不再使用的类型。

2.3 垃圾收集算法

2.3.1 分代收集理论

内容:

它虽名为理论,但实际它是一套符合大多数程序运行实际情况的经验法则,它建立在两个假说之上:
(1)弱分代假说:绝大多数对象都是朝生夕灭的。
(2)强分代假说:熬过越多次垃圾收集的过程就越难消灭掉。
这两个分代假说共同奠定了多款常用的垃圾收集器一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储。
把分代收集理论放在虚拟机里,设计者会将Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域,在新生代中,每次垃圾收集时都发现大批对象死去,而每次回收后存活的对象将会逐步晋升到老年大中存放。但是分代收集并非简单划分一下内存区域这么容易,它存在一个明显问题,那就是对象不是孤立存在的,对象之间存在跨代引用。假设要进行一次只局限于新生代区域内的收集,但新生代完全有可能被老年代所引用,为了找出该区域中存活对象,不得不再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性。这样做,无疑是为内存回收带来很大的性能负担。因此,需要第三条经验法则:
(3)跨代引用假说:跨代引用相对于同代引用仅占极少数。
根据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代建立一个全局的数据结构(记忆集),这个结构把老年代划分为若干小块,标识出老年代的哪一块内存存在跨代引用。此后当个发生Minor GC时,只有包含了跨代引用的小块内存才会被GC Roots进行扫描。

回收类型:

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中分为:
新生代收集(Minor GC):指目标只是新生代的垃圾收集。
老年代收集(Major GC):指目标只是老年代的垃圾收集。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

回收算法:

标记-复制,标记-清除,标记-整理

2.3.2 标记-清除算法

内容:

首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活对象,统一回收所有未被标记的对象。

缺点:

(1)执行效率不稳定:执行效率随对象数量的增长而降低;
(2)内存空间碎片化问题:标记清除之后会产生大量的不连续内存碎片;

2.3.3 标记-复制算法

内容:

把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存都是用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性全部复制到另一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。

缺点:

HotSpot虚拟机默认将Eden,Survivor和另一块Survivor的大小比例是8:1:1,由于无法保证每次垃圾收集时都有高于90%的对象被收集,所以,为了安全设计,当Survivor空间不足容纳一次Minor GC之后存活的对象,就需要依赖老年区进行分配担保。

2.3.4 标记-整理算法

内容:

首先标记出所有需要回收的对象,在标记完成后,让所有存活的对象都想内存空间一端移动,然后直接清理掉边界以外的内存。

缺点:

对于老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,这种停顿叫做”Stop The World“。

2.4 经典垃圾收集器

2.4.1 Serial收集器

这个收集器是一个单线程收集器,“单线程”强调的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直至它收集结束。“Stop The World”虽然看起来很酷,但是这项工作其实是由后台自动发起和自动完成的,对于很多应用来说,正常工作的线程全部停掉是不能接受的。
虽然它现在面临的状态是食之无味,弃之可惜,但是它也有着优于其他收集器的地方,那就是简单高效,对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或者处理器核心数较少的环境来说,它没有线程的交互开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

2.4.2 ParNew收集器

它是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数,收集算法,Stop The World,对象分配规则,回收策略等都与Serial完全一致。
在单核心处理器的环境中,ParNew收集器绝对比不上Serial,而且,在超线程技术实现的伪双核处理器环境中都不能百分之百超越Serial收集器。

2.4.3 Parallel Scavenge收集器

它是能够并行收集的多线程收集器,它的诸多特性从表面上看和ParNew非常相似。
但是它也有自己的特点,对于CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量

2.4.4 Serial Old收集器

它是Serial收集器的老年代版本,同样是单线程收集器,采用标记-整理算法。

2.4.5 Parallel Old收集器

它是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

2.4.6 CMS收集器

它是一种以获取最短回收停顿时间为目标的收集器,它是基于标记-清除算法实现的;整个收集过程分为四个步骤:
(1)初始标记:仅仅只是标记一下GC Roots能直接关联到的对象;
(2)并发标记:从GC Roots的直接关联的对象开始遍历整个对象图的过程;
(3)重新标记:为了修正并发并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;
(4)并发清除:清理删除掉标记阶段判断的已经死亡的对象。

虽然它已经很成功了,但是它存在三个明显的缺点:
(1)CMS收集器对处理器资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是由于占用了一部分线程而导致应用程序变慢,降低吞吐量。
(2)由于CMS收集器无法处理浮动垃圾,有可能出现“Concurrent Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
(3)因为CMS是基于标记-清除算法,因此会产生大量空间碎片,空间碎片过多,将会给大对象分配带来麻烦,往往出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,不得不触发Full GC的情况。

2.4.7 G1收集器

它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。如果想要建立起“停顿时间模型”的收集器,首先要有一个思想的转变,在之前的全部垃圾收集器,垃圾收集的目标范围要么是整个新生代(Minor GC),要么是整个老年代(Major GC),再要么就是整个Java堆(Full GC),而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1的Mixed GC模式。
虽然G1也仍然遵循分代收集理论设计的,但它不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代Eden空间,Survivor空间,或者老年代空间。
Region中还有一类特殊的存放大对象的Humongous区域,G1大多数行为把这块区域作为老年代的一部分进行看待。
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,这样可以有计划地避免在整个Java堆中进行全区域垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的价值大小,价值即回收所获得的空间大小以及回收所需时间经验值,然后在后台维护一个优先级列表,每次根据设定的允许收集停顿时间,优先处理回收价值收益最大的Region。
G1收集器的运作过程大致分为四个步骤:
(1)初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用地Region中分配新对象;
(2)并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆中的对象图,找到要回收的对象,当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的的对象;
(3)最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录;(需要Stop The World)
(4)筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回忆集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。

2.5 实战:内存分配与回收策略

2.5.1 对象优先在Eden分配

大多情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

2.5.2 大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最经典的就是很长的字符串,或者元素数量很庞大的数组。

2.5.3 长期存活的对象将进入老年代

为了做到判断哪些对象应该放在新生代,哪些对象应该放在老年代,虚拟机给每个对象定义一个对象年龄计数器,存放在对象头。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过以此Minor GC,年龄就加一,当它的年龄到达默认15,就会晋升到老年代中。

2.5.4 动态对象年龄判定

为了适应不同程度内存状况,HotSpot虚拟机并不是达到设定才晋升老年代的,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间一半,年龄大于或者等于该年龄的对象就可以直接进入老年代无需等到设定的年龄。

2.6 HotSpot的算法细节实现

2.6.1 并发的可达性分析

引入三色标记,按照“是否访问过”这个条件标记成一下三个颜色:
(1)白色:表示对象尚未被垃圾收集器访问过。
(2)黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过了。
(3)灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
由于并发,收集器在对象图上标记颜色,同时用户线程在修改引用关系,可能出现两种后果。一种是把原本消亡的对象错误标记为存活,虽然不是好事,但其实可以容忍,下次收集清理掉就好。另一种原本存活的对象错误标记为已消亡,这非常致命,程序会发生错误,下面是致命的产生过程:

当且仅当以下两个条件同时满足时,产生“对象消亡”的问题:
(1)赋值器插入了一条或多条从黑色对象到白色对象的新引用;
(2)赋值器删除了全部从灰色对象到该白色对象的直接或间接引用;
为了解决这个问题,两种解决方案:
(1)增量更新:当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
(2)原始快照:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束后,再将这些记录过的引用关系中灰色对象为根,重新扫描一次。

2.垃圾收集器与内存分配策略相关推荐

  1. 垃圾收集器与内存分配策略(五)之垃圾日志与常见参数

    2019独角兽企业重金招聘Python工程师标准>>> 垃圾收集器与内存分配策略(五)--垃圾日志与常见参数 理解GC日志 每个收集器的日志格式都可以不一样,但各个每个收集器的日志都 ...

  2. jvm(3)-垃圾收集器与内存分配策略

    [0]README 0.1)本文部分文字转自:深入理解jvm,旨在学习 垃圾收集器与内存分配策略 的基础知识: [1]垃圾回收概述 1)GC(Garbage Collection)需要完成的3件事情: ...

  3. java eden分配参数,JVM垃圾收集器与内存分配策略,

    垃圾收集器与内存分配策略 对象存活判断 引用计数算法 给对象添加一个计数器,每有一个引用+1,当引用失效-1,若为0则不在被使用. 可达性分析算法 对象是否可到达GC roots 或者说GC root ...

  4. 【深入理解Java虚拟机学习笔记】第三章 垃圾收集器与内存分配策略

    最近想好好复习一下java虚拟机,我想通过深读 [理解Java虚拟机 jvm 高级特性与最佳实践] (作者 周志明) 并且通过写一些博客总结来将该书读薄读透,这里文章内容仅仅是个人阅读后简短总结,加强 ...

  5. java_opts gc回收器_jvm垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略: 以下参考周志明的<>. 判断对象是否存活: 引用计数:通过判断对象被引用的次数(为0,则表示不可被使用),但这很难解决对象相互循环引用的问题. 根搜索算法:即采 ...

  6. jvm垃圾收集器与内存分配策略

    2019独角兽企业重金招聘Python工程师标准>>> 垃圾收集器与内存分配策略: 以下参考周志明的<<深入理解jvm高级特性与最佳实践>>. 判断对象是否存 ...

  7. JVM2:垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 文章目录 垃圾收集器与内存分配策略 对象回收 引用计数算法 可达性分析算法 四种引用类型 生存与死亡 回收方法区 垃圾收集算法 标记清除法 复制算法 标记-整理算法 HotS ...

  8. JVM:垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 1.对象已死吗 1).引用计数算法 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0的对象就 ...

  9. java虚拟机读书笔记 第三章 垃圾收集器和内存分配策略

    java虚拟机读书笔记 第三章 垃圾收集器和内存分配策略 GC需要完成的三件事情:哪些内存需要回收.什么时候回收.如何回收 垃圾回收器在对堆进行回收前,首先要确定那些对象存活,哪些对象已经死去,判断的 ...

  10. 03-1 手敲八千字,认识垃圾收集器必须清楚的前置知识【垃圾收集器的内存分配策略】

    03-1 50问!了解垃圾收集器必须清楚的前置知识--垃圾收集器的内存分配策略 author:陈镇坤27 创建时间:2021年12月27日01:58:45 字数:7932 文章目录 03-1 50问! ...

最新文章

  1. GitHub超4.1万星,最全Python入门算法来了
  2. 原版ubuntu中安装中文输入法
  3. 【小白学PyTorch】9.tensor数据结构与存储结构
  4. 某厂面试:如何优雅使用 SPI 机制
  5. Linux基本操作——VI和VIM
  6. Python_socketserver
  7. 炫彩渐变液态海报设计,太skr了!
  8. 一本通网站 1378:最短路径(shopth)
  9. 女生天生就是产品经理
  10. AIX和LINUX主机 CPU 内存 磁盘使用率监控
  11. python中日期使用_在Python中使用日期时间的每月总金额
  12. Mac下安装atari_py报错Exception: ROM is missing for pong
  13. .sln文件和.suo文件的解释
  14. Spark 中 map 与 flatMap 的区别
  15. js基础知识汇总13
  16. 什么是脏数据,缓存中是否可能产生脏数据,如果出现脏数据该怎么处理?
  17. MaxScript 例子 渲染
  18. 原生JavaScript贪吃蛇
  19. 用友u8 无法安装服务器系统,用友u8安装【解决方案】
  20. C/C++笔试题(很多)

热门文章

  1. 路由器+虚拟服务器+ssh,如何实现用SSH方式登陆路由器管理
  2. 容器云平台No.10~通过gogs+drone+kubernetes实现CI/CD
  3. libxml2的参考手册
  4. java.lang.ClassNotFoundException: org.eclipse.jdt.internal.compiler.env.INameEnvironment
  5. cad高程标注插件lisp_【源码】CAD高程转标高,CAD插件大全,小懒人CAD工具箱
  6. Unity分屏显示效果
  7. libuv之mingw64环境搭建及编译Libuv
  8. Unity中迭代器和协程的部分概念
  9. Springboot----实现邮箱验证码登录(代码部分)
  10. 分析称诺基亚仅押宝WP成自救牵绊