垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收?
垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战
在早期的C/C++时代,垃圾回收基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放。比如以下代码:
MibBridge *pBridge = new cmBaseGroupBridge() ;
//如果注册失败,使用Delete释放该对象所占内存区域
if (pBridge->Register( kDestroy) != NO_ERROR)
delete pBridge;
这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃
在有了垃圾回收机制后,上述代码块极有可能变成这样:
MibBridge *pBridge = new cmBaseGroupBridge();
pBridge->Register(kDestroy) ;
也叫根搜索算法、追踪性垃圾收集
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生
相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)
所谓"GC Roots"根集合就是一组必须活跃的引用。
基本思路:
可达性分析算法是以根对象集合(c Rbots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
在Java语言中,GC Roots包括以下几类元素:
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)。
小技巧:
背景:
执行过程:
缺点:
注意︰何为清除?
背景:
核心思想:
优点:
缺点:
特别的:
应用场景:
简单介绍
基本思想
缺点:
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一
由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况
大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用
javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存
首先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:
这里隐含着一层意思是,在抛出outOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间
例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
在javag nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。
当然,也不是在任何情况下垃圾收集器都会被触发的
stop-the-world ,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW
被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生
STW事件和采用哪款cc无关,所有的Gc都有这个事件
哪怕是G1也不能完全避免stop-the-world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
开发中不要用System.gc();会导致Stop-the-world的发生。
发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
串行(serial)
相较于并行的概念,单线程执行。
如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,,这些位置称为“安全点(Safepoint) ”
Safe Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据**“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等**
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的 Safepoint 。但是,程序“不执行”的时候呢?例如线程处于Sleep状态或Blocked 状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域 (Safe Region) 来解决。
**安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始cc都是安全的。**我们也可以把 safe Region看做是被扩展了的safepoint。
实际执行时:
我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象。
【既偏门又非常高频的面试题】强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
Reference子类中只有终结器引用是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用
软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。【第一次回收:回收不可达的对象,不可触及的对象】
软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列(Reference Queue)
类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。
在JDK 1.2版之后提供了java.lang.ref.softReference类来实现软引用。
Object obj = new Object(); //声明强引用
SoftReference<Object> sf = new SoftReference<Object>(obj); //此时既有强引用关联,也有软引用关联,所有要断开强引用关联
obj = null; //销毁强引用
弱引用也是用来描述那些非必需对象,**只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。**在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。
**软引用、弱引用都非常适合来保存那些可有可无的缓存数据。**如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收
也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。
一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
在JDK 1.2版之后提供了PhantomReference类来实现虚引用。
Object obj = new Object(); //声明强引用
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> pf = new PhantomRefereence<Object>(obj,phantomQueue);
obj = null; //销毁强引用
1、按线程数分,可以分为串行垃圾回收器和并行垃圾回收器
串行回收指的是在同一时间段内只允许有一个cPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中
在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world”机制
2、按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器
3、按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器
4、按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。
吞吐量优先,意味着在单位时间内,STW的时间最短:0.2 + 0.2 = 0.4
Java常见的垃圾收集器有哪些?
https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The world)
**优势:**简单而高效[ (与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生,使用串行回收器是可以接受的。
在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。
总结:
对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
由于ParNew收集器是基于并行回收,那么是否可以断定ParNew收集器的回收效率在任何场景下都会比serial收集器更高效?
因为除serial外,目前只有ParNew Gc能与CMS收集器配合工作
开启:
HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同样也采用了复制算法、并行回收和"stopthe world"机制。
那么Parallel收集器的出现是否多此一举?
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel old收集器,用来代替老年代的Serial old收集器。
Parallel old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-world"机制。
**-XX:+UseParallelGC:**手动指定年轻代使用Parallel并行收集器执行内存回收任务。
**-XX:+UseParalleloldGC:**手动指定老年代都是使用并行回收收集器。
**-XX:ParallelGCThreads:**设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
在默认情况下,当CPU数量小于8个,ParallelGCThreads 的值等于CPU数量。
当CPU数量大于8个,ParallelGCThreads 的值等于3+[5*CPU_Count]/8]
**-XX:MaxGCPauseMillis:**设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒。
**-XX:GCTimeRatio:**垃圾收集时间占总时间的比例(= 1 / (N + 1) )。用于衡量吞吐量的大小。
-XX:+UseAdaptivesizePolicy:设置Parallel Scavenge收集器具有自适应调节策略
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段:初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段
尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-world”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“stop-the-wor1d”,只是尽可能地缩短暂停时间。
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次**“Concurrent Mode Failure”**失败,这时虚拟机将启动后备预案:临时启用Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。
有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
**-XX:+UseConcMarkSweepGC:**手动指定使用CMS 收集器执行内存回收任务
**-XX:CMSlnitiatingoccupanyFraction:**设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数
**-XX:+UseCMsCompactAtFullcollection:**用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了
**-XX:CMSFullGCsBeforeCompaction:**设置在执行多少次Full GC后对内存空间进行压缩整理
**-XX:ParallelCMsThreads:**设置CMS的线程数量
CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕
JDK9新特性:CMS被标记为Deprecate了(JEP291)
JDK14新特性:删除CMS垃圾回收器(JEP363)
与其他 GC收集器相比,G1使用了全新的分区算法,其特点如下所示:
1、并行与并发
2、分代收集
3、空间整合
4、可预测的停顿时间模型(即:软实时soft real-time)
一个region有可能属于Eden,Survivor或者 Old/Tenured 内存区域。但是一个region只可能属于一个角色。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区域。图中空白的表示未使用的内存空间
G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的 H块 。主要用于存储大对象,如果超过1.5个region,就放到H
设置H的原因:
年轻代GC过程
第一阶段,扫描根
第二阶段,更新RSet
第三阶段,处理RSet
第四阶段,复制对象
第五阶段,处理引用
对于应用程序的引用赋值语句object.field=object, JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。在年经代回收的时候,G1会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。
那为什么不在引用赋值语句处直接更新RSet呢?
**1、初始标记阶段:**标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC
**2、根区域扫描(Root Region Scanning):**G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成。
**3、并发标记(Concurrent Marking):**在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,**若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。**同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
**4、再次标记(Remark):**由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)
**5、独占清理(cleanup,STW):**计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是STW的
**6、并发清理阶段:**识别并清理完全空闲的区域
如果想把GC日志存到文件的话,是下面这个参数:
日志补充说明:
“[GC"和”[Full GC"说明了这次垃圾收集的停顿类型,如果有"Full"则说明GC发生了"Stop The world"
使用Serial收集器在新生代的名字是Default New Generation,因此显示的是"[DefNew"
使用ParNew收集器在新生代的名字会变成"[ParNew",意思是"Parallel New Generation"
使用Parallel Scavenge收集器在新生代的名字是"[PSYoungGen"
老年代的收集和新生代道理一样,名字也是收集器决定的
使用G1收集器的话,会显示为"garbage-first heap"
Allocation Failure
[PsYoungGen:5986K->696K(8704K) ]5986K->704K(9216K)
user代表用户态回收耗时,sys内核态回收耗时,rea实际耗时。由于多核的原因,时间总和可能会超过real时间
现在G1回收器已成为默认回收器好几年了。
我们还看到了引入了两个新的收集器:
ZGC (JDK11出现)和shenandoah(open JDK12)
Open JDK12的shenandoah GC:低停顿时间的GC(实验性)
Shenandoah,无疑是众多GC中最孤独的一个。是第一款不由oracle公司团队领导开发的HotSpot垃圾收集器。不可避免的受到官方的排挤。比如号称openJDK和oracleJDK没有区别的oracle公司仍拒绝在oracle JDK12中支持Shenandoah。
shenandoah垃圾回收器最初由RedHat进行的一项垃圾收集器研究项目Pauseless GC的实现,旨在针对JVM上的内存回收实现低停顿的需求。在2014年贡献给OpenJDK。
Red Hat研发shenandoah团队对外宣称,Shenandoah垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为200 MB还是200GB,99.9%的目标都可以把垃圾收集的停顿时间限制在十毫秒以内。不过实际使用性能将取决于实际工作堆的大小和工作负载。
这是RedHat在2016年发表的论文数据,测试内容是使用Es对200GB的维基百科数据进行索引。从结果看:
停顿时间比其他几款收集器确实有了质的飞跃,但也未实现最大停顿时间控制在十毫秒以内的目标。而吞吐量方面出现了明显的下降,总运行时间是所有测试收集器里最长的。
总结:
https://docs.oracle.com/en/java/javase/12/gctuning/
AliGC是阿里巴巴JVM团队基于G1算法,面向大堆(LargeHeap)应用场景。
当然,其他厂商也提供了各种独具一格的Gc实现,例如比较有名的低延迟GC,Zing (https://www.infoq.com/articles/azul_gc_in_detail)