JVM垃圾回收(四)

垃圾回收概念

什么是垃圾回收

JVM中自动检测并移除不再使用的数据对象的这种机制称为:垃圾回收(Garbage Collection),简称GC。

GC的基本原理

JVM通过使用垃圾收集器及使用相应的垃圾回收算法将内存中不再被使用的对象进行回收。

为什么要垃圾回收

由于不同Java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行垃圾回收,整个程序会因内存耗尽导致整个程序崩溃。垃圾回收还会整理那些零散的内存碎片,碎片过多最直接的问题就是会导致无法分配大块的内存空间以及降低程序的运行效率。

哪些区域会被GC

VM栈、本地方法栈以及程序计数器会随方法或线程的结束而自然被回收,所以这些区域不需要考虑回收问题。堆空间和持久代(方法区)是GC回收的重点区域,不同区域对象的收集叫法不一样。

  • 对年轻代的对象的收集称为minor GC(eden:minor gc,整个新生代:major gc)。
  • 对老年代的对象的收集称为Full GC。程序中主动调用System.gc()强制执行的GC为Full GC。Full GC基本都是整个堆空间及持久代发生了垃圾回收,通常优化的目标之一是尽量减少GC和Full GC的频率。
  • 持久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收(Full GC)。

垃圾收集算法

标记-清除算法

最基础的收集算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象,之所以说它是最基础的收集算法,是因为很多收集算法都是基于这种思路并对其缺点进行改进而的。
它的主要缺点有两个:一个是效率问题,标记和清楚的效率都不高;另一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致,当程序在以后运行过程中需要分配较大对象是无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

复制算法

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

标记-整理算法

由于复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低,更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法(如果100%存活,老年代没有内存空间了,就没有办法按照复制算法来回收)。根据老年代的特点,有人提出了另外一种“标记-整理”算法,标记过程仍然与“标记-清楚”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现在大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现,不同的垃圾收集器有不同的内存回收算法(引用计数、标记-清除算法、复制算法、标记-整理算法等)。 JVM规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的JVM所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

垃圾收集器

串行收集器(Serial Collector)

使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是它在进行垃圾收集时,必须暂停其它所有的工作线程,直到它收集结束,这对很多应用都是难以接受的,并且也无法使用多处理器的优势,所以此收集器比较适合用在单处理器机器上。这类收集器有:Serial、Serial Old等。
使用-XX:+UseSerialGC,策略为年轻代串行复制,老年代串行标记整理。
垃圾回收简易图。

吞吐量优先的并行收集器(Throughput Collector)

并行收集器可以使用多条线程进行垃圾收集。常用于对年轻代或老年代进行并行垃圾回收,能显著减少垃圾回收时间,一般会在多线程多处理器机器上使用。这类收集器有:ParNew,Parallel Scavenge,Parallel Old等。
使用-XX:+UseParallelGC,也是JDK5 -server的默认值。已默认无需配置的参数:-XX:+UseAdaptiveSizePolicy(动态调整新生代大小)。
垃圾回收简易图。

策略

年轻代暂停应用程序

多个垃圾收集线程并行的复制收集,线程数默认为CPU个数,CPU很多时,可用–XX:ParallelGCThreads=减少线程数

年老代暂停应用程序

与串行收集器一样,单垃圾收集线程标记整理。所以需要2+的CPU时才会优于串行收集器,适用于后台处理,科学计算。可以使用-XX:MaxGCPauseMillis=-XX:GCTimeRatio来调整GC的时间。

暂停时间优先的并发收集器(Concurrent Low Pause Collector-CMS)

并发收集器主要以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,可以提高服务的响应速度,减少系统停顿的时间,以给用户带来较好的体验,此收集器适合对响应时间要求比较高的中、大规模应用。这类收集器有:CMS等。
采用CMS时候,新生代必须使用Serial GC或者ParNew GC两种。CMS共有七个步骤,只有Initial Marking和Final Marking两个阶段是stop-the-world(暂停应用)的,其他步骤均和应用并行进行。
持久代的GC也采用CMS,通过-XX:CMSPermGenSweepingEnabled -XX:CMSClassUnloadingEnabled来制定。在采用CMS GC的情况下,Young Generation变慢的原因通常是由于Old Generation出现了大量的碎片。老年代GC慢了,会影响年轻代。
使用-XX:+UseConcMarkSweepGC,已默认无需配置的参数:-XX:+UseParNewGC(Parallel收集新生代),-XX:+CMSPermGenSweepingEnabled(CMS收集持久代) ,-XX:UseCMSCompactAtFullCollection(Full GC时压缩年老代)。

策略

年轻代同样是暂停应用程序

多个垃圾收集线程并行的复制收集。此时年轻代的CMS垃圾收集算法和Serial/Parallel GC一样,需要暂停应用,进行垃圾收集。

年老代则只有两次短暂停

