您現在的位置是:首頁 > 藝術

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

由 程式碼小當家 發表于 藝術2021-05-10
簡介writeProperty):而javabean的PropertyDeor裡的getset方法,其實本身就是SoftReference包裝的看到這裡或許大家都明白了吧,前面也已經說了SoftReference是可能被GC回收掉的,時間一到

反射定律是誰發現的

前言

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

概述

公司之前有個大記憶體系統(70G以上)一直使用CMS GC,不過因為該系統對時間很敏感,偶爾會因為gclocker導致remark特別長(雖然加了-XX:+CMSScavReengeBeforeRemark引數,但是gclocker會導致remark前的YGC被delay),無法忍受這麼長的暫停就只好遷移到了G1,經過一系列的調優之後算比較穩定了,這套引數便推到了全部機器上

可是就在上週突然有機器出現了Full GC,本來G1設計出來就是希望Full GC不在出現,出現Full GC一般是不正常,GC日誌如下:

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

從上面日誌不難發現是因為Perm觸發的Full GC,並且Full GC之後Perm就降下去了,不過需要提一下的是JDK7下正常的G1 GC是不會做類解除安裝的,只有Full GC的時候才會解除安裝,但JDK8下是提供了相關引數的可以在G1 GC某些階段做類解除安裝

於是要業務方先做了coredump,儲存好現場再重啟系統,然後再針對coredump做了heap dump,不過heapdump有40G這麼大,可以透過jmap -permstat core。xxx來看看究竟perm裡有什麼東西

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

Perm裡究竟塞了什麼

既然是Perm滿了,那我們得看Perm裡究竟放了什麼,我們知道Perm裡主要存的是類的原始資料,比如我們載入了一個類,那這個類的資訊會在Perm裡分配記憶體來儲存它的一些資料結構,所以大部分情況下,Perm的使用量和載入的類個數是關係很大的,當然Perm裡在低版本的時候還會存一些其他的資料,比如String(String。intern()的情況)。

另外經驗告訴我們如果真的是Perm溢位,那有地方動態構建一個類載入器載入一個類的可能性會很大,透過上面的jmap命令,我們可以統計下sun。reflect。DelegatingClassLoader的個數居然達到了415737個

那基本可以鎖定是反射類載入器導致Perm溢位的原因了,那究竟為什麼會有這麼多反射類載入器呢,反射類載入器又是什麼,接下來先簡單說下反射的原理

反射的原理

反射大家用起來很方便,由於效能其實也比較不錯了,因此用得挺廣的,我們通常這麼用反射

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

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

獲取Method

要呼叫首先要獲取Method,而獲取Method的邏輯是透過Class這個類來的,而關鍵的幾個方法和屬性如下:

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

在Class裡有個關鍵的屬性叫做reflectionData,這裡主要存的是每次從jvm裡獲取到的一些類屬性,比如方法,欄位等,大概長這樣

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

這個屬性主要是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方法:

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

由此可見,我們每次透過呼叫getDeclaredMethod方法返回的Method物件其實都是一個新的物件,所以不宜多調哦,如果呼叫頻繁最好快取起來。不過這個新的方法物件都有個root屬性指向reflectionData裡快取的某個方法,同時其methodAccessor也是用的快取裡的那個Method的methodAccessor。

Method呼叫

有了Method之後,那就可以呼叫其invoke方法了,那先看看Method的幾個關鍵資訊

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

root屬性其實上面已經說了,主要指向快取裡的Method物件,也就是當前這個Method物件其實是根據root這個Method構建出來的,因此存在一個root Method派生出多個Method的情況。

methodAccessor這個很關鍵了,其實Method。invoke方法就是呼叫methodAccessor的invoke方法,methodAccessor這個屬性如果root本身已經有了,那就直接用root的methodAccessor賦值過來,否則的話就建立一個

MethodAccessor的實現

MethodAccessor本身就是一個介面

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

其主要有三種實現

DelegatingMethodAccessorImpl

NativeMethodAccessorImpl

GeneratedMethodAccessorXXX

其中DelegatingMethodAccessorImpl是最終注入給Method的methodAccessor的,也就是某個Method的所有的invoke方法都會呼叫到這個DelegatingMethodAccessorImpl。invoke,正如其名一樣的,是做代理的,也就是真正的實現可以是下面的兩種

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

