JVM記憶體管理的一些思考
這個文章主要是自己關於jvm記憶體的一點思考,範圍比較雜,設計類載入器,方法區和記憶體洩露等
一、 記憶體是怎麼分配的
主要是指標碰撞和空閒列表兩類。新生代一般是複製演算法,老年代一般是標記整理(cms用了標記清除導致記憶體碎片較多)。複製和標記整理採用指標碰撞,標記清除採用標記清除。如果是指標碰撞需要考慮指標的衝突,有cas和本地執行緒分配緩衝兩種策略。
二、 方法區
方法區包括 常量池(jdk7被移入堆中),程式碼,類資訊,類靜態變數。jdk8後方法區實現為本地記憶體,受本機實體記憶體影響。
三、 java物件的生命週期
建立物件(如果物件的型別沒有載入則載入),使用物件,不可達,被標記,如果實現了finalize進入終結佇列,回收
四、 Class物件是在方法區還是堆中
深入理解java虛擬機器p215,Class是在方法區中,作為程式訪問方法區的入口。這一點其實沒有太大的意義,但是知道類的靜態欄位存在在方法區中,在類載入的準備階段初始化記憶體是很重要的。
五、java物件的大小
物件由物件頭,資料和對其對齊填充位元組。主要考慮的是物件頭,物件頭包含一個指向對應Class的指標和物件資訊。32位下,兩者都是4位元組,共8位元組。64位下兩者都是8位元組共16位元組,如果開啟了指標壓縮那麼Class指標為4位元組共12位元組。如果是陣列則多4個位元組表明陣列的長度。
六、 類載入的初始化階段
在java併發程式設計思想中有個程式碼例子如下:
public abstract class testAbstract { public testAbstract() { System.out.println("absStart"); testChildImpl(); System.out.println("absEnd"); } public abstract void testChildImpl(); } public class testAbstractImpl extends testAbstract { private String str = "123"; public testAbstractImpl() { System.out.println(str); System.out.println("real"); } public static void main(String[] args) { new testAbstractImpl(); } @Override public void testChildImpl() { System.out.println(str); } }
問輸出的是什麼? 答案:
absStart null absEnd 123 real
以前在看這個問題的時候根本不知道答案,看了也覺得輸出中的null非常難以理解,在重新看書的時候想到了其實就是關於類的初始化。上面的子類作為啟動類需要初始化,但是根據載入的順序,父類要先初始化,父類初始化的時候呼叫了子類的函式,這時子類輸出靜態欄位,因為子類的靜態欄位在類載入的準備階段已經分配了記憶體並且有預設值null,“123”要到子類初始化的時候才去賦值,所以輸出為null。一切都解釋通了,這也是我喜歡計算機的原因,一切都有原因。 (可以參考深入理解jvm的p219)
七、Class.forName和Classloader
區別在於Class.forName載入類的時候會經過載入階段的初始化階段。Classloader的loadClass根據傳入resolve是否為true執行連線階段(驗證,準備,解析),始終不會執行初始化階段。測試程式碼如下:
public class ClassLoadTest { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Class<? extends ClassLoader> aClass = ClassLoader.getSystemClassLoader().getClass(); Method loadClass = aClass.getDeclaredMethod("loadClass", String.class, boolean.class); loadClass.setAccessible(true); loadClass.invoke(systemClassLoader, "jvm.classLoad.test.ClassOut", true); System.in.read(); // Class.forName("jvm.classLoad.test.ClassOut"); } }
另外補充下類載入器的一些重要方法:
- loadClass 定義了雙親委派模型的框架
- findClass 自己基本ClassLoader一般重寫這個方法
- findLoadedClass 找到當前類載入器載入的類
- defineClass 類載入的載入階段(注意這個時候Class物件已經在記憶體中生成)
- resolveClass 類載入的驗證、準備、解析階段
八、 類載入器的回收
很多場景都是講的是類的回收,我在網上查的時候很少有談到類載入的回收。類載入的回收三個條件都有講到,其中需要類載入器被回收,那麼類載入器什麼時候能被回收呢?在這裡的我的理解是當類載入器不可達時即可被回收(程式碼中就是將所有類載入器的引用消除,一般我們都在本地存有它的引用然後置為null,但是有很多意想不到的類載入器引用洩露的問題,例如序列化,log4j,ThreadLocal問題),注意類的回收是方法區,類載入器則是堆中。由此可以解釋ThreadLocal導致tomcat記憶體洩露的問題(tomcat新版已經解決了這個問題https://wiki.apache.org/tomcat/MemoryLeakProtection)。對於這個問題有時間在Tomcat好好看下原始碼。網上看了很多關於類載入器洩露的坑,自己編寫要十分謹慎。
九、 記憶體洩露
看了一篇文章 講了記憶體洩露很不錯,自己在公司遇到過一次連線洩露的問題,所以正好整理了下相關的問題來分享下。
場景描述:我們公司的使用者服務對接了第三方騰訊雲通訊服務,在使用者註冊的時候我們需要走http介面調騰訊雲,問題就出在http連線那塊,同事當時採用了,線上出現了cpu100%的問題,日誌出現java.lang.OutOfMemoryError: GC overhead limit exceeded
。
排查思路:這個其實很好定位,本來還想列印執行緒棧看下到底是哪個導致的cpu100%,一看日誌直接定位到gc出問題。GC overhead limit exceeded是指gc佔用了大量的cpu時間又回收不了記憶體引起的,從記憶體洩露去考慮,重啟服務 ,啟動引數加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./user.hprof -verbose:gc -Xloggc:user%t.log。問題復現的時候獲得了堆的dump檔案,然後通過Jprofile分析,發現有大量的http.HttpKeepAliveCache例項,佔用了80%的記憶體,大致定位到是由於http連線洩露。同事封裝的HttpUtil中使用了HttpsURLConnection,在讀取資料的時候沒有關閉InputStream導致連線沒有關閉。
十、String拼接導致記憶體溢位
公司的後臺有段時間會間歇性的卡頓,嚴重的情況下會導致cpu100%。在cpu100%的時候,通過top定位到程序號,然後輸入H切換到執行緒,記住具體的程序號,使用jstack列印java程序的執行緒棧,jstack輸出為十六進位制,需要將top的轉換成十六進位制的然後入找執行緒經常卡在哪個方法 。定位到方法發現是查詢使用者關聯裝置號的方法出問題,方法的邏輯是從資料庫查詢裝置號,在記憶體中以以逗號分隔拼接返回,如1,2,3。這個bug的原因是有如下:
- sql出錯,導致查詢返回資料量很多,正常情況最多幾百個,但是異常情況有七萬個裝置號
- 字串拼接採用str+="1234"的形式,導致大量的記憶體分配和回收。
運營在點選後臺查詢的時候發現沒返回,點掉就重新點,導致伺服器多個執行緒卡在這個方法造成cpu100%。解決完sql,改用StringBuilder問題解決。