1. 程式人生 > 其它 >android:繪圖 (android.graphics包)

android:繪圖 (android.graphics包)

一、什麼是JVM
JVM是Java Virtual Machine(Java 虛擬機器)的縮寫,JVM是一種用於計算裝置的規範,它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。
Java語言的一個非常重要的特點就是平臺無關性。而使用Java虛擬機器是實現這一特點的關鍵。一般的高階語言如果要在不同的平臺上執行,至少需要編譯成不同的目的碼。而引入Java語言虛擬機器後,Java語言在不同平臺上執行時不需要重新編譯。Java語言使用Java虛擬機器遮蔽了與具體平臺相關的資訊,使得Java語言編譯程式只需生成在Java虛擬機器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。Java虛擬機器在執行位元組碼時,把位元組碼解釋成具體平臺上的機器指令執行。這就是Java的能夠“一次編譯,到處執行”的原因。
每個使用Java的開發者都知道Java位元組碼是在JRE中執行(JRE: Java 執行時環境)。JVM則是JRE中的核心組成部分,承擔分析和執行Java位元組碼的工作,而Java程式設計師通常並不需要深入瞭解JVM執行情況就可以開發出大型應用和類庫。儘管如此,如果你對JVM有足夠了解,就會對Java有更好的掌握,並且能解決一些看起來簡單但又尚未解決的問題。
所以,在本篇文章中,我將會介紹JVM工作原理,內部結構,Java位元組碼的執行及指令的執行順序,並會介紹一些常見的JVM錯誤及其解決方案。最後會簡單介紹下Java SE7帶來的新特性。
虛擬機器
JRE由Java API和JVM組成,JVM通過類載入器(Class Loader)加類Java應用,並通過Java API進行執行。
虛擬機器(VM: Virtual Machine)是通過軟體模擬物理機器執行程式的執行器。最初Java語言被設計為基於虛擬機器器在而非物理機器,重而實現WORA(一次編寫,到處執行)的目的,儘管這個目標幾乎被世人所遺忘。所以,JVM可以在所有的硬體環境上執行Java位元組碼而無須調整Java的執行模式。
JVM的基本特性:
● 基於棧(Stack-based)的虛擬機器: 不同於Intel x86和ARM等比較流行的計算機處理器都是基於暫存器(register)架構,JVM是基於棧執行的。
● 符號引用(Symbolic reference): 除基本型別外的所有Java型別(類和介面)都是通過符號引用取得關聯的,而非顯式的基於記憶體地址的引用。
● 垃圾回收機制: 類的例項通過使用者程式碼進行顯式建立,但卻通過垃圾回收機制自動銷燬。
● 通過明確清晰基本型別確保平臺無關性: 像C/C++等傳統程式語言對於int型別資料在同平臺上會有不同的位元組長度。JVM卻通過明確的定義基本型別的位元組長度來維持程式碼的平臺相容性,從而做到平臺無關。
● 網路位元組序(Network byte order): Java class檔案的二進位制表示使用的是基於網路的位元組序(network byte order)。為了在使用小端(little endian)的Intel x86平臺和在使用了大端(big endian)的RISC系列平臺之間保持平臺無關,必須要定義一個固定的位元組序。JVM選擇了網路傳輸協議中使用的網路位元組序,即基於大端(big endian)的位元組序。
Sun 公司開發了Java語言,但任何人都可以在遵循JVM規範的前提下開發和提供JVM實現。所以目前業界有多種不同的JVM實現,包括Oracle Hostpot JVM和IBM JVM。Google公司使用的Dalvik VM也是一種JVM實現,儘管其並未完全遵循JVM規範。與基於棧機制的Java 虛擬機器不同的是Dalvik VM是基於暫存器的,Java 位元組碼也被轉換為Dalvik VM使用的暫存器指令集。
Java 位元組碼
JVM使用Java位元組碼—一種運行於Java(使用者語言)和機器語言的中間語言,以達到WORA的目的。Java位元組碼是部署Java程式的最小單元。
二、JVM總體概述
JVM總體上是由類裝載子系統(ClassLoader)、執行時資料區、執行引擎、垃圾收集這四個部分組成。其中我們最為關注的執行時資料區,也就是JVM的記憶體部分則是由方法區(Method Area)、JAVA堆(Java Heap)、虛擬機器棧(JVM Stack)、程式計數器、本地方法棧(Native Method Stack)這幾部分組成。
三、JVM體系結構

