【JAVA】一篇文章教你看懂JVM 垃圾回收GC

对于垃圾回收,其实最主要的就是三个问题

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

哪些内存需要回收

引用计数法

  给每个对象添加一个引用计数器,每当有一个地方引用他时,计数器就加1,当引用失效时,计数器就减1,任何时刻计数器为0时,就是这个对象不可能再被使用,这个时候就可以回收。
  客观的说,这个方法实现简单,而且效率也很高,但是弊端比较明显。如果objA和objB都有instance这个字段

objA.instance = objB;
objB.instance = objA;

这样一来的话,两个对象都会永久存在于内存中,不被回收。

可达性分析

  这个算法是将一部分成为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链。当一个对象到"GC Roots"没有任何引用链相连,则该对象是不可用的。
在这里插入图片描述
  在上面图中,Object5,Object6,Object7虽然有关联,但是都没有到"GC Roots"的引用,所以这三个对象也是可以被回收的。

在Java语言中,可以作为"GC Roots"的对象一般为:

  • 虚拟机栈中(栈帧中的本地变量表)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象

进一步理解引用

  在上面不论是引用计数法还是可达性分析,都涉及到对象的引用,按照之前的设定,要么就是引用,要么就是没引用,两种结果,这种说法过于绝对。所以JDK1.2后Java对引用的概念进行了扩张,将引用分为强引用,软引用,弱引用和虚引用四种。

  强引用:指在程序代码中普遍存在的,类似Object obj = new Object() 这类的引用,只要强引用还在,垃圾收集器就永远不会收掉被引用的对象。

  软引用:用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。一般用SoftReference类来实现。

  弱引用:也是用来描述非必需对象的,但是他的强度比软引用弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作的时候,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。一般用WeakReference实现。

  虚引用:也成为幽灵引用或者幻影引用,他是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。一般用PhantomReference实现。

给死亡的对象一个机会

  在按照上述的方法判定一个对象是否死亡之后,这个对象是不是就真的被认定是可以被回收了呢,Java给出的答案是,我还会给你一个重生的机会。

  一般一个对象有两次被标记的机会,第一次如果在可达性分析后发现此对象没有与 “GC Roots” 有相连接的引用链,那他将会被第一次标记并且进行第一次筛选,条件是此对象是否有必要执行finalize()方法。如果这个对象没有finalize()方法或者finalize()方法已经被虚拟机调用过,那么这个对象就被看作没有必要执行finalize()方法,那么就是可以被回收了。

  如果这个对象有必要执行finalize()方法,那么就会被放入一个F-Queue的队列中,等待执行。如果该对象在执行finalize()方法中与某个GC Roots建立了关联,那么他就不会被回收,否则在第二次标记中,还是会被标记为垃圾。

方法区也可以被回收

  在之前我们了解过JVM的内存结构,我们可以知道JVM的内存中,本地方法栈,虚拟机栈,程序计数器是线程私有的,这个内存会随着线程的结束而释放,但是还有Java堆,方法区是属于所有线程共用的,Java堆主要是存放对象的地方,而垃圾回收也是针对对象,所以主要是真的Java堆,那么方法区也属于所有线程共用的,他也可以被加入垃圾回收机制,但是因为在HotSpot中是用永久代实现的方法区,这些内存使用垃圾回收机制的话,效率远远低于Java堆中的新生代和老年代的回收效率。但有些方法区的内容也可以被加入垃圾回收。

  废弃常量:假设一个String对象叫做"abc",如果当前系统中没有一个String对象叫做"abc",也就是说没有任何String对象引用常量池中的"abc"常量,也没有其他地方引用这个字面量,那么发生垃圾回收的时候并且有必要的前提下,这个"abc"会被回收。

  无用的类:废弃常量的判定比较简单,无用的类判定就比较复杂了,一般要同时满足下面三个条件:
  1.该类的所有实例已经被回收,也就是说,Java堆中不存在该类的任何实例。
  2.加载该类的ClassLoader已经被回收
  3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

  现在我们知道,什么算是垃圾了,也就是知道回收的对象是谁,那么下一步就是该知道怎么收集垃圾了。垃圾收集方法用四种,严格的说又三种:标记-清除算法,复制算法,标记-整理算法。还有一个分代收集算法,这个其实是对新生代和老年代分别运用前面三种算法中的不同算法。

