Android性能優(yōu)化-方法區(qū)導致內存問題實例分析

說到Android內存優(yōu)化,網上相關資料主要是關于內存泄露和內存溢出,基本都是針對堆內存問題進行分析,很少有關注方法區(qū)導致的內存問題,堆內存回收主要是回收對象,方法區(qū)內存回收主要是類回收,簡單來說就是目前主要關注堆中對象回收,很少關注方法區(qū)中類信息導致的內存問題,本文主要關注方法區(qū)導致的內存問題,通過實際例子來詳細分析方法區(qū)導致的內存問題,解釋問題原因并給出修改方案。Android內存優(yōu)化(堆內存)相關內容可參考:鴻洋大神的文章Android 性能優(yōu)化的方方面面都在這兒-內存優(yōu)化,以及Android性能優(yōu)化-內存篇。

本文主要內容有:
1.先拋出實際遇到的內存問題以及網上類似的相關問題;
2.介紹虛擬機內存及類加載相關背景知識,這些知識是分析問題的方法論。
3.講解實際遇到方法區(qū)導致的內存溢出,會詳細講解分析過程,以及導致問題的根本原因,最終給出修改方案;

網上相關類似問題

how to reduce .apk mmap size in my android app
如下圖所示:

apkmmap.PNG

簡單來說就是如何減少.apk mmap所占用的內存,和文本所講的問題類似,但是目前還無人解答,文章最后會解答該問題。

實際工作遇到的內存問題

在測試過程中發(fā)生多次內存告警,dumpsys meminfo信息如下所示

** MEMINFO in pid 10095 [test.test.test] **
                   Pss  Private  Private  SwapPss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    22556    22512        0     4262    45056    23670    21385
  Dalvik Heap     2833     2796        4       23     6718     3359     3359
 Dalvik Other     3300     3300        0       60                           
        Stack       48       48        0       16                           
       Ashmem        2        0        0        0                           
    Other dev       13        0       12        0                           
     .so mmap     3569      132        4      145                           
    .apk mmap   417594   413340     3124   129860     //.apk占用內存很大                      
    .ttf mmap       26        0        0        0                           
    .dex mmap     4327        0     3208       16                           
    .oat mmap     1543        0        0        0                           
    .art mmap     1875     1216       36       28                           
   Other mmap      756        4       80        1                           
   EGL mtrack     6582     6582        0        0                           
    GL mtrack     4200     4200        0        0                           
      Unknown     1243     1240        0      558                           
        TOTAL   605436   455370     6468   134969    51774    27029    24744
 
 App Summary
                       Pss(KB)
                        ------
           Java Heap:     4048
         Native Heap:    22512
                Code:   419808   //code占用內存也很大,和.apk有關系
               Stack:       48
            Graphics:    10782
       Private Other:     4640
              System:   143598
 
               TOTAL:   605436       TOTAL SWAP PSS:   134969

可以看出.apk mmap占用的內存達到四百多兆,正常情況下.apk所占用內存為9M。同時也可以看到Code占用的內存也是四百多兆,正常情況下Code所占用的內存也是10M左右,以上就是我們遇到問題的現(xiàn)象,暫時先不分析該問題,我們先來了解虛擬機內存相關背景知識,然后再詳解講解該問題的分析過程。

虛擬機內存相關背景知識

JVM運行時數(shù)據(jù)區(qū)域如下圖所示:

JVM運行時數(shù)據(jù)區(qū).JPG

