新生代GC案例
若系统不停运行,然后把Eden给搞满:
此时必然触发Minor GC,有专门GC线程执行GC,且对不同内存区域有不同垃圾回收器,这相当于GC线程和垃圾回收器配合,使用自己的GC算法对指定内存区域执GC:
垃圾回收一定会通过一个后台运行的GC线程,如针对新生代用ParNew垃圾回收器,其针对新生代采用复制算法:标记Eden区中的存活对象,然后全部转移到Survivor1,然后一次性清空Eden中垃圾对象:
接着系统继续运行,新对象继续分配在Eden:
当Eden再满,又触发Minor GC,此时已然是GC线程运行垃圾回收器中的算法逻辑:采用复制算法逻辑,标记Eden和Survivor1中的存活对象。
然后一次性把存活对象转移到Survivor2,接着把Eden和Survivor1中的垃圾对象都回收:
GC过程中,还能创建新对象吗?
我们假设允许:
如上所示,若一边垃圾回收器在想法把Eden和Survivor2里存活对象标记出来转移到Survivor1,然后还得想法把Eden 和Survivor2里的垃圾对象都清掉,结果这时系统程还在不停往Eden创建新对象。这些新对象有的又很快成垃圾对象,现在咋办?乱套了,对程序新创建的这些对象,你怎么让垃圾回收器去持续追踪这些新对象的状态呢?
如何才能在这次GC过程中:
把新对象中的那些存活对象转移到Survivor2?
如何把新创建的对象中的垃圾都给回收?
有的同学说,那是垃圾回收器的事啊!你得好好设计!
有些事情想着很简单,但是一旦你要在JVM实现必然很复杂。所以说,在GC过程中,同时还允许我们写的Java系统继续不停的运行在Eden里持续创建新的对象,目前看很不合适。
JVM的痛点:Stop the World
因为GC时,尽可能要让垃圾回收器专心工作,不能随便让我们写的Java系统继续新建对象了,所以此时JVM会在后台直接进入“Stop the World”:停止Java系统所有工作线程,让我们写的代码无法再运行!然后让GC线程能安心执行GC:
这就能让我们的系统暂停运行,不再创建新对象,同时让GC线程尽快完成GC工作:标记和转移Eden及Survivor2的存活对象到Survivor1,然后快速地一次性回收掉Eden和Survivor2中的垃圾对象:
GC完毕,即可继续恢复Java系统的工作线程运行,继续在Eden创建新对象:
Stop the World造成的系统停顿
假设Minor GC要运行100ms,则可能导致系统直接停顿100ms,不能处理任何请求。这100ms期间用户发起的所有请求都会出现短暂卡顿,因为系统工作线程不在运行,不能处理请求。假设是Web系统,可能导致你的用户从网页或APP点击一个按钮,平时只要几十ms就可以返回响应,现在因为Web系统JVM正在执行Minor GC,暂停所有工作线程,导致你的请求过来到响应返回,这次需等待几百ms。
因为内存分配不合理,导致对象频繁进入老年代,平均7、8min一次Full GC,而Full GC最慢,有时一次回收要进行几s甚至几十s。此时一旦频繁Full GC,你的系统每隔几min就会卡死几十s。这让用户体验极差。所以无论新生代GC还是老年代GC,都尽量不能频繁,也要避免持续时间过长。
不同的垃圾回收器的不同的影响
新生代的回收,Serial垃圾回收器就是用1个线程进行垃圾回收,然后此时暂停系统工作线程,所以一般服务器程序中很少用。平时常用的新生代垃圾回收器是ParNew,其针对服务器一般都是多核CPU有优化,支持多线程GC,可大幅提升回收性能,缩短GC时间。
大致原理:
CMS也是基于多线程的,且可使用一套独特的机制尽可能的在垃圾回收的过程中减少“Stop the World”的时间,避免长时间系统卡死。
G1更是将采用复杂的回收机制将回收性能优化到机制,尽可能更多的降低“Stop the World”的时间。JVM本身迭代演进,就是不断优化垃圾回收器机制和算法,尽可能降低垃圾回收的过程对系统运行的影响。
对于我们这些 crud boy,就得合理对线程系统优化内存分配和垃圾回收,尽可能减少垃圾回收的频率,降低垃圾回收的时间,减少垃圾回收对系统运行的影响。JVM优化也就是这样。