jvm的gc一般情况下是jvm本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制gc,通过system.gc触发,还可以通过jmap来触发等,针对每个场景其实我们都可以写篇文章来做一个介绍,本文重点介绍下system.gc的原理
或许大家已经知道如下相关的知识
system.gc其实是做一次full gc
system.gc会暂停整个进程
system.gc一般情况下我们要禁掉,使用-xx:+disableexplicitgc
system.gc在cms gc下我们通过-xx:+explicitgcinvokesconcurrent来做一次稍微高效点的gc(效果比full gc要好些)
system.gc最常见的场景是rmi/nio下的堆外内存分配等
如果你已经知道上面这些了其实也说明你对system.gc有过一定的了解,至少踩过一些坑,但是你是否更深层次地了解过它,比如
为什么cms gc下-xx:+explicitgcinvokesconcurrent这个参数加了之后会比真正的full gc好?
它如何做到暂停整个进程?
堆外内存分配为什么有时候要配合system.gc?
如果你上面这些疑惑也都知道,那说明你很懂system.gc了,那么接下来的文字你可以不用看啦
先贴段代码吧(java.lang.system)
发现主要调用的是runtime里的gc方法(java.lang.runtime)
这里看到gc方法是native的,在java层面只能到此结束了,代码只有这么多,要了解更多,可以看方法上面的注释,不过我们需要更深层次地来了解其实现,那还是准备好进入到jvm里去看看
上面提到了runtime.gc是一个本地方法,那需要先在jvm里找到对应的实现,这里稍微提一下jvm里native方法最常见的也是最简单的查找,jdk里一般含有native方法的类,一般都会有一个对应的c文件,比如上面的java.lang.runtime这个类,会有一个runtime.c的文件和它对应,native方法的具体实现都在里面了,如果你有source,可能会猜到和下面的方法对应
其实没错的,就是这个方法,jvm要查找到这个native方法其实很简单的,看方法名可能也猜到规则了,java_pkgname_classname_methodname,其中pkgname里的"."替换成"_",这样就能找到了,当然规则不仅仅只有这么一个,还有其他的,这里不细说了,有机会写篇文章详细介绍下其中细节
上面的方法里是调用jvm_gc(),实现如下
看到这里我们已经解释其中一个疑惑了,就是<code>disableexplicitgc</code>这个参数是在哪里生效的,起的什么作用,如果这个参数设置为true的话,那么将直接跳过下面的逻辑,我们通过-xx:+ disableexplicitgc就是将这个属性设置为true,而这个属性默认情况下是true还是false呢
这里主要针对cmsgc下来做分析,所以我们上面看到调用了heap的collect方法,我们找到对应的逻辑
collect里一开头就有个判断,如果should_do_concurrent_full_gc返回true,那会执行collect_mostly_concurrent做并行的回收
其中should_do_concurrent_full_gc中的逻辑是如果使用cms gc,并且是system gc且explicitgcinvokesconcurrent==true,那就做并行full gc,当我们设置-xx:+ explicitgcinvokesconcurrent的时候,就意味着应该做并行full gc了,不过要注意千万不要设置-xx:+disableexplicitgc,不然走不到这个逻辑里来了
说到gc,这里要先提到vmthread,在jvm里有这么一个线程不断轮询它的队列,这个队列里主要是存一些vm_operation的动作,比如最常见的就是内存分配失败要求做gc操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(stw),整个进程相当于静止了
这里必须提到cms gc,因为这是解释并行full gc和正常full gc的关键所在,cms gc我们分为两种模式background和foreground,其中background顾名思义是在后台做的,也就是可以不影响正常的业务线程跑,触发条件比如说old的内存占比超过多少的时候就可能触发一次background式的cms gc,这个过程会经历cms gc的所有阶段,该暂停的暂停,该并行的并行,效率相对来说还比较高,毕竟有和业务线程并行的gc阶段;而foreground则不然,它发生的场景比如业务线程请求分配内存,但是内存不够了,于是可能触发一次cms gc,这个过程就必须是要等内存分配到了线程才能继续往下面走的,因此整个过程必须是stw的,因此cms gc整个过程都是暂停应用的,但是为了提高效率,它并不是每个阶段都会走的,只走其中一些阶段,这些省下来的阶段主要是并行阶段,precleaning、abortablepreclean,resizing这几个阶段都不会经历,其中sweep阶段是同步的,但不管怎么说如果走了类似foreground的cms gc,那么整个过程业务线程都是不可用的,效率会影响挺大。cms gc具体的过程后面再写文章详细说,其过程确实非常复杂的
正常的full gc其实是整个gc过程包括ygc和cms gc(这里说的是真正意义上的full gc,还有些场景虽然调用full gc的接口,但是并不会都做,有些时候只做ygc,有些时候只做cms gc)都是由vmthread来执行的,因此整个时间是ygc+cms gc的时间之和,其中cms gc是上面提到的foreground式的,因此整个过程会比较长,也是我们要避免的
并行full gc也通样会做ygc和cms gc,但是效率高就搞在cms gc是走的background的,整个暂停的过程主要是ygc+cms_initmark+cms_remark几个阶段
这里说的堆外内存主要针对java.nio.directbytebuffer,这些对象的创建过程会通过unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.directbytebuffer对象里,这样就可以直接操作这些内存。这些内存只有在directbytebuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发cms gc或者full gc,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-xx:maxdirectmemorysize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用system.gc来做一次full gc,以此来回收掉没有被使用的堆外内存,具体堆外内存是如何回收的,其原理机制又是怎样的,还是后面写篇详细的文章吧
该文章来自阿里巴巴技术协会(ata)精选集
个人公众号:
