nginx 开源 编程 google java php 微软 wordpress Android centos Python Windows Firefox linux mysql shell 程序员 apache 云计算 Ubuntu

Android性能調優篇之探索JVM內存分配

開篇廢話

今天我們一起來學習JVM的內存分配,主要目的是為我們Android內存優化打下基礎。

一直在想以什麽樣的方式來呈現這個知識點才能讓我們易於理解,最終決定使用方法為:圖解+源代碼分析。

歡迎訪問我的個人博客:senduo's blog

希望能在我們平時開發寫代碼的時候,能夠知道當前寫的這段代碼,內存方面是如何分配的。

我們深知,一個JAVA程序員在很多時候根本不用操心內存的釋放,而是依靠JVM去管理,以前寫C++代碼的時候,卻要時刻記著new的空間要及時釋放掉,不然程序很容易出現內存溢出的情況。因為,Java在這方面確實方便了許多,讓我們有更多精力去考慮業務方面的實現。但是,這並不意味著我們就能肆無忌憚的使用內存,因為:

1.JVM並不會及時的去清理內存

2.我們無法通過代碼去控制JVM去清理內存
這就要求我們平時在開發過程中,要了解JVM的垃圾回收機制,合理安排內存。

那麽怎麽樣才能合理安排內存呢?那麽就需要我們了解JVM的內存分配機制,而後才能真正控制好,讓程序運行在我們鼓掌之中。

技術詳情

1.JVM內存模型

平時我們對於Java內存都有一個比較粗略的概念,就是分堆和棧,但實際上還是復雜得多,以下給出完整內存模型:

内存模型

内存模型

相对应区域的内容为:

内容模型

內容模型

1.1程序計數器PC

這一個區域我概括了以下幾個要點:

1.這一區域不會出現OOM(Out Of Memory)錯誤的情況

2.屬於線程私有,因為每一個線程都有自己的一個程序計數器,來表示當前線程執行的字節碼行號

3.標識Java方法的字節碼地址,而不是Native方法

4.處於CPU上,我們無法直接操作這塊區域


1.2虛擬機棧

這個區域也是我們平時口中說的堆棧的棧,關於這個塊區域有如下要點:

1.屬於線程私有,與線程的生命周期相同

2.每一個java方法被執行的時候,這個區域會生成一個棧幀

4.棧幀中存放的局部變量有8種基本數據類型,以及引用類型(對象的內存地址)

5.java方法的運行過程就是棧幀在虛擬機棧中入棧和出棧的過程

6.當線程請求的棧的深度超出了虛擬機棧允許的深度時,會拋出StackOverFlow的錯誤

7.當Java虛擬機動態擴展到無法申請足夠內存時會拋出OutOfMemory的錯誤

1.3本地方法棧

這個區域,屬於線程私有,顧名思義,區別於虛擬機棧,這裏是用來處理Native方法(Java本地方法)的,而虛擬機棧是處理Java方法的。對於Native方法,Object中就有不少的Native的方法,hashCode,wait等,這些方法的執行很多時候都是借助於操作系統。

這一區域也有可能拋出StackOverFlowError 和 OutOfMemoryError

1.4 Java堆

我們平時說得最多,關註得最多的一個區域,就是他了。我們後期進行的性能優化主要針對這部分內存,GC的主戰場,這個地方存放的幾乎所有的對象實例和數組數據。這裏我大概進行了如下概括:

1.Java堆屬於線程共享區域,所有的線程共享這一塊內存區域

2.從內存回收角度,Java堆可被分為新生代和老年代,這樣分能夠更快的回收內存

3.從內存分配角度,Java堆可劃分出線程私有的分配緩存區(Thread Local Allocation Buffer,TLAB),這樣能夠更快的分配內存

4.當Java虛擬機動態擴展到無法申請足夠內存時會拋出OutOfMemory的錯誤
1.5 方法區

方法區主要存放的是已被虛擬機加載的類信息、常量、靜態變量、編譯器編譯後的代碼等數據。GC在該區域出現的比較少。概括如下:

1.方法區屬於線程共享區域

2.習慣性加他永久代

3.垃圾回收很少光顧這個區域,不過也是需要回收的,主要針對常量池回收,類型卸載

4.常量池用於存放編譯期生成的各種字節碼和符號引用,常量池具有一定的動態性,
  裏面可以存放編譯期生成的常量

5.運行期間的常量也可以添加進入常量池中,比如string的intern()方法。


1.6 運行時常量池

運行時常量池也是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用。單獨拿出來說明一下,是因為我們平時使用String比價多,涉及到這一塊的知識,但這一塊區域不會拋出OutOfMemoryError

2.JVM內存源碼示例說明

首先寫了一個main方法,來做演示,代碼如下:

package senduo.com.memory.allocate;

/**
 * *****************************************************************
 * * 文件作者:ouyangshengduo
 * * 创建时间:2017/8/11
 * * 文件描述:内存分配调用过程演示代码
 * * 修改历史:2017/8/11 9:39*************************************
 **/