方法區(qū):方法區(qū)存放的是類信息、常量、靜態(tài)變量,所有線程共享區(qū)域。
虛擬機棧:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息,線程私有區(qū)域。
本地方法棧:與虛擬機棧類似,區(qū)別是虛擬機棧為虛擬機執(zhí)行Java方法服務,本地方法棧為虛擬機使用到的Native方法服務。
:JVM管理的內存中最大的一塊,所有線程共享;用來存放對象實例,幾乎所有的對象實例都在堆上分配內存,此區(qū)域也是垃圾回收器(Garbage Collection)主要的作用區(qū)域,內存泄漏就發(fā)生在這個區(qū)域。
程序計數(shù)器:可看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器;如果線程在執(zhí)行Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令地址,如果執(zhí)行的是Native方法,這個計數(shù)器的值為空(Undefined),這個區(qū)域是唯一一個不會拋出OutOfMemoryError異常的區(qū)域。
其中程序計數(shù)器、虛擬機棧、本地方法棧3三個區(qū)域隨線程而生,隨線程而滅,棧中的棧幀隨著方法的進入和退出而有條不紊的執(zhí)行著出棧和入棧的操作,每個棧幀中分配多少內存基本上是類結構確定下來時已知的,因此這幾個區(qū)域的內存分配和回收都具備確定性,在這幾個區(qū)域內不需要過多的考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟著回收了,而Java堆和方法區(qū)不一樣,這部分內存分配和回收都是動態(tài),垃圾收集器所關注的就是這部分內存,堆內存回收資料很多,不做過多介紹,我們主要關注一下方法區(qū)回收,方法區(qū)內存在虛擬機中的永久代,Java虛擬機規(guī)范說過可以不要求虛擬機在方法區(qū)實現(xiàn)垃圾收集,而且方法區(qū)垃圾收集性價比一般比較低(永久代)。永久代垃圾收集主要回收兩部分內容:廢棄常量和無用類,回收廢棄常量與回收Java堆中對象類似,判斷常量是否是廢棄常量比較簡單,而要判定一個類是否是無用的類條件相對苛刻,類需要滿足下面三個條件才能算無用的類:
(1)該類所有實例都已被回收,也就是Java堆中不存在該類的任何實例。
(2)加載該類ClassLoader已經被回收。
(3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足以上三個條件的無用類進行回收,這里說的僅僅是”可以“,而并不是和對象一樣,不使用了就必然回收,是否對類進行回收,部分虛擬機提供了參數(shù)可以進行控制。所以對類的回收整體是比較難的。使方法區(qū)發(fā)生類導致的內存溢出基本思路:在運行時產生大量的類去填滿方法區(qū),也就是在運行時動態(tài)產生很多的類,直到方法區(qū)內存溢出。所以頻繁動態(tài)產生很多類時,需要注意方法區(qū)內存溢出。

虛擬機類加載機制相關背景知識

虛擬機把描述類的數(shù)據(jù)從Class文件加載到內存,并對數(shù)據(jù)進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確定其在Java虛擬機中唯一性,每一個類加載器,都擁有一個獨立的類名稱空間,簡答來說就是比較兩個類是否相同,只有在兩個類是由同一個類加載器加載的前提下才有意義,否則,即使兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這兩個就必定不相同。類加載采用雙親委派模型,具體內容請參與《深入理解Java虛擬機》,雙親委派模型對保證Java程序穩(wěn)定運行有很重要作用,實現(xiàn)雙親委派代碼在java.lang.ClassLoader的loadClass()方法中,如下所示

private final ClassLoader parent;
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

可以看出在加載類時先檢查類是否已經被加載過,如沒有加載則調用父加載器的loadClass()方法,若父加載器為空則默認使用啟動類加載器作為父加載器,如果父類加載器加載失敗,拋出ClassNotFoundException異常后,再調用自己的findClass()方法進行加載。與本文相關的內容就是類如果已經加載就不會再次被加載,以及判斷類是否相同的條件,如果不停的加載類,就會導致方法區(qū)內存溢出。以上就是本文所需要的背景知識,下面我們來分析遇到的方法區(qū)內存問題。

方法區(qū)導致內存問題實例分析

現(xiàn)象
如前文中dumpsys meminfo信息所示,在dumpsys meminfo中.apk mmap占用的內存達到四百多兆,正常情況下.apk所占用內存為9M。同時也可以看到Code占用的內存也是四百多兆,正常情況下Code所占用的內存也是10M左右。
分析過程
(1)《移動App性能評測與優(yōu)化》書中專門講解了如何分析內存的不同部分,以及如何優(yōu)化內存的不同部分。由于書中分析的Android版本比較舊,我們遇到的問題暫時是沒有相關內容的,但是給我們提供一些方法論,如smaps中信息是meminfo的來源,雖然版本不同,但還是有些線索供我們分析該問題的。
(2)首先我們需要搞明白.apk以及Code代表的含義,
Code:您的應用用于處理代碼和資源(如 dex 字節(jié)碼、已優(yōu)化或已編譯的 dex 碼、.so 庫和字體)的內存。
.apk mmap:apk代碼占用的內存,簡單來說就是class文件的字節(jié)碼,也可以理解apk中類所占用的內存,Android中.dex文件將所有APK中的.class里邊所包含的信息全部整合在一起,所以Class字節(jié)碼信息存儲在.dex文件中。
其中,一個Class文件占用內存大小是可以計算出來,具體內容請參考《深入理解JVM》或Java虛擬機原理圖解。
(3)APK代碼占用內存大,也就是類信息占用的內存很大,該部分內存屬于方法區(qū),在上邊虛擬機內存相關知識中介紹了類信息占用內存大,是因為動態(tài)創(chuàng)建了很多類,導致這部分內存增加。
(4)因為之前版本沒有該問題,應該是最新修改引起的,既然是不停的創(chuàng)建新的類,就查看最近修改,最近修改發(fā)現(xiàn)確實有DexClassLoader加載第三方apk,然后重復相關操作,每操作一次.apk和Code內存增長7M左右,問題代碼如下

            DexClassLoader dexClassLoader = new DexClassLoader(dexPath, cacheDir, "", ClassLoader.getSystemClassLoader());
                        testView = (View) dexClassLoader.loadClass(clsName)
                    .getMethod(method, new Class[]{Context.class, Handler.class})
                    .invoke(null, new Object[]{loadContext, handler});

所以就是動態(tài)加載第三方APK時,動態(tài)產生了很多類,導致內存不停增長,每執(zhí)行一次上述代碼,內存增加7M,并且APK中.dex文件大小當好就是7M。
(5)剛開始病急亂投醫(yī)階段,一頭霧水不知道如何分析,使用MAT工具查看hprof文件,hprof文件中存儲的是堆中對象的快照,而我們遇到的問題是類信息導致的內存問題,所以MAT工具是分析不出來的,那么分析類信息占用內存應該使用什么工具那?最后找到了showmap和swaps,showmap可以查看進程跑起來后各庫所占用內存情況,swaps中信息是meminfo中數(shù)據(jù)來源。因此,.apk內存增加和堆內存是不一樣的,使用MAT工具是分析不出來的,MAT是用來分析堆轉儲,堆轉儲是應用堆中所有對象的快照,而我們遇到的問題是.apk代碼占用內存太大,說白來就是加載了太多的類。
(6)執(zhí)行adb shell showmap pid,查看進程跑起來后各庫所占用內存情況,最終發(fā)現(xiàn)每次運行一次,第三方APK的內存每次增加7M的大小。如下所示:

 virtual                     shared   shared  private  private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS   # object
-------- -------- -------- -------- -------- -------- -------- -------- -------- ---- ------------------------------
   21132    21132    21132        0        0        0    21132        0        0    3 /dev/ashmem/dalvik-classes.dex extracted in memory from /data/app/test-5iR1QUK1UTKpuR4CbwVkTg==/base.apk (deleted)

現(xiàn)在第三方APK占用內存大小為21132Kb,每運行一次上述問題代碼,第三方APK所占用內存增加7M。
(7)dumpsys meminfo數(shù)據(jù)來源于swaps,就是將smaps不同數(shù)據(jù)相加可以得到對應meminfo中相關的數(shù)據(jù),執(zhí)行adb shell cat /proc/pid/smaps > smapsinfo.txt,我們來查看其中的第三方APK占用內存情況

712811f000-7128800000 r--p 00000000 00:05 209598                         /dev/ashmem/dalvik-classes.dex extracted in memory from /data/app/test-5iR1QUK1UTKpuR4CbwVkTg==/base.apk (deleted)
Size:               7044 kB
Rss:                7044 kB
Pss:                7044 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:      7044 kB
Referenced:         7044 kB
Anonymous:          7044 kB
AnonHugePages:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
PSwap:                 0 kB
SwapPss:               0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
MMUPageSize:           4 kB
Locked:                0 kB
713851f000-7138c00000 r--p 00000000 00:05 203443                         /dev/ashmem/dalvik-classes.dex extracted in memory from /data/app/test-5iR1QUK1UTKpuR4CbwVkTg==/base.apk (deleted)
Size:               7044 kB
Rss:                7044 kB
Pss:                7044 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:      7044 kB
Referenced:         7044 kB
Anonymous:          7044 kB
AnonHugePages:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
PSwap:                 0 kB
SwapPss:               0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB

可以看出,第三方APK占用內存大小為7M,每執(zhí)行一次問題代碼,smaps中就會多一個APK內存信息,所以我們猜測.apk占用內存的大小與上述smaps中apk占用內存有關,所以要是能保證smaps中APK不重復出來,應該就可以解決該問題,最新的smaps中信息和meminfo中的對應關系我也不是太清楚,目前也沒有找到相關資料,只能通過閱讀源碼才能知道了,如果大家知道這部分內容可以分享一下。
通過showmap和swaps相關信息我們從側面也反應出應用內存在不停的增加。至此,我們已經找到問題的復現(xiàn)路徑和問題代碼,也知道了因為加載了太多類導致.apk mmap內存不斷增加。
問題根本原因
需要從虛擬機類加載相關知識說起,在加載類時先檢查類是否已經被加載過,如沒有加載才會去重新加載該類,對于任意一個類,都需要由加載它的類加載器和這個類本身一同確定其在Java虛擬機中唯一性,每一個類加載器,都擁有一個獨立的類名稱空間,簡答來說就是比較兩個類是否相同,只有在兩個類是由同一個類加載器加載的前提下才有意義,否則,即使兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這兩個就必定不相同?,F(xiàn)在我們來看問題代碼,在每次加載第三方APK時,都會重新new一個ClassLoader,然后將第三方APK中的類重新加載一次,問題就在new ClassLoader,因為加載第三方APK中類信息時,都會new一個新的類加載器,這樣類加載器在檢查類是否已經被加載時,因為不是同一個類加載器,就會判定類還沒有被加載,所以每次執(zhí)行都需要重新加載一次類,就導致每次執(zhí)行問題代碼就將第三方apk中的類全部重新加載一遍,這樣存放類信息的.apk mmap內存就會不斷增加。
修改方案
將類加載器設置為單例或者只要保證類加載器是同一個對象即可,這樣后續(xù)每次加載第三方APK時,發(fā)現(xiàn)APK中的類信息都已經被加載過了,就不會重新加載類,相應的.apk mmap內存就不會不停的增長。

網上類似問題解答

how to reduce .apk mmap size in my android app,從描述來來看,應該是WebView中加載了第三方APK或者動態(tài)生成了大量的類,這樣就會導致.apk的內存增加,上述我們遇到的例子中加載第三方APK的時候.apk mmap內存也會增加7M(應該是將APK中所有類信息全部加載進來,APK中dex文件大小也剛好是7M),增加之后這部分內存也不會降下來,目前我們只能保證不讓這部分內存不停的增加,確實不知道如何回收這些類信息。但可以從如下兩角度來考慮如何解決:
(1)從回收類信息角度來看:回收類信息應該是虛擬機來完成,應該是ART虛擬機不支持類回收,所以這部分內存降不下來,要想回收類信息需要虛擬機支持;
(2)從減少類信息內存角度來看:在動態(tài)加載第三方APK的時候,如何才能使用APK中的部分類信息來實現(xiàn)想要的功能,而不是將APK所有類信息全部加載進來(目前我也不知道應該如何做,歡迎討論)。
以上就是方法區(qū)類信息導致內存問題的分析過程以及相關背景知識,希望對你有所幫助,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容