标记-清除算法

  标记清除算法比较简单,直接在内存中把所有垃圾标记好,然后进行回收就好了。方法简单,弊端也很明显,会有很多空间碎片产生。那么如果有需要内存比较大的对象需要进来,这个时候找不到能放下对象的连续内存,就要再次触发垃圾回收,这样效率就会变得非常低。
在这里插入图片描述

复制算法

  为了解决效率问题,就有了复制算法。复制算法先把所有内存空间分成两份,先用其中一半,在进行垃圾回收的时候,把标记为不是垃圾的对象,连续复制到另一半,然后将其余被标记为垃圾的对象回收。这个算法很好的解决了空间碎片的问题,但是最明显的弊端就是内存只用了一半,并且在老年代这种垃圾回收时,生存下来的对象占多数的情况下,复制就非常的耗时。

  但是对于新生代这种对象98%都是朝生夕死的,就很适用,因为复制的数量很少,不会因为复制而降低效率,并且由于复制的对象比较少,在一般的JVM中都是分未较大的Eden空间和两块较小的Survivor空间,这样也就解决了空间使用率低的问题。
在这里插入图片描述

标记-整理算法

  因为复制算法特别适用于新生代,但是对于老年代而言就效率很低了,所以有人提出了标记整理法,过程跟标记清理算法基本一致,只是在后续步骤不是对可回收对象对象直接进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述

分代收集算法

  根据对象存活周期将内存分为几块,一般Java将对象分为新生代和老年代,新生代用复制算法,老年代用标记清理或者标记整理算法。

HotSpot中常用的两种垃圾回收器

  在HotSpot中是用的可达性分析作为是否可回收的方法,但是很明显,因为程序运行的速度比较快,在可达性分析的时候,可能有些引用在这个时间段内会发生变化,那么可达性分析就会变得不准确,所以在垃圾回收的时候,会有一个(Stop the world)的操作,也就是让所有操作停下来,等垃圾回收完了,再继续。所以垃圾回收的时间也是一个很重要的因素。

CMS

  CMS(Conscurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,从CMS名字包含(Mark Sweep)就能知道,CMS收集器是基于 “标记-清除” 算法实现的。一般包含四个步骤:

  1.初始标记
  2.并发标记
  3.重新标记
  4.并发清除

  初始标记和重新标记这两个步骤仍然要STW(Stop The World)。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing也就是找到GC Roots间接关联到的所有对象,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(因为并发标记是不STW,所以会有因为程序运行改变的关联对象),这个阶段停顿时间一般会比初始标记阶段稍长,但远比并发标记短。并发清除就是清除可以回收的对象了,这个阶段不用STW。

  CMS怎么实现低停顿其实也很明显了,因为两个耗时最长的阶段(并发标记和并发清除)是和用户线程并发进行的,停顿时间只有初始标记和重新标记这两个用时较短的时间。
在这里插入图片描述

G1(Garbage-First)

  G1收集器是当前收集器技术发展最前沿成果之一。G1是一款面向服务端应用的垃圾收集器(HotSpot是准备在未来的时间里,想用G1替代掉CMS)。
  先全面了解一下G1的特点

  • 并行与并发:G1能充分利用多CPU,多核环境下的硬件优势,使用多个CPU来缩短STW的时间,有些其他收集器需要STW的GC操作,G1可以通过并发的方式让程序继续运行。
  • 分代收集:与其他收集器一样,G1仍然保留了分代的概念。
  • 空间整合:与CMS的标记-清理算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部来看,是基于复制算法实现的,所以无论如何这两个算法都意味着G1在运行期间不会产生内存空间碎片,
  • 可预测停顿:G1可以让使用者明确指定一个M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。这几乎已经是实时Java的垃圾收集器特征了。

  看了上面的特点不太清楚不要紧,我们只要知道大概的概念就行了,这样接下来的细节会比较好理解。

  G1也保留了新生代和老年代的概念,但在G1之前其他的收集器都是新生代和老年代是一个整体,而在G1中,他将所有内存分为多个大小相等的独立区域(Region),而新生代和老年代是多个Region的集合。也就是说G1其实是将内存化整为零了,然后再每个Region上进行操作。

  G1收集器之所以能建立可预测的停顿时间模型,是因为他可以有计划的避免Java堆中进行全区域的垃圾收集。G1会跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),然后再后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage-First的由来)这就保证了在有限时间内可以尽可能的获得高的收集效率。

  在各个Region中可能有互相的对象引用,或者新生代和老年代之间的互相引用,G1中每个Region都对应一个Remembered Set来避免全堆扫描。

  从整体上来看,G1收集器的运作可以分为四个步骤:

  1.初始标记
  2.并发标记
  3.最终标记
  4.筛选回收

  前三个步骤跟CMS有很大的相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值(不了解也没关系,知道这个值时干啥的就行),让下一个阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这个阶段也需要STW但很短。

  并发标记:从GC Roots开始对堆中对象进行可达性分析,与CMS基本一致。

  最终标记:和CMS一样也是重新标记发生变化的对象,在G1中会将这段时间对象变化记录在Remembered Set Logs中,这个阶段要把Remembered Set Logs的数据合并到Remembered Set中。

  筛选回收:先对各个Region进行排序,然后根据用户制定的策略进行回收操作。这个阶段一般是要STW。