其他时间应用程序与收集线程并发的清除,来回收以减少应用暂停时间。
注:以上提到的这些垃圾收集器都可以通过jvm参数配置来指定该jvm使用那一种垃圾收集器,例如使用-XX:+UseSerialGC可以打开使用Serial作为垃圾收集器进行内存回收。

并行(Parallel)与并发(Concurrent)区别

  • 并行:指多条垃圾收集线程并行。
  • 并发:指用户线程与垃圾收集线程并发,程序在继续运行,而垃圾收集程序运行于另一个个CPU上。

    老年代

    并发收集一开始会很短暂的停止一次所有线程来开始初始标记根对象,然后标记线程与应用线程一起并发运行,最后又很短的暂停一次,多线程并行的重新标记之前可能因为并发而漏掉的对象,然后就开始与应用程序并发的清除过程。可见,最长的两个遍历过程都是与应用程序并发执行的,比以前的串行算法改进太多太多了!这就是CMS算法的核心流程。
    串行标记清除是等年老代满了再开始收集的,而并发收集因为要与应用程序一起运行,如果满了才收集,应用程序就无内存可用,所以系统默认68%满的时候就开始收集。内存已设得较大,吃内存又没有这么快的时候,可以用-XX:CMSInitiatingOccupancyFraction=恰当增大该比率

    年轻代

    年轻代的复制收集,依然必须停止所有应用程序线程,原理如此,只能靠多CPU,多收集线程并发来提高收集速度,但除非你的Server独占整台服务器,否则如果服务器上本身还有很多其他线程时,切换起来速度就….. 所以,搞到最后,暂停时间的瓶颈就落在了年轻代的复制算法上。
    因此Young的大小设置挺重要的,大点就不用频繁GC,而且增大GC的间隔后,可以让多点对象自己死掉而不用复制了。但Young增大时,GC造成的停顿时间攀升得非常恐怖,比如在我的机器上,默认8M的Young,只需要几毫秒的时间,64M就升到90毫秒,而升到256M时,就要到300毫秒了,峰值还会攀到恐怖的800ms。谁叫复制算法,要等Young满了才开始收集,开始收集就要停止所有线程呢。

    垃圾回收类型

  • Minor GC:Young Generation当新创建对象,内存空间不够的时候,就会执行这个垃圾回收。由于执行最频繁,因此一般采用复制回收机制。
  • Major GC:清理Old Generation的内存,这里一般采用的是标记清除+标记整理机制。
  • Full GC:全面的垃圾回收。

    HotSpot垃圾回收机制

    垃圾收集简易流程图

    垃圾收集器:Young->Old简易流程图

    伊甸园区 Eden

    启动Java程序时,JVM随之启动,并将JDK的类和接口以及Java程序运行时需要的类和接口及编译后的Class文件或JAR包中的Class文件装载到JVM的方法区,在Eden中创建JVM、程序运行时必须的Java对象,当Eden的空间不足以用来创建新Java对象的时候,JVM的垃圾回收器执行对Eden区的垃圾回收工作,销毁那些不再被其他对象引用的Java对象,并将那些被其他对象所引用的Java对象(未被回收的对象)移动到幸存者0区。

    幸存者0区和幸存者1区 Survivor0 Survivor1

    如果幸存者0区有足够空间存放则直接放到幸存者0区;如果幸存者0区没有足够空间存放,则JVM的垃圾回收器执行对幸存者0区的垃圾回收工作,销毁那些不再被其他对象引用的Java对象,并将那些被其他对象所引用的Java对象移动到幸存者1区。 如果幸存者1区有足够空间存放则直接放到幸存者1区;如果幸存者1区没有足够空间存放,则JVM的垃圾回收器执行对幸存者1区的垃圾回收工作,销毁那些不再被其他对象引用的Java对象,并将那些被其他对象所引用的Java对象移动到年老区。

    老年代 Tenured Generation

    如果年老区有足够空间存放则直接放到年老区;如果年老区没有足够空间存放,则JVM的垃圾回收器执行对年老区的垃圾回收工作,销毁那些不再被其他对象引用的Java对象,并保留那些被其他对象所引用的Java对象。如果到最后年老区,幸存者1区,幸存者0区和伊甸园区都没有空间的话,则JVM会报告:“JVM堆空间溢出(java.lang.OutOfMemoryError:javaheap space)”,也即是在堆空间没有空间来创建对象。

    方法区(持久代) Perm Generation

    JVM的规范中没有规定必须实现永久代的垃圾收集。也就是说,不一定必须实现。而且永久代的垃圾回收”性价比”很低,新生代进行一次gc,一般可以回收70%-95%,但是永久代远低于此。永久代的垃圾收集主要分两个部分:废弃常量和无用的类。如果一个应用装载的class类比较多,永久代分配内存小的话,也会出现”永久存储区溢出(java.lang.OutOfMemoryError:java Permanent Space)”。
# JVM, Java

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×