如果是NativeMethodAccessorImpl,那顧名思義,該實現主要是native實現的,而GeneratedMethodAccessorXXX是為每個需要反射呼叫的Method動態生成的類,後的XXX是一個數字,不斷遞增的 並且所有的方法反射都是先走NativeMethodAccessorImpl,預設調了15次之後,才生成一個GeneratedMethodAccessorXXX類,生成好之後就會走這個生成的類的invoke方法了 那如何從NativeMethodAccessorImpl過度到GeneratedMethodAccessorXXX呢,來看看NativeMethodAccessorImpl的invoke方法

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

其中我上面說的是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究竟長什麼樣呢,大概這樣了

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

其實就是直接呼叫目標物件的具體方法了,和正常的方法呼叫沒什麼區別

GeneratedMethodAccessorXXX的類載入器

那載入GeneratedMethodAccessorXXX的類載入器是什麼呢,在生成好了位元組碼之後會呼叫下面的方法做類定義

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

所以GeneratedMethodAccessorXXX的類載入器其實是一個DelegatingClassLoader類載入器

之所以搞一個新的類載入器,是為了效能考慮,在某些情況下可以解除安裝這些生成的類,因為類的解除安裝是隻有在類載入器可以被回收的情況下才會被回收的,如果用了原來的類載入器,那可能導致這些新建立的類一直無法被解除安裝,從其設計來看本身就不希望他們一直存在記憶體裡的,在需要的時候有就行了,在記憶體緊俏的時候可以釋放掉記憶體

併發導致垃圾類建立

看到這裡不知道大家是否發現了一個問題,上面的NativeMethodAccessorImpl。invoke其實都是不加鎖的,那意味著什麼?如果併發很高的時候,是不是意味著可能同時有很多執行緒進入到建立GeneratedMethodAccessorXXX類的邏輯裡,雖然說最終使用的其實只會有一個,但是這些開銷是不是已然存在了,假如有1000個執行緒都進入到建立GeneratedMethodAccessorXXX的邏輯裡,那意味著多建立了999個無用的類,這些類會一直佔著記憶體,直到能回收Perm的GC發生才會回收

那究竟是什麼方法在不斷反射呢

有了上面對反射原理的瞭解之後,我們知道了在反射執行到一定次數之後,其實會動態構建一個類,在這個類裡會直接呼叫目標物件的對應的方法,我們從heap dump裡看到了有大量的DelegatingClassLoader類載入器載入了GeneratedMethodAccessorXXX類,那這些類到底是呼叫了什麼方法呢,於是我們不得不做一件事,那就是將記憶體裡的這些類都dump下來,然後對位元組碼做一個統計分析一下

執行時Dump類位元組碼

我們可以利用SA的介面從coredump裡或者live程序裡將對應的類dump下來,為了dump下來我們特定的類,首先我們寫一個Filter類

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

使用SA的jar($JAVA_HOME/lib/sa-jdi。jar)編譯好類之後,然後我們在編譯好的類目錄下呼叫下面的命令進行dump

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

這樣我們就可以將所有的GeneratedMethodAccessor給dump下來了,這個時候我們再透過javap -verbose GeneratedMethodAccessor9隨便看一個類的位元組碼

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

看到上面關鍵的bci為36的那行,這裡的方法便是我們反射呼叫的方法了,比如上面的那個反射呼叫的方法就是org/codehaus/xfire/util/ParamReader。readCode

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

dump出這些位元組碼之後,我們對這些所有的類的位元組碼做一個統計,就找出了所有的反射呼叫方法,然後發現某些model類(package都是相同的)居然產生了20多萬個類,這意味著有非常多的這些model類做反射

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

有了這個線索之後就去看程式碼究竟哪裡會有呼叫這些model方法的反射邏輯,但是可惜沒有找到,但是這種model物件極有可能在某種情況下出現,那就是rpc反序列化的時候,最終詢問業務方是使用的Xfire的服務,而憑藉我多年框架開發積累的經驗,確定Xfire就是透過反射的方式來反序列化物件的,具體程式碼如下(org。codehaus。xfire。aegis。type。basic。BeanType。writeProperty):

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

而javabean的PropertyDeor裡的get/set方法,其實本身就是SoftReference包裝的

我暈,又來GC血案!這下我算是懂啥是“反射原理”了

看到這裡或許大家都明白了吧,前面也已經說了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才得以釋放

作者:PerfMa

連結:https://juejin。im/post/5e8ed12bf265da47d00a58f8

推薦文章