從一塊兒GC血案談到反射原理

前言

首先回答一下提問者的問題。這主要是因爲存在大量反射而產生的臨時類加載器和 ASM 臨時生成的類,這些類會被保留在 Metaspace,一旦 Metaspace 即將滿的時候,就會觸發 FullGc,已達到回收再也不被使用的類對象的目的。具體問題請參考接下來的內容,更好的瞭解反射的實現原理。java

正文

概述

公司以前有個大內存系統(70G以上)一直使用CMS GC,不過由於該系統對時間很敏感,偶爾會由於gclocker致使remark特別長(雖然加了-XX:+CMSScavReengeBeforeRemark參數,可是gclocker會致使remark前的YGC被delay),沒法忍受這麼長的暫停就只好遷移到了G1,通過一系列的調優以後算比較穩定了,這套參數便推到了所有機器上緩存

但是就在上週忽然有機器出現了Full GC,原本G1設計出來就是但願Full GC不在出現,出現Full GC通常是不正常,GC日誌以下:數據結構

gc.jpg

從上面日誌不難發現是由於Perm觸發的Full GC,而且Full GC以後Perm就降下去了,不過須要提一下的是JDK7下正常的G1 GC是不會作類卸載的,只有Full GC的時候纔會卸載,但JDK8下是提供了相關參數的能夠在G1 GC某些階段作類卸載多線程

因而要業務方先作了coredump,保存好現場再重啓系統,而後再針對coredump作了heap dump,不過heapdump有40G這麼大,能夠經過jmap -permstat <executable java> core.xxx來看看究竟perm裏有什麼東西併發

這篇文章相對來講比較長,涉及到的知識點比較多,若是實在忍不住看下去,能夠跳到最後看下我對這個問題的描述再反過來看這篇文章或許讓你有更清晰的認識app

Perm裏究竟塞了什麼

既然是Perm滿了,那咱們得看Perm裏究竟放了什麼,咱們知道Perm裏主要存的是類的原始數據,好比咱們加載了一個類,那這個類的信息會在Perm裏分配內存來存儲它的一些數據結構,因此大部分狀況下,Perm的使用量和加載的類個數是關係很大的,固然Perm裏在低版本的時候還會存一些其餘的數據,好比String(String.intern()的狀況)。框架

另外經驗告訴咱們若是真的是Perm溢出,那有地方動態構建一個類加載器加載一個類的可能性會很大,經過上面的jmap命令,咱們能夠統計下sun.reflect.DelegatingClassLoader的個數竟然達到了415737個jvm

那基本能夠鎖定是反射類加載器致使Perm溢出的緣由了,那究竟爲何會有這麼多反射類加載器呢,反射類加載器又是什麼,接下來先簡單說下反射的原理ide

反射的原理

反射你們用起來很方便,因爲性能其實也比較不錯了,所以用得挺廣的,咱們一般這麼用反射性能

Method method = XXX.class.getDeclaredMethod(xx,xx);method.invoke(target,params)

不過這裏我不許備用大量的代碼來描述其原理,而是講幾個關鍵的東西,而後將他們串起來

獲取Method

要調用首先要獲取Method,而獲取Method的邏輯是經過Class這個類來的,而關鍵的幾個方法和屬性以下:

class.jpg

在Class裏有個關鍵的屬性叫作reflectionData,這裏主要存的是每次從jvm裏獲取到的一些類屬性,好比方法,字段等,大概長這樣

3.jpg

這個屬性主要是SoftReference的,也就是在某些內存比較苛刻的狀況下是可能被回收的,不過正常狀況下能夠經過-XX:SoftRefLRUPolicyMSPerMB這個參數來控制回收的時機,一旦時機到了,只要GC發生就會將其回收,那回收以後意味着再有需求的時候要從新建立一個這樣的對象,同時也須要從JVM裏從新拿一份數據,那這個數據結構關聯的Method,Field字段等都是從新生成的對象。若是是從新生成的對象那可能有什麼麻煩?講到後面就明白了

getDeclaredMethod方法其實很簡單,就是從privateGetDeclaredMethods返回的方法列表裏複製一個Method對象返回。而這個複製的過程是經過searchMethods實現的

若是reflectionData這個屬性的declaredMethods非空,那privateGetDeclaredMethods就直接返回其就能夠了,不然就從JVM裏去撈一把出來,並賦值給reflectionData的字段,這樣下次再調用privateGetDeclaredMethods時候就能夠用緩存數據了,不用每次調到JVM裏去獲取數據,由於reflectionData是Softreference,因此存在取不到值的風險,一旦取不到就又去JVM裏撈了