3.1 類裝載子系統
Class Loader類載入器負責載入.class檔案,class檔案在檔案開頭有特定的檔案標示,並且ClassLoader負責class檔案的載入等,至於它是否可以執行,則由Execution Engine決定。
3.2 執行時資料區
棧管執行,堆管儲存。JVM調優主要是優化Java堆和方法區。
3.2.1 方法區(Method Area)
方法區是各執行緒共享的記憶體區域,它用於儲存已被JVM載入的類資訊、常量、靜態變數、執行時常量池等資料。
3.2.2 Java堆(Java Heap)
Java堆是各執行緒共享的記憶體區域,在JVM啟動時建立,這塊區域是JVM中最大的, 用於儲存應用的物件和陣列,也是GC主要的回收區,一個 JVM 例項只存在一個堆記憶體,堆記憶體的大小是可以調節的。類載入器讀取了類檔案後,需要把類、方法、常變數放到堆記憶體中,以方便執行器執行,堆記憶體分為三部分:新生代、老年代、永久代。
說明:
● Jdk1.6及之前:常量池分配在永久代 。
● Jdk1.7:有,但已經逐步“去永久代” 。
● Jdk1.8及之後:無永久代,改用元空間代替(java.lang.OutOfMemoryError: PermGen space,這種錯誤將不會出現在JDK1.8中)。
3.2.3 Java棧(JVM Stack)

  1. 棧是什麼
    Java棧是執行緒私有的,是線上程建立時建立,它的生命期是跟隨執行緒的生命期,執行緒結束棧記憶體也就釋放,對於棧來說不存在垃圾回收問題,只要執行緒一結束該棧就Over,生命週期和執行緒一致。基本型別的變數和物件的引用變數都是在函式的棧記憶體中分配。

  2. 棧儲存什麼
    每個方法執行的時候都會建立一個棧幀,棧幀中主要儲存3類資料:
    ● 區域性變量表:輸入引數和輸出引數以及方法內的變數;
    ● 棧操作:記錄出棧和入棧的操作;
    ● 棧幀資料:包括類檔案、方法等等。

  3. 棧執行原理
    棧中的資料都是以棧幀的格式存在,棧幀是一個記憶體區塊,是一個數據集,是一個有關方法和執行期資料的資料集。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在棧中從入棧到出棧的過程。

  4. 本地方法棧(Native Method Stack)
    本地方法棧和JVM棧發揮的作用非常相似,也是執行緒私有的,區別是JVM棧為JVM執行Java方法(也就是位元組碼)服務,而本地方法棧為JVM使用到的Native方法服務。它的具體做法是在本地方法棧中登記native方法,在執行引擎執行時載入Native Liberies.有的虛擬機器(比如Sun Hotpot)直接把兩者合二為一。

  5. 程式計數器(Program Counter Register)
    程式計數器是一塊非常小的記憶體空間,幾乎可以忽略不計,每個執行緒都有一個程式計算器,是執行緒私有的,可以看作是當前執行緒所執行的位元組碼的行號指示器,指向方法區中的方法位元組碼(下一個將要執行的指令程式碼),由執行引擎讀取下一條指令。

  6. 執行時常量池
    執行時常量池是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。相較於Class檔案常量池,執行時常量池更具動態性,在執行期間也可以將新的變數放入常量池中,而不是一定要在編譯時確定的常量才能放入。最主要的運用便是String類的intern()方法。