在这里插入图片描述

内存分配策略

  所以,在上面介绍完之后,其实Java的垃圾回收机制可以归结为自动化解决了两个问题:给对象分配内存和回收分配给对象的内存,后者就是垃圾回收算法,那么前者其实也有一定的讲究。

  之前在复制算法中提到过,为了不让复制空间变成一半一半,可以把内存分为较大的Eden和两个较小的Survivor空间,一般是8:1:1 ,所以一般把Java堆内存分为新生代和老年代,有时候也有永久代(元空间),新生代又分为三个空间。
在这里插入图片描述

  对于年轻代的策略

  年轻代分为三个区域,Eden区,Survivor1区,Survivor2区,有时候Survivor1区,Survivor2区又叫做from区和to区,对象优先分配到Eden区,Eden区要满的时候,会有一次复制回收,把存活的对象放在Survivor1区,等Eden区再次快要满的时候,又会有一次复制,把Eden区和Survivor1区存活的对象存放在Survivor2区,然后如此循环

  
  对于老轻代的策略

  虚拟机提供了一个-XX:PretenureSizeThreshold参数,大于这个参数的对象会直接进入老年代,放置年轻代发生大量内存复制,效率太低。
  

  年轻代到老年代

  年轻代的对象每熬过一次MinorGC年龄就增加一岁,默认15岁,就会进入老年时代,不过这个条件并非绝对,如果Survivor中相同年龄的对象总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象可以直接晋升老年代
  
  当然年轻代到老年代在极端情况下也是有风险的,年轻代在MinorGC后会有对象进入老年代,在极端情况下,年轻代所有对象都存活并进入老年代,所以在MinorGC之前,虚拟机会检查老年代的连续内存空间是否大于年轻代所有对象总和。

  如果空间不够,那么这次MinorGC是有风险的。

  如果允许冒险,MinorGC会直接执行,如果失败,会发起一次full GC

  如果不允许冒险,则先执行一次GC,在进行MinorGC

minorGC和MajorGC分别发生在什么时候?

  minorGC:

  1)Eden区满了 2)新创建对象的大小大于Eden所剩余空间

  majorGC:

  1)每次晋升到老年代的对象平均大小超过了老年代剩余空间

  2)minorGC后存活的对象超过了老年代剩余空间

  minor GC和major GC的过程?

  minor GC:在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

  major GC:参考CMS的工作过程。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值