垃圾回收器机制(三):正确姿势解读GC日志

垃圾回收器机制(三):正确姿势解读GC日志,第1张

GC的基本原理 Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。 对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为\"不可达的\"GC将负责回收所有\"不可达\"对象的内存空间。 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象(详见 参考资料1 )。通过这种方式确定哪些对象是\"可达的\",哪些对象是\"不可达的\"当GC确定一些对象为\"不可达\"时,GC就有责任回收这些内存空间。但是,为了保证GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。因此,不同的JVM的实现者往往有不同的实现算法。这也给Java程序员的开发带来行多不确定性。本文研究了几个与GC工作相关的问题,努力减少这种不确定性给Java程序带来的负面影响。 增量式GC( Incremental GC ) GC在JVM中通常是由一个或一组进程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CPU当GC进程运行时,应用程序停止运行。因此,当GC运行时间较长时,用户能够感到Java程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低,这意味着还有很多应该回收的对象没有被回收,仍然占用大量内存。因此,在设计GC的时候,就必须在停顿时间和回收率之间进行权衡。一个好的GC实现允许用户定义自己所需要的设置,例如有些内存有限有设备,对内存的使用量非常敏感,希望GC能够准确的回收内存,它并不在意程序速度的放慢。另外一些实时网络游戏,就不能够允许程序有长时间的中断。增量式GC就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然,增量式GC在整体性能上可能不如普通GC的效率高,但是它能够减少程序的最长停顿时间。 Sun JDK提供的HotSpot JVM就能支持增量式GCHotSpot JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在运行Java程序时增加-Xincgc的参数。HotSpot JVM增量式GC的实现是采用Train GC算法。它的基本想法就是,将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁高和具有相关性的对象放在一队中,随着程序的运行,不断对组进行调整。当GC运行时,它总是先回收最老的(最近很少访问的)的对象,如果整组都为可回收对象,GC将整组回收。这样,每次GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。

1、取出记录本登记使用者和开始时间。

2、打开净化器上的载气开关阀,然后检查是否漏气,保证气密性良好。

3、调节肿瘤量为适当值。

4、调节分流阀是分流流量为实验所需的流量,柱流量即为总流量减去分流量。

5、调节尾吹流量控制阀使尾吹流量为适当值,并使尾吹流量与柱流量之和不低于载气总流量。

6、打开净化器的空气、氢气开关阀,调节空气、氢气流量为适当值。

7、根据实验需要设置柱温、进样口温度和FID检测器温度。

8、FID检测器温度达到150摄氏度以上,按FIRE键点燃FID检测器火焰。

9、设置FID检测器灵敏度和输出信号衰减。

10、如果基线不在零位,调节调零电位器A使FID输出信号在纪录仪或积分仪零位附近。待所设参数达到设置时,即可进行分析。

11、但所设参数达到设置时即可进样分析。

本文主要内容

本文主要从概念上介绍内存回收及垃圾收集器相关内容,不涉及具体性能调优。

内存回收是程序员永恒的主题,虽然Java虚拟机自动回收内存,但仍存在内存漏泄的可能,需要理解内存回收机制,有助于程序员规避、排查内存泄漏问题。

GC机制,最重要的是三个问题:

对象已死

对象是否已经死亡,可被回收,经常能听到下面这种说法:

引用计数法 :给对象中添加一个引用计数器,每当有一个地方引用时,计数器就加1,当引用失效时,计数器值就减1,任何时候计数器为0的对象就是不可能再被使用的

不过,它存在一个致命缺陷,很难解决循环引用问题,比如对象AB,A引用B,B也引用A,但它们再没有被其它人所引用,AB理应是被回收对象,但它们的引用计数器仍然不为0,导致无法回收。

Java虚拟机不使用此算法来判定对象是否死亡,不过它依然有很多优点,简单。Android 中的智能指针即是使用这种方法,不过添加了智能指针强弱引用来解决循环引用问题

可达性分析算法 ,这个算法的基本思路就是通过一系列的名为“GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到“GC Roots"没有任何引用链相连,则此对象不可用,将被回收

在Java中,可作为“GC Roots"的对象包括以下几种:

引用

在JDK12以前,引用的概念为:

JDK12之后,引用概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,四种引用强度依次减弱。

垃圾收集算法

本章将介绍三种垃圾收集算法。

标记清除算法 ,算法分成“标记”和“清除”两个阶段,首先标记出需要回收的对象,在标记完成后统一回收掉所有被标记的对象

它主要有两个问题,一是效率不高,标记和清除过程的效率都不高,另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多,当程序在运行中需要分配较大对象,因为碎片过多,可能无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法 ,为了解决效率问题,一种称为复制的收集算法出现了。它将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。每次都是只对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是代价高昂,将内存缩小为原来的一半。

现在的商业虚拟机都采用这种算法来回收新生代,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当回收时,将Eden和Survivor看还存活的对象一次性拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

虚拟机默认Eden和Survivor的大小比例是8比1,所以只有10%的空间会被浪费。

如果存活的对象较多而Survivor空间不够用时,需要依赖其它内存(老年代)进行分配担保,如果另外一块Survivor空间没有足够的空间来存放上一次新生代的存活对象,这些对象将直接通过分配担保机制进入老年代

标记整理算法 ,复制收集算法在对象存活率较高时就要执行较多的复制 *** 作,效率将会变得更低,更关键的是,如果不想浪费一半的空间,就需要额外的空间进行分配担保,以应对所有对象百分百存活的极端情况。所以老年代一般不直接使用这种算法

根据老年代的特点,提出了“标记整理算法”,过程依然和“标记清除算法”一致,但后续步骤不是直接对可回收对象进行清除,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集算法。它根据对象存活周期的不同将内存划分为几块。

一般是把Java堆分成新生代和老年代,新生代又分成一块较大的Eden和两块Survivor。根据各个年代的特点采用最适当的收集算法。

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

垃圾收集器

Serial收集器

Serial收集器是最基本、历史最悠久的收集器。顾名思义,这个收集器是一个单线程收集器。单线程的意义并不仅仅说明它只会使用一个CPU或者一条线程去完成垃圾收集工作,更重要的是它在垃圾收集时,必须暂停其他所有的工作线程(Sun将这件事情称之为“Stop the world”),直到它结束

“Stop the world”,非常影响用户体验,虚拟机在后台自动发起和完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉。

虽然Serial收集器出现时间较长,但它依然是Client模式下的默认新生代收集器

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程控制版本,除了使用多条线程进行垃圾收集之外,其它和Serial收集器完全一样。

ParNew收集器是Server模式下虚拟机中的首选的新生代收集器。而且在单CPU环境下,ParNew绝对不会比Serial更高效,甚至由于存在线程交互的开销,在两个CPU的环境中都不一定比Serial更好

Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法,又是并行的多线程收集器,看上去和ParNew一样,但它非常的有特点。

Parallel Scavenge收集器关注点即是吞吐量,CMS等收集器的关注点是尽量缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge的目的则是达到一个可控制的吞吐量。

Parallel Scavenge提供两个参数用于精准控制吞吐量

Serial old收集器

Serial old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记整理算法,它的主要意义也是被Client模式下的虚拟机使用

Parallel old收集器

Parallel old是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它很适合互联网站或者B/S系统的服务端上。

从Mark Sweep名字可知,CMS用的是标记清除算法,它的动作过程较之前的复杂一些,整个过程分为4个步骤:

其中初始标记、重新标记这两个步骤仍然需要 “Stop the world”。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发阶段就是进行GC Root Tracing的过程,而重新标记则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,重新标记时间略比初始标记长,但远比并发标记时间短。

整个过程最耗时的是并发标记和并发清除,但用户线程和收集器线程一起工作,所以总体上说,CMS收集器的内存回收过程是与用户线程一起并发地执行。

所谓jvm垃圾回收机制其实就是相较于于c、c++语言的优势之一是自带垃圾回收器,垃圾回收是指不定时去堆内存中清理不可达对象。垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用Systemgc 方法来建议执行垃圾收集器。

在我们程序运行中会不断创建新的对象,这些对象会存储在内存中,如果没有一套机制来回收这些内存,那么被占用的内存会越来越多,可用内存会越来越少,直至内存被消耗完。于是就有了一套垃圾回收机制来做这件维持系统平衡的任务。