3.3 執行引擎(Execution Engine)
執行引擎執行包在裝載類的方法中的指令,也就是方法。執行引擎以指令為單位讀取Java位元組碼。它就像一個CPU一樣,一條一條地執行機器指令。每個位元組碼指令都由一個1位元組的操作碼和附加的運算元組成。執行引擎取得一個操作碼,然後根據運算元來執行任務,完成後就繼續執行下一條操作碼。
不過Java位元組碼是用一種人類可以讀懂的語言編寫的,而不是用機器可以直接執行的語言。因此,執行引擎必須把位元組碼轉換成可以直接被JVM執行的語言。位元組碼可以通過以下兩種方式轉換成合適的語言:
● 直譯器: 一條一條地讀取,解釋並執行位元組碼執行,所以它可以很快地解釋位元組碼,但是執行起來會比較慢。這是解釋執行語言的一個缺點。
● 即時編譯器:用來彌補直譯器的缺點,執行引擎首先按照解釋執行的方式來執行,然後在合適的時候,即時編譯器把整段位元組碼編譯成原生代碼。然後,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過原生代碼去執行。執行原生代碼比一條一條進行解釋執行的速度快很多,編譯後的程式碼可以執行的很快,因為原生代碼是儲存在快取裡的。

3.4 垃圾收集(Garbage Collection, GC)
3.4.1 什麼是垃圾收集
垃圾收集即垃圾回收,簡單的說垃圾回收就是回收記憶體中不再使用的物件。所謂使用中的物件(已引用物件),指的是程式中有指標指向的物件;而未使用中的物件(未引用物件),則沒有被任何指標給指向,因此佔用的記憶體也可以被回收掉。
垃圾回收的基本步驟分兩步:
● 查詢記憶體中不再使用的物件(GC判斷策略)
● 釋放這些物件佔用的記憶體(GC收集演算法)
3.4.2 GC判斷策略

  1. 引用計數演算法
    引用計數演算法是給物件新增一個引用計數器,每當有一個引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的物件就是不可能再被使用的物件。缺點:很難解決物件之間相互迴圈引用的問題。
  2. 根搜尋演算法
    根搜尋演算法的基本思路就是通過一系列名為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(也就是說從GC Roots到這個物件不可達)時,則證明此物件是不可用的。
    在Java語言裡,可作為GC Roots的物件包括以下幾種:
    ● 虛擬機器棧(棧幀中的本地變量表)中引用的物件;
    ● 方法區中類靜態屬性引用的物件;
    ● 方法區中常量應用的物件;
    ● 本地方法棧中JNI(Native方法)引用的物件。

注:在根搜尋演算法中不可達的物件,也並非是“非死不可”的,因為要真正宣告一個物件死亡,至少要經歷兩次標記過程:第一次是標記沒有與GC Roots相連線的引用鏈;第二次是GC對在F-Queue執行佇列中的物件進行的小規模標記(物件需要覆蓋finalize()方法且沒被呼叫過)。
3.4.3 GC收集演算法

  1. 標記-清除演算法(Mark-Sweep)
    標記-清楚演算法採用從根集合(GC Roots)進行掃描,首先標記出所有需要回收的物件(根搜尋演算法),標記完成後統一回收掉所有被標記的物件。

該演算法有兩個問題:
● 效率問題:標記和清除過程的效率都不高;
● 空間問題:標記清除後會產生大量不連續的記憶體碎片, 空間碎片太多可能會導致在執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集。
2) 複製演算法(Copying)
複製演算法是將可用記憶體按容量劃分為大小相等的兩塊, 每次只用其中一塊, 當這一塊的記憶體用完, 就將還存活的物件複製到另外一塊上面, 然後把已使用過的記憶體空間一次清理掉。

  1. 標記-整理演算法(Mark-Compact)
    標記整理演算法的標記過程與標記清除演算法相同, 但後續步驟不再對可回收物件直接清理, 而是讓所有存活的物件都向一端移動,然後清理掉端邊界以外的記憶體。

  2. 分代收集演算法(Generational Collection)
    分代收集演算法是目前大部分JVM的垃圾收集器採用的演算法。它的核心思想是根據物件存活的生命週期將記憶體劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量物件需要被回收,而新生代的特點是每次垃圾回收時都有大量的物件需要被回收,那麼就可以根據不同代的特點採取最適合的收集演算法。