public class MemoryAllocateDemo {
    public static void main(String[] args){ //JVM自动寻找main方法
        /**
         * 执行第一句代码,创建一个test实例test,在栈中分配一块内存,存放一个指向堆区实例对象的指针
         */
        Test test = new Test();
        
        /**
         * 执行第二句代码,声明定义一个int型变量(8种基本数据类型),在栈区直接分配一块内存存储这个变量的值
         */
        int date = 9;
        
        /**
         * 执行第三句代码,创建一个BirthDate实例bd1,在栈中分配一块内存,存放一个指向堆区实例对象的指针
         */
        BirthDate bd1 = new BirthDate(13,6,1991);
        
        /**
         * 执行第四句代码,创建一个BirthDate实例bd2,在栈中分配一块内存,存放一个指向堆区实例对象的指针
         */
        BirthDate bd2 = new BirthDate(30,4,1991);
        
        /**
         * 执行第五句代码,方法test1入栈帧,执行完出栈
         */
        test.test1(date);
        
        /**
         * 执行第六句代码,方法test2入栈帧,执行完出栈
         */
        test.test2(bd1);
        
        /**
         * 执行第七句代码,方法test3入栈帧,执行完出栈
         */
        test.test3(bd2);

    }
}

演示過程一

1.JVM自動尋找main方法,執行第一句代碼,創建一個Test類的實例test,
  在棧中分配一塊內存,存放一個指向堆區對象的指針110925。

2.創建一個int型的變量date,由於是基本類型,直接在棧中存放date對應的值9。

3.創建兩個BirthDate類的實例bd1、bd2,在棧中分別存放了對應的指針指向各自的對象
  ,他們在實例化時調用了有參數的構造方法,因此對象中有自定義初始值。
  
圖解如下:

内存分配调用演示一

内存分配调用演示一

演示过程二

1.test1方法入栈帧,以date为参数

2.value为局部变量,把value放在栈中,并且把date的值赋值给value

3.把123456赋值给value局部变量

4.test1方法执行完,value内存被释放,test1方法出栈

内存分配调用演示二

内存分配调用演示二

内存分配调用演示二

内存分配调用演示二

内存分配调用演示二

內存分配調用演示二

演示過程三

1.test2方法入棧幀,以實例bd1為參數

2.birthDate為局部變量,把birthDate放在棧中,把bd1的引用的值賦值給birthDate,
  也就是bd1與birthDate的地址都是指向同一個堆區的實例
3.在堆區new了一個對象,並且把這個堆區的指針保存在棧區中birthDate對應的內存空
  間,這個時候,bd1與birthDate指向了不同的堆區,那麽birthDate的改變,並不會對
  bd1造成影響

4.test2方法執行完,棧中的birthDate空間被釋放,test2方法出棧,但堆區的內存空間
  則要等待系統自動回收

内存分配调用演示三

内存分配调用演示三

内存分配调用演示三

内存分配调用演示三

内存分配调用演示三

內存分配調用演示三

演示過程四

1.test3方法入棧幀,以實例bd2為參數

2.birthDate為局部變量,把birthDate放在棧中,把bd2的引用的值賦值給birthDate,
  也就是bd2與birthDate的地址都是指向同一個堆區的實例
3.調用birthDate的setDay方法,因為birthDate與bd2指向的是同一個對象,也就是bd2調用了setDay方法,所以,也會bd2造成影響

4.test3方法執行完,棧中的birthDate空間被釋放,test3方法出棧

内存分配调用演示四

内存分配调用演示四

内存分配调用演示四

内存分配调用演示四

内存分配调用演示四

內存分配調用演示四

3.JVM內存分配小結

跟著上面四個步驟,走一遍,會發現其實也不會那麽復雜,掌握思想就能摸到門路了,我們平時註意區分一下基本數據類型的變量和引用數據類型變量,以下進行了幾點概括:

1.局部變量中的基本數據類型的值直接存棧中

2.局部變量中的引用數據類型在棧中存的是引用類型的指針(地址)

3.棧中的數據與堆中的數據內存回收並不是同步的,棧中的只要方法運行完,就會直接
  銷毀局部變量,但堆中的對象不一定立即銷毀
  
4.類的成員變量在不同對象中各不相同,都有自己的存儲空間(成員變量在堆中的對象中
  )。而類的方法卻是該類的所有對象共享的,只有一套,對象使用方法的時候方法才被
  壓入棧,方法不使用則不占用內存
幹貨總結

終於把JVM內存分配的分享寫完了,一路寫下來,確實對內存分配又深入了解了一次。期間參考了以下博客:

Java之美[從菜鳥到高手演變]之JVM內存管理及垃圾回收

Java 內存分配全面淺析

Jvm內存模型

通過對JVM內存模型的認識後,下一章將進行JVM垃圾回收機制的探索。


著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

延伸阅读

评论