Android 性能优化 之谈谈Java内存区域
最近一年副業主要在學習投資和技能學習,把以前學習內存分析的一些筆記總結發出來,寫了很多筆記總結都沒有寫完就又忙著了,最近再次總結復習學習一遍,還有提醒各位同學一定要學會投資。。
了解Android 內存管理,就有必要了解Java GC機制,必須要理解JVM 內存區域,這里我們主要從概念上介紹Java虛擬機內存的各個區域。
Android 應用運行時大部分時間都用于處理內存數據,雖然很多開發者都意識到優化內存,但并不是所有人都知道內存使用對性能的影響,網上也有很多關于內存優化技術的文章,但大多都不夠系統,沒有貫通全局。在《Android 性能優化之String篇》文中我們理解了一個對象占用空間的大小以及String 字符串對我們性能的影響,而在本文中我們將學習一些Android的內存,主要是基礎的java 的內存管理,Java程序在運行的過程中管理的內存的數據區,為后面的Android內存分析做為鋪墊。
在閱讀過程中有任何問題,請及時聯系。如需轉載請注明 fuchenxuan blog
本章系《Android 之美 從0到1 – 高手之路》Android 性能優化 之深入理解Java內存結構。
- Android 性能優化 之談談Java內存區域
- Java 內存區域
- 程序計數器
- 虛擬機棧(JVM Stack)
- 本地方法棧(Native Method Statck)
- 堆區(Heap)
- 方法區(Method Area)
- 運行時常量池
- 運行時常量池與Class文件常量池區別
- 對象的創建
- 對象的訪問
- 句柄方式
- 直接指針方式
- 總結
- Java 內存區域
Java 內存區域
Java虛擬機在執行程序過程中會把管理的內存劃分為不同的數據區域,包括方法區、堆、虛擬機棧、本地方法棧、程序計數器這5個部分的運行時數據區。如下圖:
程序計數器
在計算機組成原理里,CPU內部的寄存器中就包含一個程序計數器,存放程序執行的下一條指令地址。
以下引自wiki的一段介紹。
依照特定機器的細節而不同,他可能是保存著正在被運行的指令,也可能是下一個要運行指令的地址。程序計數器在每個指令周期會自動地增加,所以指令會正常地從寄存器中連續地被取出。某些指令,像是跳躍和子程序調用,會中斷程序執行的序列,將新的數值內容存放到程序計數器中。
在大部分的處理器,指令指針都是在提取程序指令后就被立即增加;也就是說跳躍指令的目的地址,是由跳躍指令的運算對象加上跳躍指令之后下一個指令的地址(比特或字節,視電腦形態而定)來獲得目的地。
而我們的JVM內存中的程序計數器也是這樣的作用,它儲存JVM當前執行bytecode的地址。
Java虛擬機中的程序計數器是Java運行時數據區中的一小塊內存區域,但是它的功能和通常的程序計數器是類似的,它指向虛擬機正在執行字節碼指令的地址。具體點兒說,當虛擬機執行的方法不是native的時,程序計數器指向虛擬機正在執行字節碼指令的地址;當虛擬機執行的方法是native的時,程序計數器中的值是未定義的。另外,程序計數器是線程私有的,也就是說,每一個線程都擁有僅屬于自己的程序計數器
此內存區域是唯一一個在 Java 虛擬機規范
中沒有規定任何 OutOfMemoryError 情況的區域。
虛擬機棧(JVM Stack)
與程序計數器一樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與 線程相同。虛擬機棧描述的是 Java 方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,
就對應著一個棧幀在 虛擬機棧中從入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、 double)、對象引用(reference 類型),它不等同于對象本身,根據不同的虛擬機實現,它可能是一個指向 4 / 31
對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和 returnAddress
類型(指向了一條字節碼指令的地址)。
其中 64 位長度的 long 和 double 類型的數據會占用 2 個局部變量空間(Slot),其余的數據類型只占 用 1 個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大 的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。 在 Java 虛擬機規范中,對這個 區域規定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出 StackOverflowError 異
常;如果虛擬機棧可以動態擴展(當前大部分的 Java 虛擬機都可動態擴展,只不過 Java 虛擬機規范中也允許
固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出 OutOfMemoryError 異常。
本地方法棧(Native Method Statck)
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為
虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的 Native 方法服務。虛擬機 規范中對本地方法棧中的方法使用的語言、使用方式與數據結構并沒有強制規定,因此具體的虛擬機可以自由實 現它。甚至有的虛擬機(譬如 Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一
樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
堆區(Heap)
對于大多數應用來說,Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所 有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存 放對象實例 ,幾乎所有的對象 實例都在這里分配內存。這一點在 Java 虛擬機規范中的描述是:所有的對象實例以及數組都要在堆上分配,但 是隨著 JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發 生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。
Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”(Garbage Collected Heap,幸
好國內沒翻譯成“垃圾堆”)。如果從內存回收的角度看,由于現在收集器基本都是采用的分 代 收 集 算法,所以 Java 堆中還可以細分為:新生代和老年代;再細致一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。如果從內存分配的角度看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩沖區(Thread
Local Allocation Buffer,TLAB)。不過,無論如何劃分,都與存放內容無關,無論哪個區域,存儲的都仍然 是對象實例,進一步劃分的目的是為了更好地回收內存,或者更快地分配內存。在本章中,我們僅僅針對內存區
域的作用進行討論,Java 堆中的上述各個區域的分配和回收等細節將會是下一章的主題。
根據 Java 虛擬機規范的規定,Java 堆可以處于物理上不連續的內存空間中,只要邏輯上是連續的即可, 就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都
是按照可擴展來實現的(通過-Xmx 和-Xms 控制)。如果在堆中沒有內存完成實例分配,并且堆也無法再擴展 時,將會拋出 OutOfMemoryError 異常。
堆內存唯一目的就是存放創建的對象實例,所有對象實例和數組都要在這里分配內存,因此也是垃圾回收的主要區域。根據虛擬機規范,java堆可以在物理空間上不連續,只要邏輯上連續即可,當堆內存無法繼續分配時會拋出OutOfMemery異常。
方法區(Method Area)
方法區(Method Area)與 Java 堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類
信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然 Java 虛擬機規范把方法區描述為堆的一個邏輯 部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
對于習慣在 HotSpot 虛擬機上開發和部署程序的開發者來說,很多人愿意把方法區稱為“永久 代”Permanent Generation),本質上兩者并不等價,僅僅是因為 HotSpot 虛擬機的設計團隊選擇把 GC 分
代收集擴展至方法區,或者說使用永久代來實現方法區而已。對于其他虛擬機(如 BEA JRockit、IBM J9 等) 來說是不存在永久代的概念的。即使是 HotSpot 虛擬機本身,根據官方發布的路線圖信息,現在也有放棄永久 代并“搬家”至 Native Memory 來實現方法區的規劃了。
Java 虛擬機規范對這個區域的限制非常寬松,除了和 Java 堆一樣不需要連續的內存和可以選擇固定大小
或者可擴展外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據
進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標 主要是針對常量池的回收和對類型 的卸載,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區
域的回收確實是有必要的。在 Sun 公司的 BUG 列表中, 曾出現過的若干個嚴重的 BUG 就是由于低版本的
HotSpot 虛擬機對此區域未完全回收而導致內存泄漏。 根據 Java 虛擬機規范的規定,當方法區無法滿足內存 分配需求時,將拋出 OutOfMemoryError 異常。
用于存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼數據等。Java虛擬機規范把方法區描述為堆的一個邏輯部分。
運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項就是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、 方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字 面量和符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。 Java 虛擬機對 Class 文件的每 一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用于存儲哪種數據都必須符合規范上的要求, 這樣才會被虛擬機認可、裝載和執行。但對于運行時常量池,Java 虛擬機規范沒有做任何細節的要求,不同的 提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。不過,一般來說,除了保存 Class 文件中描述 的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。 運行時常量池相對于 Class 文件常量池
的另外一個重要特征是具備動態性,Java 語言并不要求常量一定只能在編譯期產生,也就是并非預置入 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員
利用得比較多的便是 String 類的 intern()方法。 既然運行時常量池是方法區的一部分,自然會受到方法區內 存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。
運行時常量池與Class文件常量池區別
JVM對Class文件中每一部分的格式都有嚴格的要求,每一個字節用于存儲那種數據都必須符合規范上的要求才會被虛擬機認可、裝載和執行;但運行時常量池沒有這些限制,除了保存Class文件中描述的符號引用,還會把翻譯出來的直接引用也存儲在運行時常量區
相較于Class文件常量池,運行時常量池更具動態性,在運行期間也可以將新的變量放入常量池中,而不是一定要在編譯時確定的常量才能放入。最主要的運用便是String類的intern()方法
對象的創建
以上我們介紹了5個數據區域,三個是線程隔離的,它們有:Java虛擬機棧,本地方法棧,程序計數器。另外還有兩個數據區域是線程共享的,這兩個區域是垃圾回收發生的地方,它們分別是:堆,方法區。
我們最常見的也是最簡單的創建對象有以下幾種方式:
通過這種方式我們可以調用任意的構造函數(無參的和有參的)去創建對象。
Object object = new Object();- 1
使用Class類的newInstance方法(反射機制)
Student stu = Student.class.newInstance();
new 指令時主要涉及一些一個過程:
- 檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過
- 類加載檢查通過
- 虛擬機java堆為新主對象分配內存,對象所需內存的大小在類加載完成后確定
- 虛擬機將分配到的內存空間都初始化為零值
- 設置對象頭,對象頭中存儲了該對象是拿了類的實例等信息
- 執行 init 方法,把對象初始化
對象的訪問
介紹完 Java 虛擬機的運行時數據區之后,我們就可以來探討一個問題:在 Java 語言中,對象訪問是如何 進行的?對象訪問在 Java 語言中無處不在,是最普通的程序行為,但即使是最簡單的訪問,也會卻涉及 Java
棧、Java 堆、方法區這三個最重要內存區域之間的關聯關系,如下面的這句代碼: Object obj = new Object();
假設這句代碼出現在方法體中,那“Object obj”這部分的語義將會反映到 Java 棧的本地變量表中,作為一個 reference 類型數據出現。而“new Object()”這部分的語義將會反映到 Java 堆中,形成一塊存儲了 Object 類型所有實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機 實現的對象內存布局(Object Memory Layout)的不同,這塊內存的長度是不固定的。另外,在 Java 堆中
還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則 存儲在方法區中。
句柄方式
如果使用句柄訪問方式,Java 堆中將會劃分出一塊內存 來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息
直接指針方式
如果使用的是直接指針訪問方式,Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,
reference 中直接存儲的就是對象地址
這兩種對象的訪問方式各有優勢,使用句柄訪問方式的最大好處就是 reference 中存儲的是穩定的句柄地 址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而 reference
本 身 不 需要被修改。 使用直接指針訪問方式的最大好處就 是速度更快,它節省了一次指針定位 的時間開銷,由 于對象的訪問在 Java 中非常頻繁,因此這類開銷積少成多后也是一項非常可觀的執行成本。就本書討論的主
要虛擬機 Sun HotSpot 而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的范圍來看,各種語言 和框架使用句柄來訪問的情況也十分常見。
總結
上面說了那么說,下面來個簡單的概括:
方法區:方法區存放的是類信息、常量、靜態變量,所有線程共享區域。
虛擬機棧:它的生命周期與 線程相同,每個方法在執行的同時都會創建一個棧幀(Stack Frame)用存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,線程私有區域。
本地方法棧:與虛擬機棧類似,區別是虛擬機棧為虛擬機執行Java方法服務,本地方法棧為虛擬機使用到的Native方法。
堆:線程共享;用來存放對象實例,幾乎所有的對象實例都在堆上分配內存;此區域也是垃圾回收器(Garbage Collection)主要的作用區域,內存泄漏就發生在這個區域。
程序計數器:用于指示當前線程所執行的字節碼執行到了第幾行,可以理解為是當前線程的行號指示器。如果線程在執行Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令地址。
通過上面的學習,我們了解了JVM的內存區域,和java 對象的基本創建和訪問方式。為我們后面線上OOM 分析、Dalvik內存管理學習做鋪墊,以及分析線上各種OOM 的原因以及如何去解決。
后面幾篇文章:
1.Android 性能優化 之談談Java內存區域
2.Android 性能優化 之了解Dalvik內存分配策略
3.Android 性能優化 之內存泄漏分析
4.Android 性能優化 之Dalvik內存管理、線上OOM分析總結
更多Android 之美,請閱讀《Android 之美 從0到1 – 高手之路》系列文章
水平有限,若有錯漏,歡迎指正,批評,如需轉載,請注明出處–http://blog.csdn.net/vfush,謝謝!
總結
以上是生活随笔為你收集整理的Android 性能优化 之谈谈Java内存区域的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android热修复技术原理详解(最新最
- 下一篇: android sina oauth2.