java虛擬機器
一 。Java記憶體區域
分為 與所有執行緒共享的 和 單獨執行緒鎖擁有的
1.1 所有執行緒的共享記憶體
- 方法區(jdk1.8之後移除,也成永久區):
jdk8之前,大多數人習慣稱之為永久代(深入理解java虛擬機器裡面的東西),主要用於儲存已被虛擬機器載入的類資訊、常量、靜態變數。常量池在jdk1.7開始已經放到 堆 中了,jdk1.8 移除 永久區,增加了元空間
元空間:
元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:後設資料空間並不在虛擬機器中,而是使用本地記憶體 - 堆
很多時候也被稱做 GC 堆,虛擬機器中記憶體區域中最大的一塊,堆記憶體的唯一目的就是存放物件例項幾乎所有的物件例項,陣列 都在堆記憶體分配 新生代(Eden區 和 from Survivor, 和 to Survivor)+老年代
1.2 不與其他執行緒共享的(生命週期就是執行緒內)
- 程式計數器(暫存器)
Java虛擬機器唯一沒有規定任何OutOfMemoryError的區域
是一塊較小的記憶體空間,用來指定當前執行緒執行位元組碼的行數,每個執行緒計數器都是私有的,因為每個執行緒都需要記錄執行的行數
;這裡解釋一下為什麼每個執行緒都需要一個執行緒計數器?
程式先去執行A執行緒,執行到一半,然後就去執行B執行緒,然後又跑回來接著執行A執行緒,那程式是怎麼記住A執行緒已經執行到哪裡了呢?這就需要程式計數器了 為了執行緒切換後能夠恢復到正確的執行位置,每條執行緒都有一個獨立的程式計數器 - 虛擬機器棧 俗稱 棧
八大繼承型別 和 物件引用 因為android 本身就是一個程式, 一個程式裡面必有一個執行緒 每個方法執行的同時都會建立一個棧幀,每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程 用於儲存區域性變數表、操作棧、動態連結、方法出口等資訊。區域性變數表存放的是:編譯期可知的基本資料型別、物件引用型別 如果執行緒請求的棧深度太深,超出了虛擬機器所允許的深度,就會出現StackOverFlowError(比如無限遞迴) 擬機棧可以動態擴充套件,如果擴充套件到無法申請足夠的記憶體空間,會出現OOM - 本地(native)方法棧
本地方法棧與java虛擬機器棧作用非常類似,其區別是:java虛擬機器棧是為虛擬機器執行java方法服務的,而本地方法棧則為虛擬機器執使用到的Native方法服務。
二 記憶體模型(執行時記憶體模型)
2.1 主記憶體
所有的變數都儲存在主記憶體中(虛擬機器記憶體的一部分),對於所有執行緒都是共享的。
2.2 工作記憶體
每條執行緒都有自己的工作記憶體,工作記憶體中儲存的是主存中某些變數的拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。
2,3 關係示意圖
2.4 記憶體間操作
8中操作都是原子的,不可再分的
- lock(鎖定),作用於主記憶體變數,把一個變數標記為執行緒獨佔。
- unlock(解鎖),與lock正相反。
- read(讀取),作用於主記憶體變數,它把一個變數從主記憶體傳輸到工作記憶體中。
- load(載入),作用於工作記憶體變數,把從read裡面獲取的變數放入工作記憶體的變數副本中
- use(使用),作用於工作記憶體變數,把變數的值傳遞給執行引擎。
- assign(複製),作用於工作記憶體變數,把執行引擎的值 複製給工作記憶體變數。同use相反
- store(儲存),作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
- write(寫入),作用於主記憶體變數,把store獲取的值,寫入到住記憶體中的變數。
三 3大特性 和 指令重拍
3.1 原子性
原子是世界上的最小單位,具有不可分割性,儘管jvm沒有把lock和unlock開放給我們使用,但jvm以更高層次的指令monitorenter和monitorexit指令開放給我們使用,
反應到java程式碼中就是---synchronized關鍵字,也就是說synchronized滿足原子性
注意點:
比如 a=0 大致認為基本資料型別的訪問讀寫具備原子性
long和double型別在32位作業系統中的讀寫操作不是原子的,因為long和double佔64位,
需要分成2個步驟來處理,在讀寫時分別拆成2個位元組進行讀寫 // 我們只是知道這件事情就可以 無需介意, 一般不會發生
但是市面上的jvm都規定 long 和double 是原子性操作(深入java虛擬機器說的) 所以編寫程式碼的時候 這兩個不用
怎樣保證?
鎖、synchronized
3.2 可見性
當一個執行緒修改了執行緒共享變數的值,其它執行緒能夠立即得知這個修改
怎樣保證?
- volatile修飾的變數,就會具有可見性 不能保證原子性 用volatile修飾的依然會有副本拷貝 是最輕量級同步的機制
也就是說當一個執行緒改變一個值得時候, 會通知另一個執行緒 告訴他,你不要用你的工作記憶體(副本)中的值了, 你再重新拿主記憶體的值吧 - final也會可見性(暫時瞭解)
- synchronized能夠實現:1.原子性(同步) 2.可見性
3.3 順序性
即jvm程式執行的順序按照程式碼的先後順序執行
3.4 指令重拍
JVM可以對它們在 "不改變資料依賴關係" 的情況下進行任意排序以提高程式效能。 一般來說,處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的雖然重排序不會影響單個執行緒內程式執行的結果,但是會影響到多執行緒
例子1
執行緒1
int a = 1
a++;
執行緒2
if(a>2){
...
}
複製程式碼
執行緒1 肯定會按照寫的順序執行的, 但是當執行2的時候,有可能對1重新排序。 因為 2依賴於1中的a, 所以會有不確定值
例子2: 單利模式
synchronized存在巨大的效能開銷 所以用了雙重校驗
public class Singleton {
private Singleton() { }
private volatile Singleton instance;
public Singleton getInstance(){
if(instance==null){// 1:第一次檢查
synchronized (Singleton.class){ // 2:加鎖
if(instance==null){ // 3:第二次檢查
instance = new Singleton();// 4:加 volatile關鍵字 的根源出在這裡
}
}
}
return instance;
}
}
}
複製程式碼
這裡為什麼要加volatile了?
在jdk1.5之前不能完全保證指令重拍的,之後才有了雙重校驗鎖機制.
問題 出現在 instance = new Singleton()
因為這條語句有下面的操作
- 分配物件的記憶體空間
- 初始化物件;
- 設定instance指向剛分配的記憶體地址
當a執行緒正在初始化這個單利的時候 ,此時另一個b執行緒執行到 步驟1的時候 發現instance !=null , 此時 jvm 有可能對 a執行緒指令重拍 假如 2 和 3 重拍的話 b執行緒 拿到就是不對的
四 gc
4.1 怎樣判斷物件是否已死?
1.引用計數演算法
給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用時效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的,引用計數演算法管理記憶體很高效, 但是java虛擬機器沒有使用,因為是他很難解決物件間相互迴圈引用的問題。兩個物件互相引用,導致它們的引用計數都不為0,於是引用計數演算法無法通知GC收集器回收它們。
2.引用計數演算法
這個演算法的基本思路就是通過一系列的稱為“GC Roots” 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明物件是不可用的。 這個演算法防止 AB互相引入
4.2 可以作為gc roots的
- 虛擬機器棧(棧幀中的本地變數表)中引用的物件。
- 方法區中靜態類屬性引用的物件。
- 方法區中常量引用的物件。
- 本地方法棧中JNI(即一般說的Native方法)引用的物件。
4.3 Java中常用的垃圾收集演算法
1.引用計數演算法 老年代 永久代
標記-清除演算法採用從根集合(GC Roots)進行掃描,對存活的物件進行標記,標記完畢後,再掃描整個空間中未被標記的物件,進行回收
2.複製演算法: 年輕代
它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了, 就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉
2.1 缺點:
- (1)效率問題:在物件存活率較高時,複製操作次數多,效率降低;
- (2)空間問題:記憶體縮小了一半;需要額外空間做分配擔保(老年代)
3. 標記-整理演算法: 老年代 永久代
先標記所有該回收的,然後整理到而是將存活的物件都向一端移動,最後直接清理掉邊界以外的記憶體
4. 分代收集演算法
頻繁收集生命週期短的區域(Young area);
比較少的收集生命週期比較長的區域(Old area);
基本不收集的永久區(Perm area)。
-
新生代:朝生夕滅的物件(例如:方法的區域性變數等)。 gc 15 次還不死的話 進入老年代 只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成 gc完成之後,複製沒有被回收的物件 細分: Eden區 和 from Survivor, 和 to Survivor, 比例 8:1:1 除法gc的時候 把 eden和from區 沒有被回收的物件放到 to區 , eden和from 全部回收 之後 把這次的to變成from *老年代:有的大物件直接進入老年代 (很長的字串 和 陣列) ,存活得比較久,但還是要死的物件(例如:快取物件、單例物件等)。
-
永久代:物件生成後幾乎不滅的物件(例如:載入過的類資訊)。
-
所以老年代和永久代 用 標記-清除演算法 或者 標記-整理演算法