新生代(Young Generation)的回收演算法(以複製演算法為主)
● 所有新生成的物件首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件。
● 新生代記憶體按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分物件在Eden區中生成。回收時先將eden區存活物件複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活物件複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。
● 當survivor1區不足以存放 eden和survivor0的存活物件時,就將存活物件直接存放到老年代。若是老年代也滿了就會觸發一次Full GC(Major GC),也就是新生代、老年代都進行回收。
● 新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)。
老年代(Tenured Generation)的回收演算法(以標記-清除、標記-整理為主)
● 在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到老年代中。因此,可以認為老年代中存放的都是一些生命週期較長的物件。
● 記憶體比新生代也大很多(大概比例是1:2),當老年代記憶體滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代物件存活時間比較長,存活率標記高。
永久代(Permanet Generation)的回收演算法
用於存放靜態檔案,如Java類、方法等。永久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如Hibernate 等,在這種時候需要設定一個比較大的永久代空間來存放這些執行過程中新增的類。永久代也稱方法區。方法區主要回收的內容有:廢棄常量和無用的類。對於廢棄常量也可通過根搜尋演算法來判斷,但是對於無用的類則需要同時滿足下面3個條件:
● 該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項;
● 載入該類的ClassLoader已經被回收;
● 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
3.4.4 垃圾收集器

  1. Serial收集器(複製演算法)
    新生代單執行緒收集器,標記和清理都是單執行緒,優點是簡單高效。是client級別預設的GC方式,可以通過-XX:+UseSerialGC來強制指定。
  2. Serial Old收集器(標記-整理演算法)
    老年代單執行緒收集器,Serial收集器的老年代版本。
  3. ParNew收集器(停止-複製演算法)
    新生代多執行緒收集器,其實就是Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現。
  4. Parallel Scavenge收集器(停止-複製演算法)
    新生代並行的多執行緒收集器,追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 使用者執行緒時間/(使用者執行緒時間+GC執行緒時間)。適合後臺應用等對互動相應要求不高的場景。是server級別預設採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定執行緒數。
  5. Parallel Old收集器(停止-複製演算法)
    老年代並行的多執行緒收集器,Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先。
  6. CMS(Concurrent Mark Sweep)收集器(標記-清除演算法)
    CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,CMS收集器是基於“標記--清除”(Mark-Sweep)演算法實現的,整個過程分為四個步驟:
    ● 初始標記: 標記GC Roots能直接關聯到的物件,速度很快;
    ● 併發標記: 進行GC Roots Tracing的過程;
    ● 重新標記: 修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但比並發標記時間短;
    ● 併發清除: 整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。
    ● 優點:併發收集、低停頓
    ● 缺點:對CPU資源非常敏感、無法處理浮動垃圾、產生大量空間碎片。
  7. G1(Garbage First)收集器(標記-整理演算法)
    G1是一款面向服務端應用的垃圾收集器,是基於“標記-整理”演算法實現的,與其他GC收集器相比,G1具備如下特點:
    ● 並行與併發
    ● 分代收集
    ● 空間整合
    ● 可預測性的停頓
    G1運作步驟:
    ● 初始標記(stop the world事件,CPU停頓只處理垃圾)
    ● 併發標記(與使用者執行緒併發執行)
    ● 最終標記(stop the world事件,CPU停頓處理垃圾)
    ● 篩選回收(stop the world事件,根據使用者期望的GC停頓時間回收)
    3.4.5 垃圾收集結構圖