Thinking in Java 學習筆記
Java記憶體
JVM記憶體空間劃分:
l 方法區(Method Area):方法區存放類資訊(類名、修飾)、類的靜態變數、final常量和方法資訊。在Hotspot中,方法區對應的是持久代(permanent generation)。方法區的垃圾收集主要針對常量池回收和對已載入類的解除安裝
l 堆區(Heap):堆區存放物件例項及陣列,通過new建立的物件都儲存在堆區,堆區在虛擬機器啟動的時候被建立,並被所有的執行緒所共享。堆區是Java GC機制管理的主要記憶體區域
l 虛擬機器棧(JVM Stack):每個執行緒對應一個虛擬機器棧,一個執行緒的每個方法在執行時都會建立一個棧幀,棧幀中儲存區域性變量表、方法出口、操作站、動態連結。區域性變量表中儲存著方法的相關區域性變數,包括各種基本資料型別,物件的引用,返回地址等
l 本地方法棧(Native Method Stack):本地方法棧用於支援native方法的執行,儲存每個本地方法呼叫的狀態,本地方法棧和虛擬機器方法棧執行機制一致。在很多虛擬機器中(如Sun的JDK預設的Hotspot虛擬機器),會將本地方法棧與虛擬機器棧放在一起使用
l 程式計數器(Program Counter Register):程式計數器用於指示當前執行緒所執行的位元組碼執行到了第幾行,它是一個比較小的記憶體區域。位元組碼直譯器在工作時,會通過改變這個計數器的值來取下一條語句指令。
【注】java 8之後,方法區改為元資料區,並且移除了持久代。元資料儲存在本地記憶體,被稱為metaspace
記憶體分代回收:當物件分配越多,物件列表越來越大時,GC掃描和移動耗時增加,而大部分物件存活時間較短,少部分物件存活時間較長,所以採用分代回收的策略實際是為了提高GC效率
l 年輕代(Young Generation):物件在被建立時,記憶體首先在年輕代進行分配(大物件可以直接在老年代分配),年輕代由Eden Space和兩塊相同大小的SurvivorSpace組成。年輕代的Eden區記憶體是連續的,所以其分配會非常快;同樣Eden區的回收也非常快。年輕代需要回收時觸發Minor GC
l 老年代(Old Generation):老年代用於存放在年輕代中經過多次垃圾回收仍然存在的物件,例如快取。除此之外,還有兩種情況會在老年代直接分配記憶體:一種是大物件,另外一種是大的陣列物件(陣列中無引用外部物件)。老年代回收時觸發Major GC
l 持久代(Permanent Generation):持久代對應虛擬機器記憶體中的“方法區”,即主要儲存類資訊、類的靜態變數、方法資訊等,由於這些資訊在執行過程中基本上會一直使用,所以針對持久代的記憶體回收效率很低
記憶體回收的幾種演算法:
l 複製(Copying):採用的方式為從根集合掃描出存活的物件,並將其複製到一塊空閒空間,當待回收控制元件中存活的物件較少時,該演算法執行效率較高(年輕代的Eden區域即採用此種演算法)
l 標記-清除(Marking -Deleting):採用的方式為從根集合開始掃描,標記存活的物件,然後掃描控制元件中未標記的物件並進行清除。標記-清除演算法不需要移動物件,在控制元件物件存活較多的情況下比較高效,但也因此或造成記憶體碎片
l 標記-壓縮(Marking - Compact):標記-壓縮演算法前面部分的處理方式同標記-清除演算法,只是在物件清除後,會再把存活的物件移動到空閒空間,從而避免了記憶體碎片的問題,當然由於增加了一步移動,所以成本會比標記-清除演算法高
Java記憶體模型(JMM:Java MemoryMode):
l Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數等細節(這裡的變數不是值java程式設計中的變數,而是例項欄位,靜態欄位和構成陣列物件的元素)
l 呼叫棧和本地變數存放線上程棧上,物件存放在堆上。所有的變數都儲存在主記憶體(Main Memory)中,每個執行緒還有自己的工作記憶體(Working Memory),執行緒的工作記憶體儲存了該執行緒使用變數的主記憶體副本拷貝
l 從更低的層次來說,主記憶體就是硬體的記憶體,而為了獲取更好的執行速度,虛擬機器及硬體系統可能會讓工作記憶體優先儲存於暫存器和快取記憶體中
l 執行緒對所有變數的操作(讀取、賦值)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,不同執行緒之間無法互相訪問彼此執行緒工作記憶體中的變數,執行緒之間的值傳遞都需要通過主記憶體完成
l 執行緒見資料交換過程:1)執行緒S1將工作記憶體中的變數更新到主記憶體,2)執行緒S2讀取更新後的主記憶體變數並複製到S2的工作記憶體中
關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成:
· lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
· unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
· read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
· load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
· use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
· assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
· store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
· write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。
Java中的volatile關鍵字:volatile關鍵字可以保證直接從主存中讀取一個變數,如果這個變數被修改後,總是會被寫回到主存中去。Java記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性的,無論是普通變數還是volatile變數都是如此,普通變數與volatile變數的區別是:volatile的特殊規則保證了新值能立即同步到主記憶體,以及每個執行緒在每次使用volatile變數前都立即從主記憶體重新整理。因此我們可以說volatile保證了多執行緒操作時變數的可見性,而普通變數則不能保證這一點。
Java中的synchronized關鍵字:同步塊的可見性是由“如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值”、“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store和write操作)”這兩條規則獲得的。
初始化和清理
在Java中,“初始化”和“建立”捆綁在一起,兩者不能分離。
構造方法是一種特殊型別的方法,它沒有返回值(與返回void不同)。
方法過載(overload):在同一個類中或子類中,名字相同而引數不同的方法
l 每個過載的方法都必須有一個獨一無二的引數型別列表(引數個數、型別或順序不同)
l 過載的方法可以改變訪問修飾符
l 過載的方法可以宣告新的或更廣的異常檢查
l 過載的方法可以有不同的返回值型別,但是不能僅僅通過返回值型別來區分過載方法
方法重寫(override):重寫是子類對父類方法的實現過程進行重新編寫,返回值和引數不變
l 引數列表必須與被重寫方法完全相同
l 返回型別必須與被重寫方法的返回型別完全相同
l 訪問許可權不能比父類中被重寫方法的訪問許可權更小
l Final方法和static方法不能被重寫
l 重寫的方法可以丟擲任何非強制性異常,但是不能丟擲任何新的強制性異常(反之可以)
Static關鍵字:
l 可以在沒有建立任何物件的情況下,僅僅通過類本身來呼叫static方法
l Static方法的內部不能呼叫非static方法,反之可以
l Static方法的內部不能使用非靜態的static變數
l Static方法內部不能使用this關鍵字(this關鍵字只能在方法內部使用,表示對“呼叫此方法的那個物件”的引用,但是static方法可以通過類本身來呼叫,所以static方法中不能使用this關鍵字)
l Static 方法無法具有多型屬性
垃圾回收:使用垃圾回收的原因是回收程式不再使用的記憶體
finalize()方法:finalize()方法主要用來清理本地方法所分配的記憶體空間。本地方法指在Java中呼叫的非Java方法。
成員初始化:
l 類的成員,且為基本型別時,如果不對其進行初始化,該成員將獲得一個初始值
l 類的成員,不管是基本型別還是物件引用,當在構造方法中使用時,如果不對其進行初始化,則該成員會獲得一個初始值(物件引用的初始值為null)
l 除上述兩種情況之外,所有成員都必須進行初始化,否則編譯報錯
靜態變數初始化:
l Static關鍵字不能應用於區域性變數
l 無論建立多少個物件,靜態資料都只佔用一份儲存區
l 靜態變數的初始化只有在必要的時候才會進行,且只執行一次
訪問許可權控制
許可權從大到小依次為:public、protected、包訪問許可權(沒有關鍵字,預設的訪問許可權)和private
l 每個編譯單元(檔案)都只能有一個public類,如果有一個以上public類,編譯報錯
l Public類的名稱必須和包含該編譯單元的檔名完全一致,否則編譯報錯
l 類的訪問許可權只能是public或包訪問許可權(內部類除外),不能是protected和private
許可權控制表:
修飾符 | 當前類 | 同一包內 | 子孫類 | 其他包 | 其他包子孫類 |
public | Y | Y | Y | Y | Y |
protected | Y | Y | Y | N | Y/N(說明) |
default | Y | Y | N | N | N |
private | Y | N | N | N | N |
說明:
- 基類的 protected 成員是包內可見的,並且對子類可見;
- 若子類與基類不在同一包中,那麼在子類中,子類例項可以訪問其從基類繼承而來的protected方法,而不能訪問基類例項的protected方法。
複用類
複用類的兩種方式:組合和繼承
向上轉型:從一個專用型別向較通用型別的轉換
Final關鍵字:
l Final資料:對於基本型別,final使數值恆定不變;對於物件引用,final使引用恆定不變(一旦final引用被初始化指向一個物件,就無法再把他改為指向另一個物件)
l Final方法:定義final方法以防止任何繼承類修改他的含義,即防止子類覆蓋父類的final方法
l Final類:當不希望一個類被繼承時,可以將一個類定義為final類
多型
多型是同一個行為具有多種不同表現形式的能力,實現多型必須滿足三點:繼承、方法重寫以及父類引用指向子類物件
靜態方法是與類,而不是與單個的物件相關聯,所以如果一個方法是靜態方法,則其無法具有多型性
介面
抽象類:包含抽象方法的類叫做抽象類。如果一個類包含一個或多個抽象方法,則該類必須定義為抽象類。抽象方法是指僅有方法宣告而沒有方法體的方法。
介面是Java實現多重繼承的途經
Java容器
基本的集合類:List,Set,Queue和Map,java容器類都可以自動地調整尺寸
List必須按照插入的順序儲存元素
Set對於每個值都只儲存一次(不能有重複元素)
Map是允許將某些物件與其他一些物件關聯起來的關聯陣列
Collection的構造器可以接收另一個Collection,用來初始化自身。首選使用Collection.addAll()的方式。也可以使用Arrays.asList()的輸出作為一個List,但是使用此種方法,其底層是陣列,所以不能調整尺寸,也就不能再呼叫add()方法和delete()方法。示例:
List<Integer> list1 = new ArrayList<Integer>();
Collections.addAll(list1, 1, 2, 3);
for (Integer integer : list1) {
System.out.println(integer);
}
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
ArrayList和LinkedList比較:
l ArrayList在隨機訪問元素上面有優勢,但是在中間插入和移除元素時較慢
l LinkedList在隨機訪問時較慢,但是能夠通過較低的代價在其中插入和移除元素
LinkedList具有實現Stack功能的所有方法;另外LinkedList實現了Queue介面,因此LinkedList可以用作Queue的一種實現,並可以向上轉型為Queue
l TreeSet將元素儲存在紅黑樹資料結構中(改進的二叉查詢樹),保持元素排序狀態
l HashSet使用的是雜湊函式,HashSet提供最快的查詢速度
l LinkedHashList也使用了雜湊函式,不過看起來是使用了連結串列來維護元素插入順序