1确保被引用对象的内存不被错误的回收

2回收不再被引用的对象的内存空间

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

优点:引用计数收集器可以很快地执行,交织在程序的运行之中。

缺点:很难处理循环引用,比如上图中相互引用的两个对象,计数器不为0,则无法释放,但是这样的对象存在是没有意义的,空占内存了。

引用计数法处理不了的相互引用的问题,那么就有了可达性分析来解决了这个问题。

从GC Roots作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之最终不能与GC Roots有引用关系的视为不可达,不可达对象即为垃圾回收对象。

我自己的理解是,皇室家族每过一段时间,会进行皇室成员排查,从皇室第一代开始往下找血缘关系的后代,如果你跟第一代皇室没有关系,那么你就会被剔除皇室家族。

1虚拟机栈中引用的对象(正在运行的方法使用到的变量、参数等)

2方法区中类静态属性引用的对象(static关键字声明的字段)

3方法区中常量引用的对象,(也就是final关键字声明的字段)

4本地方法栈中引用的对象(native方法)

1显示地赋予某个对象为null,切断可达性

在main方法中创建objectA、objectB两个局部变量,而且相互引用。相互引用直接调Systemgc()是回收不了的。而将两者都置为null,切断相互引用,切断了可达性,与GCRoots无引用,那么这两个对象就会被回收调。

2将对象的引用指向另一个对象

这里将one的引用也指向了two引用指向的对象,那么one原本指向的对象就失去了GCRoots引用,这里就判断该对象可被回收。

3局部对象的使用

当方法执行完,局部变量object对象会被判定为可回收对象。

4只有软、弱、虚引用与之关联

new出来的对象被强引用了,就需要去掉强引用,改为弱引用。被弱引用之后,需要置空来干掉强引用,达到随时可回收的效果。

只被软引用的对象在内存不足的情况,可能会被GC回收掉。

只被弱引用持有的对象,随时都可能被GC回收,该对象就为可回收对象。

是不是被判定为了可回收对象,就一定会被回收了呢。其实Ojbect类中还有一个finalize方法。这个方法是对象在被GC回收之前会被触发的方法。

该方法翻译过来就是:当垃圾回收确定不再有对该对象的引用时,由垃圾回收器在对象上调用。子类重写finalize方法以处置系统资源或执行其他清除。说人话就是对象死前会给你一个回光返照,让你清醒一下,想干什么就干什么,甚至可以把自己救活。我们可以通过重写finalize方法,来让对象复活一下。

示例:

执行的结果:

这里我们重写FinalizeGC类的finalize方法, 使用FinalizeGCinstance = this语句,让对象又有了引用,不再被判定为可回收对象,这里就活了。然后再置空再回收一下,这个对象就死了,没有再被救活了。所以finalize方法只能被执行一次,没有再次被救活的机会。

在JDK18版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。

元空间有注意有两个参数:

MetaspaceSize :初始化元空间大小,控制发生GC阈值

MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

为什么移除永久代?

移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。

有了元空间就不再会出现永久代OOM问题了!

1Generational Collection(分代收集)算法

分代收集算法是GC垃圾回收算法的总纲领。现在主流的Java虚拟机的垃圾收集器都采用分代收集算法。Java 堆区基于分代的概念,分为新生代(Young Generation)和老年代(Tenured Generation),其中新生代再细分为Eden空间、From Survivor空间和To Survivor空间。 (Survivor:幸存者)

分代收集算法会结合不同的收集算法来处理不同的空间,因此在学习分代收集算法之前我们首先要了解Java堆区的空间划分。Java堆区的空间划分在Java虚拟机中,各种对象的生命周期会有着较大的差别。因此,应该对不同生命周期的对象采取不同的收集策略,根据生命周期长短将它们分别放到不同的区域,并在不同的区域采用不同的收集算法,这就是分代的概念。

当执行一次GC Collection时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次GC Collection并在From Survivor空间存活的仍年轻的对象也会复制到To Survivor空间。

对象进入到From和To区之后,对象的GC分代年龄ege的属性,每经过GC回收存活下来,ege就会+1,当ege达到15了,对象就会晋级到老年代。

2Mark-Sweep(标记-清除)算法

标记清除:标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。标记-清除算法主要是运用在Eden区,该区对象很容易被回收掉,回收率很高。

3Copying(复制)算法

复制算法的使用在From区和To区,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

缺点:可使用内存缩减为一半大小。

那么复制算法使可使用内存大小会减半,设计上是怎么解决这个问题的呢。就是给From和To区划分尽可能小的区域。经过大数据统计之后,对象在第一次使用过后,绝大多数都会被回收,所以能进入第一次复制算法的对象只占10%。那么设计上,Eden、From、To区的比例是8:1:1,绝大多数对象会分配到Eden区,这样就解决了复制算法缩减可用内存带来的问题。

4Mark-Compact (标记—整理)算法

在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制 *** 作,导致效率变低。标记—清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记—整理算法,与标记—清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。

垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现:

Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,

优点是简单高效;

Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器

的老年代版本;

ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器

的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;

CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行

收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿

的特点,追求最短 GC 回收停顿时间。

LeakCanary Git地址

目前最新版本是27,使用起来比较简单,只需要在gradle里面加入一句话就可以了而且debugImplementation只在debug模式下有效,所以不用担心用户在正式环境下也会出现LeakCanary收集。

在项目中加入LeakCanary之后就可以开始检测项目的内存泄露了,把项目运行起来之后, 开始随便点自己的项目,下面以一个Demo项目为例,来聊一下LeakCanary记录内存泄露的过程以及我如何解决内存泄露的。

项目运行起来之后,在控制台可以看到LeakCanary的打印信息:

这说明LeakCanary正在不断的检测项目中是否有剩余对象。那么LeakCanary是如何工作的呢LeakCanary的基础是一个叫做ObjectWatcher Android的library。它hook了Android的生命周期,当activity和fragment 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给ObjectWatcher, ObjectWatcher持有这些被销毁对象的弱引用(weak references)。如果弱引用在等待5秒钟并运行垃圾收集器后仍未被清除,那么被观察的对象就被认为是保留的(retained,在生命周期结束后仍然保留),并存在潜在的泄漏。LeakCanary会在Logcat中输出这些日志。

我一直来回进入内测泄漏和内存抖动的页面,一会就有通知提示了,点击这个通知,可以在手机上查看信息

此时也可以在logcat上查看信息

路径中的每一个节点都对应着一个java对象。熟悉java内存回收机制的同学都应该知道”可达性分析算法“,LeakCanary就是用可达性分析算法,从GC ROOTS向下搜索,一直去找引用链,如果某一个对象跟GC Roots没有任何引用链相连时,就证明对象是”不可达“的,可以被回收。

我们从上往下看:

在泄漏路径的顶部是GC Root。GC Root是一些总是可达的特殊对象。

接着是:

这里先看一下Leaking的状态(YES、NO、UNKNOWN),NO表示没泄露。那我们还得接着向下看。一直到viewIOSStyleloadingView1 instance 才出现Yes

这说明这里出现了内存泄漏,这里直接指出来是我们自写的View出现了内存泄漏

一般推断内存泄露是从最后一个没有泄漏的节点(Leaking: NO )到第一个泄漏的节点(Leaking: YES)之间的引用。

可以看到liaking状态是YES之前是AnimatorUpdateListener,那就说明是此listener没有做释放,非常精准的定位问题了

对于每个被保留的对象,LeakCanary会找出阻止该保留对象被回收的引用链:泄漏路径。泄露路径就是从GC ROOTS到保留对象的最短的强引用路径的别名。确定泄漏路径以后,LeakCanary使用它对Android框架的了解来找出在泄漏路径上是谁泄漏了。

NO: 没有内存泄漏

UNKNOWN: 表示这里可能出现了内存泄露,这些引用你需要花时间来调查一下,看看是哪里出了问题。

YES: 表示此处有内存泄漏

参考文章 >

以上就是关于垃圾回收器机制(三):正确姿势解读GC日志全部的内容,包括:垃圾回收器机制(三):正确姿势解读GC日志、【JVM】对象分配与回收--垃圾回收机制、求教一个关于java GC的问题:怎么判断GC Roots到这个对象不可达等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9320622.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-27
下一篇 2023-04-27

发表评论

登录后才能评论

评论列表(0条)

保存