⑴ GC的复制算法和标记整理算法
复制算法
复制算法是将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中的一个区间(称为活动区间),而另外一个区间(空闲区间)是空闲的。
当有效内存空间耗尽时,JVM将暂停程序运行,开启复制GC线程。接下来GC线程会将活动区的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。
此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空闲区间的同时,垃圾对象已经被一次性全部回收了。
开始时:
在被GC线程处理后:
可以看到,1和4号对象被清楚了,而2,3,5,6号对象则是规则的排列在刚才的空闲区间,也就是现在的活动区间之间,此时左半部分已经变成空闲区间,然而,如中在下一次GC之后,左半部分再次变成活动区间。
显然:
复制算法缺点时非常明显的:
1.直接浪费了一般的内存。
2.再就是加入对象存活率非常高,达到了极端的100%,那么我们需要将所有的对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定的程度时,将会变得不可忽视。
由此可见,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们态悔必须要克服50%内存浪费。
标记整理算法
分为两个阶段:
1.标记:遍历GC roots,然后将存活的对象标记。
2.整理:移动所帆橡正有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收,这个阶段才为整理阶段。
它与GC 前后与复制算法类似,只不过没有了活动区间和空闲区间
倘若此时GC线程开始工作,那么紧接着开始的就是标记阶段了。此阶段与标记/清除算法的标记阶段是一样一样的,我们看标记阶段过后对象的状态,如下图。
接下来,便应该是整理阶段了。我们来看当整理阶段处理完以后,内存的布局是如何的,如下图。
⑵ 深入理解GC垃圾回收机制
在我们程序运行中会不断创建新的对象,这些对象会存储在内存中,如果没有一套机制来回收这些内存,那么被占用的内存会越来越多,可用内存会越来越少,直至内存被消耗完。于是就有了一套垃圾回收机制来做这件维持系统平衡的任务。
1.确保被引用对象的内存不被错误的回收
2.回收不再被引用的对象的内存空间
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时, 计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:引用计数收集器可以很快地执行,交织在程序的运行之中。
缺点:很难处理循环引用,比如上图中相互引用的两个对象,计数器不为0,则无法释放,但是这样的对象存在是没有意义的,空占内存了。
引用计数法处理不了的相互引用的问题,那么就有了可达性分析来解决了这个问题。
从GC Roots作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之最终不能与GC Roots有引用关系的视为不可达,不可达对象即为垃圾回收对象。
我自己的理解是,皇室家族每过一段时间,会进行皇室成员排查,从皇室第一代开始往下找血缘关系的后代,如果你跟第一代皇室没有关系,那么你就会被剔除皇室家族。
1.虚拟机栈中引用的对象(正在运行的方法使用到的变量、参数等)
2.方法区中类静态属性引用的对象(static关键字声明的字段)
3.方法区中常量引用的对象,(也就是final关键字声明的字段)
4.本地方法栈中引用的对象(native方法)
1.显示地赋予某个对象为null,切断可达性
在main方法中创建objectA、objectB两个局部变量,而且相互引用。相互引用直接调System.gc()是回收不了的。而将两者都置为null,切断相互引用,切断了可达性,与GCRoots无引用,那么这两个对象就会被回收调。
2.将对象的引用指向另一个对象
这里将one的引用也指向了two引用指向的对象,那么one原本指向的对象就失去了GCRoots引用,这里就判断该对象可被回收。
3.局部对象的使用
当方法执行完,局部变量object对象会被判定为可回收对象。
4.只有软、弱、虚引用与之关联
new出来的对象被强引用了,就需要去掉强引用,改为弱引用。被弱引用之后,需要置空来干掉强引用,达到随时可回收的效果。
只被软引用的对象在内存不足的情况,可能会被GC回收掉。
只被弱引用持有的对象,随时都可能被GC回收,该对象就为可回收对象。
是不是被判定为了可回收对象,就一定会被回收了呢。其实Ojbect类中还有一个finalize方法。这个方法是对象在被GC回收之前会被触发的方法。
该方法翻译过来就是:当垃圾回收确定不再有对该对象的引用时,由垃圾回收器在对象上调用。子类重写finalize方法以处置系统资源或执行其他清除。说人话就是对象死前会给你一个回光返照,让你清醒一下,想干什么就干什么,甚至可以把自己救活。我们可以通过重写finalize方法,来让对象复活一下。
示例:
执行的结果:
这里我们重写FinalizeGC类的finalize方法, 使用FinalizeGC.instance = this语句,让对象又有了引用,不再被判定为可回收对象,这里就活了。然后再置空再回收一下,这个对象就死了,没有再被救活了。所以finalize方法只能被执行一次,没有再次被救活的机会。
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有注意有两个参数:
MetaspaceSize :初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
为什么移除永久代?
移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
有了元空间就不再会出现永久代OOM问题了!
1.Generational 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了,对象就会晋级到老年代。
2.Mark-Sweep(标记-清除)算法
标记清除:标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。标记-清除算法主要是运用在Eden区,该区对象很容易被回收掉,回收率很高。
3.Copying(复制)算法
复制算法的使用在From区和To区,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
缺点:可使用内存缩减为一半大小。
那么复制算法使可使用内存大小会减半,设计上是怎么解决这个问题的呢。就是给From和To区划分尽可能小的区域。经过大数据统计之后,对象在第一次使用过后,绝大多数都会被回收,所以能进入第一次复制算法的对象只占10%。那么设计上,Eden、From、To区的比例是8:1:1,绝大多数对象会分配到Eden区,这样就解决了复制算法缩减可用内存带来的问题。
4.Mark-Compact (标记—整理)算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记—清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记—整理算法,与标记—清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。
垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现:
Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,
优点是简单高效;
Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器
的老年代版本;
ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器
的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行
收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿
的特点,追求最短 GC 回收停顿时间。
⑶ 三色标记法与垃圾回收器(CMS、G1)
JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。
三色标记法将对象的颜色分为了黑、灰、白,三种颜色。
存在问题:
浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,此时,此对象不是白色的不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,这种情况对系统的影响不大,留给下一次GC进行处理即可。
对象漏标问题(需要的对象被回收):并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。
在JVM虚拟机中有两种常见垃圾回收器使用了该算法:
CMS(Concurrent Mark Sweep)
CMS,是非常有名的JVM垃圾回收器,它起到了承上启下的作用,开启了并发回收的篇章。
但是CMS由于许多小问题,现在基本已经被淘汰。
增量更新(Increment Update)
在应对漏标问题时,CMS使用了Increment Update方法来做:
在一个未被标记的对象(白色对象)被重新引用后,==引用它的对象==,若为黑色则要变成灰色,在下次二次标记时让GC线程继续标记它的属性对象。
但是就算时这样,其仍然是存在漏标的问题:
在一个灰色对象正在被一个GC线程回收时,当它已经被标记过的属性指向了一个白色对象(垃圾)
而这个对象的属性对象本身还未全部标记结束,则为灰色不变
而这个GC线程在标记完最后一个属性后,认为已经将所有的属性标记结束了,将这个灰色对象标记为黑色,被重新引用的白色对象,无法被标记
补充,CMS除了这个缺陷外,仍然存在两个个较为致命的缺陷:
解决方案:使用Mark-Sweep-Compact算法,减少垃圾碎片
当JVM认为内存不够了,再使用CMS进行并发清理内存可能会发生OOM的问题,而不得不进行Serial Old GC,Serial Old是单线程垃圾回收,效率低
解决方案:降低触发CMS GC的阈值,让浮动垃圾不那么容易占满老年代
G1(Garbage First)
从G1垃圾回收器开始,G1的物理内存不再分代,而是由一块一块的Region组成;逻辑分代仍然存在。
前置知识 — Card Table(多种垃圾回收器均具备)
由于在进行YoungGC时,我们在进行对一个对象是否被引用的过程,需要扫描整个Old区,所以JVM设计了CardTable,将Old区分为一个一个Card,一个Card有多个对象;如果一个Card中的对象有引用指向Young区,则将其标记为Dirty Card,下次需要进行YoungGC时,只需要去扫描Dirty Card即可。
Card Table 在底层数据结构以 Bit Map实现。
CSet(Collection Set)
SATB(Snapshot At The Beginning)
在应对漏标问题时,CMS使用了SATB方法来做:
因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用,并配合Rset来判断当前对象是否被引用来进行回收;
并且在最后G1并不会选择回收所有垃圾对象,而是根据Region的垃圾多少来判断与预估回收价值(指回收的垃圾与回收的STW时间的一个预估值),将一个或者多个Region放到CSet中,最后将这些Region中的存活对象压缩并复制到新的Region中,清空原来的Region。
问题:G1会不会进行Full GC?
会,当内存满了的时候就会进行Full GC;且JDK10之前的Full GC,为单线程的,所以使用G1需要避免Full GC的产生。
解决方案:
加大内存;
提高CPU性能,加快GC回收速度,而对象增加速度赶不上回收速度,则Full GC可以避免;
降低进行Mixed GC触发的阈值,让Mixed GC提早发生(默认45%)
G1的第一篇paper(附录1)发表于2004年,在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。为何oracle要极力推荐G1呢,G1有哪些优点?
首先,G1的设计原则就是简单可行的性能调优
开发人员仅仅需要声明以下参数即可:
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。
其次,G1将新生代,老年代的物理空间划分取消了。
这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。
对象分配策略
说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:
对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。
在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。
但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。
需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
Young GC 阶段:
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
它的GC步骤分2步:
全局并发标记(global concurrent marking)
拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?
在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:
初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。
根对象被置为黑色,子对象被置为灰色。
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。
这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题
我们看下面一种情况,当垃圾收集器扫描到下面情况时:
这时候应用程序执行了以下操作:
这样,对象的状态图变成如下情形:
这时候垃圾收集器再标记扫描的时候就会下图成这样:
很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:
在插入的时候记录对象
在删除的时候记录对象
刚好这对应CMS和G1的2种不同实现方式:
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
至此,混合式GC告一段落了。下一小节我们讲进入调优实践。
MaxGCPauseMillis调优
前面介绍过使用GC的最基本的参数:
前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。
Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。
其他调优参数
避免使用以下参数:
避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。
触发Full GC
在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?
并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
由于篇幅有限,G1还有很多调优实践,在此就不一一列出了,大家在平常的实践中可以慢慢探索。最后,期待java 9能正式发布,默认使用G1为垃圾收集器的java性能会不会又提高呢?
G1处理和传统的垃圾收集策略是不同的,关键的因素是它将所有的内存进行了子区域的划分。
总结
G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过**首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。
参考链接:
https://juejin.cn/post/6859931488352370702
https://blog.csdn.net/qq_39276448/article/details/104470796
⑷ java gc中为什么复制算法比标记整理算法快
1、因为复制gc只需要把“活”的对象拷贝到survivor
2、复制算法:两个区域A和B,初始对象在A,继续存活的对象被转移到B。此为新生代最常用的算法
标记清理:一块区域,标记要回收的对象,然后回收,一定会出现碎片,那么引出
标记-整理算法:多了碎片整理,整理出更大的内存放更大的对象。
3、每次都是对其中的一块进行内存回收,没存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
⑸ JVM 技术详解:常见的 GC 算法(Parallel/CMS/G1)
学习了 GC 算法的相关概念之后,我们将介绍在 JVM 中这些算法的具体实现。首先要记住的是,大多数 JVM 都需要使用两种不同的 GC 算法——一种用来清理年轻代,另一种用来清理老年代。厅行
我们可以选择 JVM 内置的各种算法。如果不通过参数明确指定垃圾收集算法,则会使用相应 JDK 版本的默认实现。本章会详细介绍各种算法的实现原理。
串行 GC 对年轻代使用 mark-(标记—复制)算法,对老年代使用 mark-sweep-compact(标记—清除—整理)算法。
两者都是单线程的垃圾收集器,不能进行并行处理,所以都枣型会触发全线暂停(STW),停止所有的应用线程。
因此这种 GC 算法不能充分利用多核 CPU。不管有多少 CPU 内核,JVM 在垃圾收集时都只能使用单个核心。
要启用此款收集器,只需要指定一个 JVM 启动参数即可,同时对年轻代和老年代生效:
该选项只适合几百 MB 堆内存的 JVM,而且是单核 CPU 时比较有用。
对于服务器端来说,因为一般是多个 CPU 内核,并不推荐使用,除非确实需要限制 JVM 所使用的资源。大多数服务器端应用部署在多核平台上,选择 串行 GC 就意味着人为地限制了系统资源的使用,会导致资源闲置,多余的 CPU 资源也不能用增加业务处理凳伏猜的吞吐量。
关于串行垃圾收集器的日志内容,我们在后面的内容《GC 日志解读与分析》之中进行详细的讲解。
并行垃圾收集器这一类组合,在年轻代使用“标记—复制(mark-)算法”,在老年代使用“标记—清除—整理(mark-sweep-compact)算法”。年轻代和老年代的垃圾回收都会触发 STW 事件,暂停所有的应用线程来执行垃圾收集。两者在执行“标记和复制/整理”阶段时都使用多个线程,因此得名“ Parallel ”。通过并行执行,使得 GC 时间大幅减少。
通过命令行参数 -XX:ParallelGCThreads=NNN 来指定 GC 线程数,其默认值为 CPU 核心数。可以通过下面的任意一组命令行参数来指定并行 GC:
并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到更高的吞吐量:
⑹ java有哪些垃圾回收算法
常用的垃圾回收算法有:
(1).引用计数算法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
引用计数算法实现简单,效率很高,微软的COM技术、ActionScript、Python等都使用了引用计数算法进行内存管理,但是引用计数算法对于对象之间相互循环引用问题难以解决,因此java并没有使用引用计数算法。
(2).根搜索算法:
通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。
主流的商用程序语言C#、java和Lisp都使用根搜素算法进行内存管理。
在java语言中,可作为GC Root的对象包括以下几种对象:
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b.方法区中的类静态属性引用的对象。
c.方法区中的常量引用的对象。
d.本地方法栈中JNI本地方法的引用对象。
java方法区在Sun HotSpot虚拟机中被称为永久代,很多人认为该部分的内存是不用回收的,java虚拟机规范也没有对该部分内存的垃圾收集做规定,但是方法区中的废弃常量和无用的类还是需要回收以保证永久代不会发生内存溢出。
判断废弃常量的方法:如果常量池中的某个常量没有被任何引用所引用,则该常量是废弃常量。
判断无用的类:
(1).该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。
(2).加载该类的类加载器已经被回收。
(3).该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。
Java中常用的垃圾收集算法:
(1).标记-清除算法:
最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(2).复制算法:
将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法的缺点显而易见,可使用的内存降为原来一半。
(3).标记-整理算法:
标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。
复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。
(4).分代收集算法:
根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。
⑺ JVM有哪些垃圾回收算法
标记-清除,标记-复制,标记-整理
⑻ 你不得不知道的JVM 垃圾回收
一、四种引用方式
1.1 强引用
1.2 软引用(SoftReference)
1.3 弱引用(WeakReference)
1.4 虚引用(PhantomReference)
二、如何判断对象是垃圾
2.1 引用计数法
2.2 根可达性分析
三、垃圾回收算法
3.1 标记-清除(mark-sweep)
3.2 标记-整理(mark-compact)
3.3 标记-复制(mark-)
四、垃圾收集器
4.1 分类及特点简述
4.1.1 串行
4.1.2 吞吐量优先
4.1.3 响应时间优先
4.2 串行垃圾回收器详述
4.2.1 Serial
4.2.2 Serial-Old
4.2.3 流程图
4.3 吞吐量优先垃圾回收器详述
4.3.1 JVM相关参数
4.3.2 流程图
4.4、响应时间优先垃圾回收器详述
4.4.1 JVM相关参数
4.4.2 流程图
4.3.3 CMS的特点
五、G1垃圾回收器
5.1 相关JVM参数
5.2 特点
5.3 G1新生代垃圾回收
5.4 G1老年代垃圾回收
只有所有 GC Roots对象都不通过【强引用】引用该对象,该对象才可以被回收。
某个对象只要有一处引用关系,该对象的引用次数就加1,如果一个对象的引用次数为0,则说明该对象是垃圾。
优势:实现简单,效率较高
弊端:如果有一对对象之间形成了相互引用,但是这两个对象都已经没有被其它对象所引用了,正常情况下,这一对对象应该被作为垃圾回收掉,但是因为形成了相互引用导致无法被回收。
通过GC Root对象开始向下寻找,寻找不到的对象即说明没有被引用,那么这些没有被引用的对象被认定为垃圾。
目前,如下对象可以作为GC Root对象:
很好理解,即在GC的放生时候,先对所有对象进行根可达性分析,借此标记所有的垃圾对象;所有对象标记完毕之后会进行清理操作。
因此,总体来说,就是先标记再清除。
弊端;标记清除之后会产生大量不连续的内存碎片,碎片太多可能会导致程序运行过程中需要分配较大对象时,无法满足分配要求导致GC操作。
该回收算法操作过程基本等同于 标记-清除 算法只不过,第二步有点区别,该种方式会在清除的过程中进行 整理 操作,这是最大的不同。
优势:最终不会出现若干空间碎片而导致的空间浪费。
弊端:在整理过程中带来的计算不可小觑。
该种方式与前两种有较大的区别:
该种方式会将存储区分成两个部分,分别为From、To,其中From区域中可能存在着对象,而To区域始终为空,用做下一次接受数据做准备。
分别有两个指针指向这两个区域:From-Pointer、To-Pointer,
优点:这种算法非常适合早生夕死的对象
缺点:始终有一块内存区域是未使用的,造成空间的浪费。
特点:
特点:
特点:
JVM开关:-XX:+UseSerialGC = Serial + SerialOld
上图是:CMS垃圾回收器在老年代GC的工作流程图:
经过上面的文字分析,新生代的Region个数为所有Region个数的5%;这个数值其实是很小的,那么当新生代Region不够用的时候,JVM会划分更多的Region个数给新生代;
当新生代的Region个数占比所有Region个数超过 60% 时,就会进行一次新生代的垃圾回收。
新生代垃圾回收会造成STW。
具体的垃圾回收算法同其它几个新生代垃圾回收器一样,新生代都使用复制算法。
老年代垃圾回收触发机制与参数-XX:InitaingHeapOccupancyPercent有关。
但是需要注意的是:这一次的老年代回收,其实是一次混合垃圾回收,会同时清理新生代、老年代、Humongous。
与新生代回收算法一致,依然使用复制算法,但是垃圾回收的过程等同于老年代响应时间优先的CMS方式
流程分为:
⑼ java常见gc算法有哪些
1:标记—清除 Mark-Sweep
过程:标记可回收对象,进行清除
缺点:标记和清除效率低,清除后会产生内存碎片
2:复制算法
过程:将内存划分为相等的两块,将存活的对象复制到另一块内存,把已经使用的内存清理掉
缺点:使用的内存变为了原来的一半
进化:将一块内存按8:1的比例分为一块Eden区(80%)和两块Survivor区(10%)
每次使用Eden和一块Survivor,回收时,将存活的对象一次性复制到另一块Survivor上,如果另一块Survivor空间不足,则使用分配担保机制存入老年代
3:标记—整理 Mark—Compact
过程:所有存活的对象向一端移动,然后清除掉边界以外的内存
4:分代收集算法
过程:将堆分为新生代和老年代,根据区域特点选用不同的收集算法,如果新生代朝生夕死,则采用复制算法,老年代采用标记清除,或标记整理
面试的话说出来这四种足够了