searchMethods將從privateGetDeclaredMethods返回的方法列表裏找到一個同名的匹配的方法,而後複製一個方法對象出來,這個複製的具體實現,其實就是Method.copy方法:

4.jpg

因而可知,咱們每次經過調用getDeclaredMethod方法返回的Method對象其實都是一個新的對象,因此不宜多調哦,若是調用頻繁最好緩存起來。不過這個新的方法對象都有個root屬性指向reflectionData裏緩存的某個方法,同時其methodAccessor也是用的緩存裏的那個Method的methodAccessor。

Method調用

有了Method以後,那就能夠調用其invoke方法了,那先看看Method的幾個關鍵信息

5.jpg

root屬性其實上面已經說了,主要指向緩存裏的Method對象,也就是當前這個Method對象實際上是根據root這個Method構建出來的,所以存在一個root Method派生出多個Method的狀況。

methodAccessor這個很關鍵了,其實Method.invoke方法就是調用methodAccessor的invoke方法,methodAccessor這個屬性若是root自己已經有了,那就直接用root的methodAccessor賦值過來,不然的話就建立一個

MethodAccessor的實現

MethodAccessor自己就是一個接口

6.jpg

其主要有三種實現

  • DelegatingMethodAccessorImpl

  • NativeMethodAccessorImpl

  • GeneratedMethodAccessorXXX

其中DelegatingMethodAccessorImpl是最終注入給Method的methodAccessor的,也就是某個Method的全部的invoke方法都會調用到這個DelegatingMethodAccessorImpl.invoke,正如其名同樣的,是作代理的,也就是真正的實現能夠是下面的兩種 7.jpg 若是是NativeMethodAccessorImpl,那顧名思義,該實現主要是native實現的,而GeneratedMethodAccessorXXX是爲每一個須要反射調用的Method動態生成的類,後的XXX是一個數字,不斷遞增的 而且全部的方法反射都是先走NativeMethodAccessorImpl,默認調了15次以後,才生成一個GeneratedMethodAccessorXXX類,生成好以後就會走這個生成的類的invoke方法了 那如何從NativeMethodAccessorImpl過分到GeneratedMethodAccessorXXX呢,來看看NativeMethodAccessorImpl的invoke方法 8.jpg 其中我上面說的是15次就是ReflectionFactory.inflationThreshold()這個方法返回的,這個15固然也不是一塵不變的,咱們能夠經過-Dsun.reflect.inflationThreshold=xxx來指定,咱們還能夠經過-Dsun.reflect.noInflation=true來直接繞過上面的15次NativeMethodAccessorImpl調用,和-Dsun.reflect.inflationThreshold=0的效果同樣的 而GeneratedMethodAccessorXXX都是經過new MethodAccessorGenerator().generateMethod來生成的,一旦建立好以後就設置到DelegatingMethodAccessorImpl裏去了,這樣下次Method.invoke就會調到這個新建立的MethodAccessor裏了。

那生成的GeneratedMethodAccessorXXX究竟長什麼樣呢,大概這樣了 9.jpg 其實就是直接調用目標對象的具體方法了,和正常的方法調用沒什麼區別

GeneratedMethodAccessorXXX的類加載器

那加載GeneratedMethodAccessorXXX的類加載器是什麼呢,在生成好了字節碼以後會調用下面的方法作類定義 10.jpg

因此GeneratedMethodAccessorXXX的類加載器實際上是一個DelegatingClassLoader類加載器

之因此搞一個新的類加載器,是爲了性能考慮,在某些狀況下能夠卸載這些生成的類,由於類的卸載是隻有在類加載器能夠被回收的狀況下才會被回收的,若是用了原來的類加載器,那可能致使這些新建立的類一直沒法被卸載,從其設計來看自己就不但願他們一直存在內存裏的,在須要的時候有就好了,在內存緊俏的時候能夠釋放掉內存

併發致使垃圾類建立

看到這裏不知道你們是否發現了一個問題,上面的NativeMethodAccessorImpl.invoke其實都是不加鎖的,那意味着什麼?若是併發很高的時候,是否是意味着可能同時有不少線程進入到建立GeneratedMethodAccessorXXX類的邏輯裏,雖說最終使用的其實只會有一個,可是這些開銷是否是已然存在了,假若有1000個線程都進入到建立GeneratedMethodAccessorXXX的邏輯裏,那意味着多建立了999個無用的類,這些類會一直佔着內存,直到能回收Perm的GC發生纔會回收

那到底是什麼方法在不斷反射呢

有了上面對反射原理的瞭解以後,咱們知道了在反射執行到必定次數以後,其實會動態構建一個類,在這個類裏會直接調用目標對象的對應的方法,咱們從heap dump裏看到了有大量的DelegatingClassLoader類加載器加載了GeneratedMethodAccessorXXX類,那這些類究竟是調用了什麼方法呢,因而咱們不得不作一件事,那就是將內存裏的這些類都dump下來,而後對字節碼作一個統計分析一下

運行時Dump類字節碼

咱們能夠利用SA的接口從coredump裏或者live進程裏將對應的類dump下來,爲了dump下來咱們特定的類,首先咱們寫一個Filter類

11.jpg

使用SA的jar($JAVA_HOME/lib/sa-jdi.jar)編譯好類以後,而後咱們在編譯好的類目錄下調用下面的命令進行dump

12.jpg

這樣咱們就能夠將全部的GeneratedMethodAccessor給dump下來了,這個時候咱們再經過javap -verbose GeneratedMethodAccessor9隨便看一個類的字節碼

12.jpg

看到上面關鍵的bci爲36的那行,這裏的方法即是咱們反射調用的方法了,好比上面的那個反射調用的方法就是org/codehaus/xfire/util/ParamReader.readCode

定位到具體的反射類及方法

dump出這些字節碼以後,咱們對這些全部的類的字節碼作一個統計,就找出了全部的反射調用方法,而後發現某些model類(package都是相同的)竟然產生了20多萬個類,這意味着有很是多的這些model類作反射

13.jpg

有了這個線索以後就去看代碼究竟哪裏會有調用這些model方法的反射邏輯,可是惋惜沒有找到,可是這種model對象極有可能在某種狀況下出現,那就是rpc反序列化的時候,最終詢問業務方是使用的Xfire的服務,而憑藉我多年框架開發積累的經驗,肯定Xfire就是經過反射的方式來反序列化對象的,具體代碼以下(org.codehaus.xfire.aegis.type.basic.BeanType.writeProperty):

鄭州哪家不孕不育醫院好:http://www.zztjby.com/

14.jpg

而javabean的PropertyDeor裏的get/set方法,其實自己就是SoftReference包裝的

14.jpg

看到這裏或許你們都明白了吧,前面也已經說了SoftReference是可能被GC回收掉的,時間一到在下次GC裏就會被回收,若是被回收了,那就要從新獲取,而後至關因而調用的新的Method對象的invoke方法,那調用次數一多,就會產生新的動態構建的類,而這份類會一直存到直到能夠回收Perm的GC

G1回收Perm

注意下業務系統使用的是JDK7的G1,而JDK7的G1對perm其實正常狀況下是不會回收的,只有在Full GC的時候纔會回收Perm,這就解釋了通過了屢次G1 GC以後,那些Softreference的對象會被回收,可是新產生的類其實並不會被回收,因此G1 GC越頻繁,那意味着SoftReference的對象越容易被回收(雖然正常狀況下是時間到了,可是若是gc不頻繁,即便時間到了,也會留在內存裏的),越容易被回收那就越容易產生新的類,直到Full GC發生

解決方案

  • 升級到jdk8,能夠在G1 GC過程當中對類作卸載

  • 換一個序列化協議,不走方法反射的,好比hessian

  • 調整SoftRefLRUPolicyMSPerMB這個參數變大,不過這個不能治本

總結

上面涉及的內容很是多,若是很少讀幾遍可能難以串起來,我這裏將這個問題發生的狀況大體描述一下:

這個系統在JDK7下使用G1,而這個版本的G1只有在Full GC的時候纔會對Perm裏的類作卸載,該系統由於大量的請求致使G1 GC發生很頻繁,同時該系統還設置了-XX:SoftRefLRUPolicyMSPerMB=0,那意味着SoftReference的生命週期不會跨GC週期,能很快被回收掉,這個系統存在大量的RPC調用,走的Xfire協議,對返回結果作反序列化的時候是走的Method.invoke的邏輯,而相關的method所以被SoftReference引用,所以很容易被回收,一旦被回收,那就建立一個新的Method對象,再調用其invoke方法,在調用到必定次數(15次)以後,就構建一個新的字節碼類,伴隨着GC的進行,同一個方法的字節碼類不斷構建,直到將Perm充滿觸發一次Full GC才得以釋放。

鄭州不孕不育正規醫院:http://jbk.39.net/yiyuanfengcai/tsyl_zztjyy/3029/

相關文章
相關標籤/搜索