【备战秋招系列-3】Java高频知识点——排序、设计模式、JavaSE、JVM
排序算法 9
P1:分類
排序算法可以分為內(nèi)部排序和外部排序,在內(nèi)存中進(jìn)行的排序稱為內(nèi)部排序,當(dāng)要排序的數(shù)據(jù)量很大時(shí)無法全部拷貝到內(nèi)存,這時(shí)需要使用外存進(jìn)行排序,這種排序稱為外部排序。
內(nèi)部排序包括比較排序和非比較排序,比較排序包括插入排序、選擇排序、交換排序和歸并排序,非比較排序包括計(jì)數(shù)排序、基數(shù)排序和桶排序。
其中插入排序又包括直接插入排序和希爾排序,選擇排序包括直接選擇排序和堆排序,交換排序包括冒泡排序和快速排序。
P2:直接插入排序
直接插入排序?qū)儆诓迦肱判?#xff0c;是一種穩(wěn)定的排序,平均/最差時(shí)間復(fù)雜度均為 O(n2),當(dāng)元素基本有序時(shí)最好時(shí)間復(fù)雜度為 O(n),空間復(fù)雜度為 O(1)。
基本原理:每一趟將一個(gè)待排序記錄按其關(guān)鍵字的大小插入到已排好序的一組記錄的適當(dāng)位置上,直到所有待排序記錄全部插入為止。
適用場(chǎng)景:待排序記錄較少或基本有序的情況。
public void insertionSort(int[] nums) {for (int i = 1; i < nums.length; i++) {int insertNum = nums[i];int insertIndex;for (insertIndex = i - 1; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex--) {nums[insertIndex + 1] = nums[insertIndex];}nums[insertIndex + 1] = insertNum;}}優(yōu)化:直接插入沒有利用到要插入的序列已有序的特點(diǎn),插入第 i 個(gè)元素時(shí)可以通過二分查找找到插入位置 insertIndex,再把 i~insertIndex 之間的所有元素后移一位,把第 i 個(gè)元素放在插入位置上。
public void binaryInsertionSort(int[] nums) {for (int i = 1; i < nums.length; i++) {int insertNum = nums[i];int insertIndex = -1;int start = 0;int end = i - 1;while (start <= end) {int mid = start + (end - start) / 2;if (insertNum > nums[mid])start = mid + 1;else if (insertNum < nums[mid])end = mid - 1;else {insertIndex = mid + 1;break;}}if (insertIndex == -1)insertIndex = start;if (i - insertIndex >= 0)System.arraycopy(nums, insertIndex, nums, insertIndex + 1, i - insertIndex);nums[insertIndex] = insertNum;}}P3:希爾排序
希爾排序?qū)儆诓迦肱判?#xff0c;又稱縮小增量排序,是對(duì)直接插入排序的一種改進(jìn),并且是一種不穩(wěn)定的排序,平均時(shí)間復(fù)雜度為O(n1.3),最差時(shí)間復(fù)雜度為 O(n2),最好時(shí)間復(fù)雜度為 O(n),空間復(fù)雜度為 O(1)。
基本原理:把記錄按下標(biāo)的一定增量分組,對(duì)每組進(jìn)行直接插入排序,每次排序后減小增量,當(dāng)增量減至 1 時(shí)排序完畢。
適用場(chǎng)景:中等規(guī)模的數(shù)據(jù)量,對(duì)規(guī)模很大的數(shù)據(jù)量不是最佳選擇。
public void shellSort(int[] nums) {for (int d = nums.length / 2; d > 0 ; d /= 2) {for (int i = d; i < nums.length; i++) {int insertNum = nums[i];int insertIndex;for (insertIndex = i - d; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex -= d) {nums[insertIndex + d] = nums[insertIndex];}nums[insertIndex + d] = insertNum;}}}P4:直接選擇排序
直接選擇排序?qū)儆谶x擇排序,是一種不穩(wěn)定的排序,任何情況下時(shí)間復(fù)雜度都是 O(n2),空間復(fù)雜度為 O(1)。
基本原理:每次在未排序序列中找到最小元素,和未排序序列的第一個(gè)元素交換位置,再在剩余未排序序列中重復(fù)該操作直到所有元素排序完畢。
適用場(chǎng)景:數(shù)據(jù)量較小的情況,比直接插入排序稍快。
public void selectSort(int[] nums) {int minIndex;for (int index = 0; index < nums.length - 1; index++){minIndex = index;for (int i = index + 1;i < nums.length; i++){if(nums[i] < nums[minIndex]) minIndex = i;}if (index != minIndex){swap(nums, index, minIndex);}}}P5:堆排序
堆排序?qū)儆谶x擇排序,是對(duì)直接選擇排序的改進(jìn),并且是一種不穩(wěn)定的排序,任何情況時(shí)間復(fù)雜度都為 O(nlogn),空間復(fù)雜度為 O(1)。
基本原理:將待排序記錄看作完全二叉樹,可以建立大根堆或小根堆,大根堆中每個(gè)節(jié)點(diǎn)的值都不小于它的子節(jié)點(diǎn)值,小根堆中每個(gè)節(jié)點(diǎn)的值都不大于它的子節(jié)點(diǎn)值。
以大根堆為例,在建堆時(shí)首先將最后一個(gè)節(jié)點(diǎn)作為當(dāng)前節(jié)點(diǎn),如果當(dāng)前節(jié)點(diǎn)存在父節(jié)點(diǎn)且值大于父節(jié)點(diǎn),就將當(dāng)前節(jié)點(diǎn)和父節(jié)點(diǎn)交換。在移除時(shí)首先暫存根節(jié)點(diǎn)的值,然后用最后一個(gè)節(jié)點(diǎn)代替根節(jié)點(diǎn)并作為當(dāng)前節(jié)點(diǎn),如果當(dāng)前節(jié)點(diǎn)存在子節(jié)點(diǎn)且值小于子節(jié)點(diǎn),就將其與值較大的子節(jié)點(diǎn)進(jìn)行交換,調(diào)整完堆后返回暫存的值。
適用場(chǎng)景:數(shù)據(jù)量較大的情況。
public void add(int[] nums, int i, int num){nums[i] = num;int curIndex = i;while (curIndex > 0) {int parentIndex = (curIndex - 1) / 2;if (nums[parentIndex] < nums[curIndex]) swap(nums, parentIndex, curIndex);else break;curIndex = parentIndex;}}public int remove(int[] nums, int size){int result = nums[0];nums[0] = nums[size - 1];int curIndex = 0;while (true) {int leftIndex = curIndex * 2 + 1;int rightIndex = curIndex * 2 + 2;if (leftIndex >= size) break;int maxIndex = leftIndex;if (rightIndex < size && nums[maxIndex] < nums[rightIndex])maxIndex = rightIndex;if (nums[curIndex] < nums[maxIndex])swap(nums, curIndex, maxIndex);else break;curIndex = maxIndex;}return result;}P6:冒泡排序
冒泡排序?qū)儆诮粨Q排序,是一種穩(wěn)定的排序,平均/最壞時(shí)間復(fù)雜度均為 O(n2),當(dāng)元素基本有序時(shí)最好時(shí)間復(fù)雜度為O(n),空間復(fù)雜度為 O(1)。
基本原理:是比較相鄰的元素,如果第一個(gè)比第二個(gè)大就進(jìn)行交換,對(duì)每一對(duì)相鄰元素做同樣的工作,從開始第一對(duì)到結(jié)尾的最后一對(duì),每一輪排序后末尾元素都是有序的,針對(duì) n 個(gè)元素重復(fù)以上步驟 n -1 次排序完畢。
public void bubbleSort(int[] nums) {for (int i = 0; i < nums.length - 1; i++) {for (int index = 0; index < nums.length - 1 - i; index++) {if (nums[index] > nums[index + 1]) swap(nums, index, index + 1)}}}優(yōu)化:當(dāng)序列已經(jīng)有序時(shí)仍會(huì)進(jìn)行不必要的比較,可以設(shè)置一個(gè)標(biāo)志記錄是否有元素交換,如果沒有直接結(jié)束比較。
public void betterBubbleSort(int[] nums) {boolean swap;for (int i = 0; i < nums.length - 1; i++) {swap = true;for (int index = 0; index < nums.length - 1 - i; index++) {if (nums[index] > nums[index + 1]) {swap(nums, index ,index + 1);swap = false;}}if (swap) break;}}P7:快速排序
快速排序?qū)儆诮粨Q排序,是對(duì)冒泡排序的一種改進(jìn),并且是一種不穩(wěn)定的排序,平均/最好時(shí)間復(fù)雜度均為 O(nlogn),當(dāng)元素基本有序時(shí)最壞時(shí)間復(fù)雜度為O(n2),空間復(fù)雜度為 O(logn)。
基本原理:首先選擇一個(gè)基準(zhǔn)元素,通過一趟排序?qū)⒁判虻臄?shù)據(jù)分割成獨(dú)立的兩部分,一部分全部小于等于基準(zhǔn)元素,一部分全部大于等于基準(zhǔn)元素,再按此方法遞歸對(duì)這兩部分?jǐn)?shù)據(jù)進(jìn)行快速排序。
快速排序的一次劃分從兩頭交替搜索,直到 low 和 high 指針重合,因此一趟時(shí)間復(fù)雜度是 O(n),而整個(gè)算法的時(shí)間復(fù)雜度與劃分趟數(shù)有關(guān)。最好情況是每次劃分選擇的中間數(shù)恰好將當(dāng)前序列等分,經(jīng)過 log(n) 趟劃分便可得到長(zhǎng)度為 1 的子表,這樣算法的時(shí)間復(fù)雜度為O(nlogn)。最壞情況是每次所選中間數(shù)是當(dāng)前序列中的最大或最小元素,這使每次劃分所得子表其中一個(gè)為空表,另一個(gè)子表的長(zhǎng)度為原表的長(zhǎng)度 - 1。這樣長(zhǎng)度為 n 的數(shù)據(jù)表的需要經(jīng)過 n 趟劃分,整個(gè)排序算法的時(shí)間復(fù)雜度為O(n2)。
適用場(chǎng)景:數(shù)據(jù)量較大且元素基本無序的情況。
public void quickSort(int[] nums, int start, int end) {if (start < end) {int pivotIndex = getPivotIndex(nums, start, end);quickSort(nums, start, pivotIndex - 1);quickSort(nums, pivotIndex + 1, end);}}public int getPivotIndex(int[] nums, int start, int end) {int pivot = nums[start];int low = start;int high = end;while (low < high) {while (low <= high && nums[low] <= pivot) low++;while (low <= high && nums[high] > pivot) high--;if (low < high) swap(nums, low, high);}swap(nums, start, high);return high;}優(yōu)化:當(dāng)規(guī)模足夠小時(shí),例如 end - start < 10 時(shí),采用直接插入排序。
P8:歸并排序
歸并排序基于歸并操作,是一種穩(wěn)定的排序算法,任何情況時(shí)間復(fù)雜度都為 O(nlogn),空間復(fù)雜度為 O(n)。
基本原理:應(yīng)用分治法將待排序序列分成兩部分,然后對(duì)兩部分分別遞歸排序,最后進(jìn)行合并,使用一個(gè)輔助空間并設(shè)定兩個(gè)指針分別指向兩個(gè)有序序列的起始元素,將指針對(duì)應(yīng)的較小元素添加到輔助空間,重復(fù)該步驟到某一序列到達(dá)末尾,然后將另一序列剩余元素合并到輔助空間末尾。
適用場(chǎng)景:數(shù)據(jù)量大且對(duì)穩(wěn)定性有要求的情況。
int[] help;public void mergeSort(int[] arr) {int[] help = new int[arr.length];sort(arr, 0, arr.length - 1);}public void sort(int[] arr, int start, int end) {if (start == end) return;int mid = start + (end - start) / 2;sort(arr, start, mid);sort(arr, mid + 1, end);merge(arr, start, mid, end);}public void merge(int[] arr, int start, int mid, int end) {if (end + 1 - start >= 0) System.arraycopy(arr, start, help, start, end + 1 - start);int p = start;int q = mid + 1;int index = start;while (p <= mid && q <= end) {if (help[p] < help[q]) arr[index++] = help[p++];else arr[index++] = help[q++];}while (p <= mid) arr[index++] = help[p++];while (q <= end) arr[index++] = help[q++];}P9:排序算法的選擇原則
當(dāng)數(shù)據(jù)量規(guī)模較小時(shí),考慮直接插入排序或直接選擇排序,當(dāng)元素分布有序時(shí)直接插入排序?qū)⒋蟠鬁p少比較次數(shù)和移動(dòng)記錄的次數(shù),如果不要求穩(wěn)定性,可以使用直接選擇排序,效率略高于直接插入排序。
當(dāng)數(shù)據(jù)量規(guī)模中等時(shí),選擇希爾排序。
當(dāng)數(shù)據(jù)量規(guī)模較大時(shí)考慮堆排序、快速排序和歸并排序。如果對(duì)穩(wěn)定性有要求可以采用歸并排序,如果元素分布隨機(jī)可以采用快速排序,如果元素分布接近正序或逆序可以采用堆排序。
一般不使用冒泡排序。
設(shè)計(jì)模式 12
P1:原則
開閉原則:面向?qū)ο笤O(shè)計(jì)中最基礎(chǔ)的設(shè)計(jì)原則,指一個(gè)軟件實(shí)體(類、模塊、方法等)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。它強(qiáng)調(diào)用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié),提高代碼的可復(fù)用性和可維護(hù)性。例如在版本更新時(shí)盡量不修改源代碼,但可以增加新功能。
單一職責(zé)原則:一個(gè)類、接口或方法只負(fù)責(zé)一個(gè)職責(zé),提高代碼可讀性和可維護(hù)性,降低代碼復(fù)雜度以及變更引起的風(fēng)險(xiǎn)。
依賴倒置原則:程序應(yīng)該依賴于抽象類或接口,而不是具體的實(shí)現(xiàn)類。可以降低代碼的耦合度,提高系統(tǒng)的穩(wěn)定性。
接口隔離原則:將不同功能定義在不同接口中實(shí)現(xiàn)接口隔離,避免了類依賴它不需要的接口,減少了接口之間依賴的冗余性和復(fù)雜性。
里氏替換原則:對(duì)開閉原則的補(bǔ)充,規(guī)定了任何父類可以出現(xiàn)的地方子類都一定可以出現(xiàn),可以約束繼承泛濫,加強(qiáng)程序健壯性。
迪米特原則:也叫最少知道原則,每個(gè)模塊對(duì)其他模塊都要盡可能少地了解和依賴,降低代碼耦合度。
合成/聚合原則:盡量使用組合(has-a)/聚合(contains-a)而不是繼承(is-a)達(dá)到軟件復(fù)用的目的,避免濫用繼承帶來的方法污染和方法爆炸,方法污染指父類的行為通過繼承傳遞給子類,但子類并不具備執(zhí)行此行為的能力;方法爆炸指繼承樹不斷擴(kuò)大,底層類擁有的方法過于繁雜,導(dǎo)致很容易選擇錯(cuò)誤。
P2:分類
創(chuàng)建型模式:提供了一種在創(chuàng)建對(duì)象的同時(shí)隱藏創(chuàng)建邏輯的方式,而不是使用 new 運(yùn)算符直接實(shí)例化對(duì)象,使程序在判斷需要?jiǎng)?chuàng)建哪些對(duì)象時(shí)更加靈活。包括工廠模式、抽象工廠模式、單例模式、建造者模式、原型模式。
結(jié)構(gòu)型模式:通過類和接口之間的繼承和引用實(shí)現(xiàn)創(chuàng)建復(fù)雜結(jié)構(gòu)的對(duì)象。包括適配器模式、橋接模式、過濾器模式、組合模式、裝飾器模式、外觀模式、享元模式、代理模式。
行為型模式:通過類之間不同的通信方式實(shí)現(xiàn)不同的行為。包括責(zé)任鏈模式、命名模式、解釋器模式、迭代器模式、中介者模式、備忘錄模式、觀察者模式、狀態(tài)模式、策略模式、模板模式、訪問者模式。
###P3:簡(jiǎn)單工廠模式
工廠模式屬于創(chuàng)建型模式,分為簡(jiǎn)單工廠模式,工廠方法模式和抽象工廠模式。
簡(jiǎn)單工廠模式指由一個(gè)工廠對(duì)象來創(chuàng)建實(shí)例,客戶端不需要關(guān)注創(chuàng)建的邏輯,只需要提供傳入工廠對(duì)象的參數(shù)。適用于工廠類負(fù)責(zé)創(chuàng)建對(duì)象較少的情況,缺點(diǎn)是如果要增加新產(chǎn)品,就需要修改工廠類的判斷邏輯,違背開閉原則,且產(chǎn)品多的話會(huì)使工廠類比較復(fù)雜。
應(yīng)用舉例
Calendar 抽象類的 getInstance 方法,該方法調(diào)用了 createCalendar 方法,根據(jù)不同的地區(qū)參數(shù)創(chuàng)建不同的日歷對(duì)象。
Spring 中的 BeanFactory 使用簡(jiǎn)單工廠模式,根據(jù)傳入一個(gè)唯一的標(biāo)識(shí)來獲得 Bean 對(duì)象。
舉例
一個(gè)工廠和一種抽象產(chǎn)品。例如一個(gè)麥當(dāng)勞店可以生產(chǎn)漢堡。
public class MacDonaldFactory {public Hamburger eatHamburger(String name) {if ("beef".equals(name))return new BeefHamburger();else if ("pig".equals(name))return new PigHamburger();return null;}}interface Hamburger {void eat();}class BeefHamburger implements Hamburger {@Overridepublic void eat() {System.out.println("吃牛肉漢堡");}}class PigHamburger implements Hamburger {@Overridepublic void eat() {System.out.println("吃豬肉漢堡");}}P4:工廠方法模式
工廠方法模式指定義一個(gè)創(chuàng)建對(duì)象的接口,讓接口的實(shí)現(xiàn)類來決定創(chuàng)建哪一種對(duì)象,讓類的實(shí)例化推遲到子類中進(jìn)行??蛻舳酥恍桕P(guān)心對(duì)應(yīng)的工廠而無需關(guān)心創(chuàng)建細(xì)節(jié),主要解決了產(chǎn)品擴(kuò)展的問題,在簡(jiǎn)單工廠模式中如果產(chǎn)品種類變多,工廠的職責(zé)會(huì)越來越多,不便于維護(hù)。
應(yīng)用舉例
Collection 接口這個(gè)抽象工廠中定義了一個(gè)抽象的 iterator 工廠方法,返回一個(gè) Iterator 類的抽象產(chǎn)品。該方法通過 ArrayList 、HashMap 等具體工廠實(shí)現(xiàn),返回 Itr、KeyIterator 等具體產(chǎn)品。
Spring 的 FactoryBean 接口的 getObject 方法也是一個(gè)工廠方法。
舉例
多個(gè)工廠和一種抽象產(chǎn)品。例如一個(gè)麥當(dāng)勞店可以生產(chǎn)漢堡,一個(gè)肯德基店也可以生產(chǎn)漢堡。
public interface HamburgerFactory {Hamburger build();}class MCFactory implements HamburgerFactory {@Overridepublic Hamburger build() {return new MCHamburger();}}class KFCFactory implements HamburgerFactory {@Overridepublic Hamburger build() {return new KFCHamburger();}}interface Hamburger {void eat();}class MCHamburger implements Hamburger {@Overridepublic void eat() {System.out.println("吃麥當(dāng)勞漢堡");}}class KFCHamburger implements Hamburger {@Overridepublic void eat() {System.out.println("吃肯德基漢堡");}}P5:抽象工廠模式
抽象工廠模式指提供一個(gè)創(chuàng)建一系列相關(guān)或相互依賴對(duì)象的接口,無需指定它們的具體類??蛻舳瞬灰蕾囉诋a(chǎn)品類實(shí)例如何被創(chuàng)建和實(shí)現(xiàn)的細(xì)節(jié),主要用于系統(tǒng)的產(chǎn)品有多于一個(gè)的產(chǎn)品族,而系統(tǒng)只消費(fèi)其中某一個(gè)產(chǎn)品族產(chǎn)品的情況。抽象工廠模式的缺點(diǎn)是不方便擴(kuò)展產(chǎn)品族,并且增加了系統(tǒng)的抽象性和理解難度。
應(yīng)用舉例
java.sql.Connection 接口就是一個(gè)抽象工廠,其中包括很多抽象產(chǎn)品如 Statement、Blob、Savepoint 等。
舉例
多個(gè)工廠和多種抽象產(chǎn)品。例如一個(gè)麥當(dāng)勞店和一個(gè)肯德基店都可以生產(chǎn)漢堡和可樂。
public interface FoodFactory {Hamburger buildHamburger();Drink buildDrink();}class MCFactory implements FoodFactory {@Overridepublic Hamburger buildHamburger() {return new MCHamburger();}@Overridepublic Drink buildDrink() {return new MCDrink();}}class KFCFactory implements FoodFactory {@Overridepublic Hamburger buildHamburger() {return new KFCHamburger();}@Overridepublic Drink buildDrink() {return new KFCDrink();}}interface Hamburger {void eat();}class MCHamburger implements Hamburger {@Overridepublic void eat() {System.out.println("吃麥當(dāng)勞漢堡");}}class KFCHamburger implements Hamburger {@Overridepublic void eat() {System.out.println("吃肯德基漢堡");}}interface Drink {void drink();}class MCDrink implements Drink {@Overridepublic void drink() {System.out.println("喝麥當(dāng)勞飲料");}}class KFCDrink implements Drink {@Overridepublic void drink() {System.out.println("喝肯德基飲料");}}P6:單例模式
單例模式屬于創(chuàng)建型模式,指一個(gè)單例類在任何情況下都只存在一個(gè)實(shí)例,構(gòu)造方法必須是私有的、由自己創(chuàng)建一個(gè)靜態(tài)實(shí)例對(duì)象存儲(chǔ)該實(shí)例,并對(duì)外提供一個(gè)靜態(tài)公有方法獲取實(shí)例。優(yōu)點(diǎn)是內(nèi)存中只有一個(gè)實(shí)例,減少了開銷,尤其是頻繁創(chuàng)建和銷毀實(shí)例的情況下,并且可以避免對(duì)資源的多重占用。缺點(diǎn)是沒有抽象層,難以擴(kuò)展,與單一職責(zé)原則沖突。
應(yīng)用舉例
Spring 的 ApplicationContext 創(chuàng)建的 Bean 實(shí)例都是單例對(duì)象,還有 ServletContext、數(shù)據(jù)庫(kù)連接池等也都是單例模式。
餓漢式:在類加載時(shí)就初始化創(chuàng)建單例對(duì)象,線程安全,但不管是否使用都創(chuàng)建對(duì)象可能會(huì)浪費(fèi)內(nèi)存。
public class HungrySingleton {private HungrySingleton(){}private static HungrySingleton instance = new HungrySingleton();public static HungrySingleton getInstance() {return instance;}}懶漢式:在外部調(diào)用時(shí)才會(huì)加載,線程不安全,可以加鎖保證線程安全但效率低。
public class LazySingleton {private LazySingleton(){}private static LazySingleton instance;public static LazySingleton getInstance() {if(instance == null) {instance = new LazySingleton();}return instance;}}雙重檢查鎖:使用 volatile 以及多重檢查來減小鎖范圍,提升效率。
public class DoubleCheckSingleton {private DoubleCheckSingleton(){}private volatile static DoubleCheckSingleton instance;public static DoubleCheckSingleton getInstance() {if(instance == null) {synchronized (DoubleCheckSingleton.class) {if (instance == null) {instance = new DoubleCheckSingleton();}}}return instance;}}靜態(tài)內(nèi)部類:同時(shí)解決餓漢式的內(nèi)存浪費(fèi)問題和懶漢式的線程安全問題。
public class StaticSingleton {private StaticSingleton(){}public static StaticSingleton getInstance() {return StaticClass.instance;}private static class StaticClass {private static final StaticSingleton instance = new StaticSingleton();}}枚舉:Effective Java 作者提倡的方式,不僅能避免線程安全問題,還能防止反序列化重新創(chuàng)建新的對(duì)象,絕對(duì)防止多次實(shí)例化,也能防止反射破解單例的問題。
public enum EnumSingleton {INSTANCE;}P7:代理模式
代理模式屬于結(jié)構(gòu)型模式,為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問。優(yōu)點(diǎn)是可以增強(qiáng)目標(biāo)對(duì)象的功能,降低代碼耦合度,擴(kuò)展性好。缺點(diǎn)是在客戶端和目標(biāo)對(duì)象之間增加代理對(duì)象會(huì)導(dǎo)致請(qǐng)求處理速度變慢,增加系統(tǒng)復(fù)雜度。
應(yīng)用舉例
Spring 利用動(dòng)態(tài)代理實(shí)現(xiàn) AOP,如果 Bean 實(shí)現(xiàn)了接口就使用 JDK 代理,否則使用 CGLib 代理。
靜態(tài)代理:代理對(duì)象持有被代理對(duì)象的引用,調(diào)用代理對(duì)象方法時(shí)也會(huì)調(diào)用被代理對(duì)象的方法,但是會(huì)在被代理對(duì)象方法的前后增加其他邏輯。需要手動(dòng)完成,在程序運(yùn)行前就已經(jīng)存在代理類的字節(jié)碼文件,代理類和被代理類的關(guān)系在運(yùn)行前就已經(jīng)確定了。 缺點(diǎn)是一個(gè)代理類只能為一個(gè)目標(biāo)服務(wù),如果要服務(wù)多種類型會(huì)增加工作量。
public interface Company {void findWorker();}public class Hr implements Company {@Overridepublic void findWorker() {System.out.println("需要找招聘一個(gè)員工");}}public class Proxy implements Company {private Hr hr;public Proxy(){this.hr = new Hr();}@Overridepublic void findWorker() {hr.findWorker();System.out.println("找到了員工");}}動(dòng)態(tài)代理:動(dòng)態(tài)代理在程序運(yùn)行時(shí)通過反射創(chuàng)建具體的代理類,代理類和被代理類的關(guān)系在運(yùn)行前是不確定的。動(dòng)態(tài)代理的適用性更強(qiáng),主要分為 JDK 動(dòng)態(tài)代理和 CGLib 動(dòng)態(tài)代理。
- JDK 動(dòng)態(tài)代理:通過 Proxy 類的 newInstance 方法獲取一個(gè)動(dòng)態(tài)代理對(duì)象,需要傳入三個(gè)參數(shù),被代理對(duì)象的類加載器、被代理對(duì)象實(shí)現(xiàn)的接口,以及一個(gè) InvocationHandler 調(diào)用處理器來指明具體的邏輯,相比靜態(tài)代理的優(yōu)勢(shì)是接口中聲明的所有方法都被轉(zhuǎn)移到 InvocationHandler 的 invoke 方法集中處理。 public static void main(String[] args) {Hr hr = new Hr();Company proxyHr = (Company) Proxy.newProxyInstance(hr.getClass().getClassLoader(), hr.getClass().getInterfaces(), (proxy, method, args1) -> {System.out.println("接收代理請(qǐng)求");Object obj = method.invoke(hr, args1);System.out.println("找到了員工,完成請(qǐng)求");return obj;});proxyHr.findWorker();}```
- CGLib 動(dòng)態(tài)代理:JDK 動(dòng)態(tài)代理要求實(shí)現(xiàn)被代理對(duì)象的接口,而 CGLib 要求繼承被代理對(duì)象,如果一個(gè)類是 final 類則不能使用 CGLib 代理。兩種代理都在運(yùn)行期生成字節(jié)碼,JDK 動(dòng)態(tài)代理直接寫字節(jié)碼,而 CGLib 動(dòng)態(tài)代理使用 ASM 框架寫字節(jié)碼,ASM 的目的是生成、轉(zhuǎn)換和分析以字節(jié)數(shù)組表示的已編譯 Java 類。 JDK 動(dòng)態(tài)代理調(diào)用代理方法通過反射機(jī)制實(shí)現(xiàn),而 GCLib 動(dòng)態(tài)代理通過 FastClass 機(jī)制直接調(diào)用方法,它為代理類和被代理類各生成一個(gè)類,該類為代理類和被代理類的方法分配一個(gè) int 參數(shù),調(diào)用方法時(shí)可以直接定位,因此調(diào)用效率更高。
P8:裝飾器模式
裝飾器模式屬于結(jié)構(gòu)型模式,在不改變?cè)袑?duì)象的基礎(chǔ)上將功能附加到對(duì)象,相比繼承可以更加靈活地?cái)U(kuò)展原有對(duì)象的功能。這種模式創(chuàng)建了一個(gè)裝飾類用來包裝原有的類,并在保持類方法簽名完整性的前提下提供了額外的功能。裝飾器模式適合的場(chǎng)景:在不想增加很多子類的前提下擴(kuò)展一個(gè)類的功能。
應(yīng)用舉例
java.io 包中,InputStream 字節(jié)輸入流通過裝飾器 BufferedInputStream 增強(qiáng)為緩沖字節(jié)輸入流。
和代理模式的區(qū)別:裝飾器模式的關(guān)注點(diǎn)在于給對(duì)象動(dòng)態(tài)添加方法,而動(dòng)態(tài)代理更注重對(duì)象的訪問控制。動(dòng)態(tài)代理通常會(huì)在代理類中創(chuàng)建被代理對(duì)象的實(shí)例,而裝飾器模式會(huì)將裝飾者作為構(gòu)造方法的參數(shù)。
P9:適配器模式
適配器模式屬于結(jié)構(gòu)型模式,它作為兩個(gè)不兼容接口之間的橋梁,結(jié)合了兩個(gè)獨(dú)立接口的功能,將一個(gè)類的接口轉(zhuǎn)換成另外一個(gè)接口,這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)加入獨(dú)立或不兼容的接口功能。優(yōu)點(diǎn)是使得原本由于接口不兼容而不能一起工作的類可以一起工作。 缺點(diǎn)是過多使用適配器會(huì)讓系統(tǒng)非?;靵y,不易整體把握。
應(yīng)用舉例
- java.io 包中,InputStream 字節(jié)輸入流通過適配器 InputStreamReader 轉(zhuǎn)換為 Reader 字符輸入流。
- Spring MVC 中的 HandlerAdapter,由于 handler 有很多種形式,包括 Controller、HttpRequestHandler、Servlet 等,但調(diào)用方式又是確定的,因此需要適配器來進(jìn)行處理,根據(jù)適配規(guī)則調(diào)用 handle 方法。
- Arrays.asList 方法,將數(shù)組轉(zhuǎn)換為對(duì)應(yīng)的集合(注意不能使用修改集合的方法,因?yàn)榉祷氐?ArrayList 是 Arrays 的一個(gè)內(nèi)部類)。
和裝飾器模式的區(qū)別:適配器模式?jīng)]有層級(jí)關(guān)系,適配器和被適配者沒有必然連續(xù),滿足 has-a 的關(guān)系,主要用于解決不兼容的問題,注重兼容和轉(zhuǎn)換,是一種后置考慮。裝飾器模式具有層級(jí)關(guān)系,裝飾器與被裝飾者實(shí)現(xiàn)同一個(gè)接口,滿足 is-a 的關(guān)系,注重覆蓋和擴(kuò)展,是一種前置考慮。
和代理模式的區(qū)別:適配器模式主要改變所考慮對(duì)象的接口,而代理模式不能改變所代理類的接口。
P10:策略模式
策略模式屬于行為型模式,定義了一系列算法并把它們封裝起來,讓它們之間可以互相替換。策略模式的應(yīng)用場(chǎng)景主要包括:① 在一個(gè)系統(tǒng)里面有許多類,它們之間的區(qū)別僅在于它們的行為,使用策略模式可以動(dòng)態(tài)地讓一個(gè)對(duì)象在許多行為中選擇一種行為。 ② 一個(gè)系統(tǒng)需要?jiǎng)討B(tài)地在幾種算法中選擇一種。策略模式主要解決在有多種算法相似的情況下,使用 if/else 所帶來的復(fù)雜和難以維護(hù)。策略模式的優(yōu)點(diǎn)是算法可以自由切換,可以避免使用多重條件判斷并且擴(kuò)展性良好,缺點(diǎn)是策略類會(huì)增多并且所有策略類都需要對(duì)外暴露。
應(yīng)用舉例
- 比較器 Comparator:在集合框架中,經(jīng)常需要通過構(gòu)造方法傳入一個(gè)比較器 Comparator 進(jìn)行比較排序。Comparator 就是一個(gè)抽象策略,一個(gè)類通過實(shí)現(xiàn)該接口并重寫 compare 方法成為具體策略類。
- ThreadPoolExecutor 中的四種拒絕策略:在創(chuàng)建線程池時(shí),需要傳入拒絕策略,當(dāng)創(chuàng)建新線程使當(dāng)前運(yùn)行的線程數(shù)超過maximumPoolSize 時(shí)會(huì)使用相應(yīng)的拒絕策略進(jìn)行處理。
P11:模板模式
模板模式屬于行為型模式,指定義一個(gè)算法的骨架,并允許子類為一個(gè)或多個(gè)步驟提供實(shí)現(xiàn)。模板模式使子類可以在不改變算法結(jié)構(gòu)的情況下重新定義算法的某些步驟,適用于抽取子類的重復(fù)代碼到一個(gè)公共父類中。優(yōu)點(diǎn)是可以封裝固定不變的部分,擴(kuò)展可變的部分。缺點(diǎn)是每一個(gè)不同實(shí)現(xiàn)都需要一個(gè)子類維護(hù),會(huì)增加類的數(shù)量。為防止惡意操作,一般模板方法都以 final 修飾。
應(yīng)用舉例
每一個(gè) Servlet 都必須實(shí)現(xiàn) Servlet 接口,GenericServlet 是實(shí)現(xiàn)了該接口的通用抽象類,而 HttpServlet 繼承了 GenericServlet,提供了處理 HTTP 協(xié)議的通用實(shí)現(xiàn),所以一般定義 Servlet 只需要繼承 HttpServlet 即可。
HttpServlet 定義了一套處理 HTTP 請(qǐng)求的模板,service 方法為模板方法,定義了處理HTTP請(qǐng)求的基本流程,doXXX 等方法為基本方法,根據(jù)請(qǐng)求方法的類型做相應(yīng)的處理,子類可重寫這些方法。
P12:觀察者模式
觀察者模式屬于行為型模式,也叫發(fā)布訂閱模式,定義對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新。主要解決一個(gè)對(duì)象狀態(tài)改變給其他對(duì)象通知的問題,缺點(diǎn)是如果一個(gè)被觀察者對(duì)象有很多的直接和間接觀察者的話,通知很耗時(shí), 并且如果存在循環(huán)依賴的話可能導(dǎo)致系統(tǒng)崩潰,另外觀察者無法知道目標(biāo)對(duì)象具體是怎么發(fā)生變化的。
應(yīng)用舉例
ServletContextListener 能夠監(jiān)聽 ServletContext 對(duì)象的生命周期,實(shí)際上就是監(jiān)聽 Web 應(yīng)用。在 ServletContextListener 中定義了處理對(duì)應(yīng)事件的兩個(gè)方法,當(dāng) Servlet 容器啟動(dòng) Web 應(yīng)用時(shí)調(diào)用 contextInitialized 方法,終止 Web 應(yīng)用時(shí)調(diào)用 contextDestroyed 方法。
Java 基礎(chǔ) 22
P1:基本概念
優(yōu)點(diǎn)
① 具有平臺(tái)無關(guān)性,擺脫了硬件平臺(tái)的束縛,實(shí)現(xiàn)了"一次編寫,到處運(yùn)行"。
② 提供了一種相對(duì)安全的內(nèi)存管理和訪問機(jī)制,避免了大部分內(nèi)存泄漏和指針越界問題。
③ 實(shí)現(xiàn)了熱點(diǎn)代碼檢測(cè)和運(yùn)行時(shí)編譯及優(yōu)化,使得程序隨運(yùn)行時(shí)間增長(zhǎng)可以獲得更高的性能。
④ 有一套完善的應(yīng)用程序接口,還支持很多第三方類庫(kù)。
Java 平臺(tái)無關(guān)性原理
主要通過 JVM 和語言規(guī)范實(shí)現(xiàn)。
- 編譯器生成一個(gè)體系結(jié)構(gòu)中立的目標(biāo)文件格式,這是一種編譯后的代碼,只要有 Java 運(yùn)行時(shí)系統(tǒng),這些編譯后的代碼就可以在很多處理器上運(yùn)行。Java 編譯器通過生成與特定計(jì)算機(jī)體系結(jié)構(gòu)無關(guān)的字節(jié)碼指令來實(shí)現(xiàn)這一特性,字節(jié)碼文件不僅可以很容易地在任何機(jī)器上解釋執(zhí)行,還可以動(dòng)態(tài)地轉(zhuǎn)換成本地機(jī)器代碼,轉(zhuǎn)換是由 JVM 實(shí)現(xiàn)的,JVM 是平臺(tái)相關(guān)的,屏蔽了不同操作系統(tǒng)的差異。
- Java 中基本數(shù)據(jù)類型的大小以及有關(guān)運(yùn)算的行為都有明確的說明,例如 int 類型永遠(yuǎn)為 32 位整數(shù),而在 C/C++ 中可能是 16 位整數(shù)、32 位整數(shù),也可能是編譯器開發(fā)商指定的其他任何大小。在 Java 中數(shù)值類型有固定的字節(jié)數(shù),二進(jìn)制數(shù)據(jù)以固定的格式進(jìn)行存儲(chǔ)和傳輸,字符串則采用標(biāo)準(zhǔn)的 Unicode 格式存儲(chǔ)。
JDK 和 JRE 的區(qū)別
JDK:Java Development Kit,Java 開發(fā)工具包。它提供了編譯、運(yùn)行 Java 程序所需的各種工具和資源,包括 Java 編譯器、JRE 以及常用的基礎(chǔ)類庫(kù)等,是 JAVA 的核心。
JRE:Java Runtime Environment,Java 運(yùn)行時(shí)環(huán)境,是運(yùn)行基于 Java 語言編寫的程序所不可缺少的運(yùn)行環(huán)境,包括 JVM、核心類庫(kù)、核心配置工具等。
P2:數(shù)據(jù)類型
基本數(shù)據(jù)類型
JVM 并沒有針對(duì) boolean 數(shù)據(jù)類型進(jìn)行賦值的專用字節(jié)碼指令,boolean f = false 就是使用 ICONST_0,即常數(shù) 0 來進(jìn)行賦值。單個(gè)boolean 變量用 int 代替,而 boolean 數(shù)組會(huì)編碼成 byte 數(shù)組。
每個(gè)基本數(shù)據(jù)類型都對(duì)應(yīng)一個(gè)自己的包裝類,除了 int 和 char 對(duì)應(yīng) Integer 和 Character 之外,其余基本數(shù)據(jù)類型的包裝類都是首字母大寫即可。自動(dòng)裝箱指的是將基本數(shù)據(jù)類型包裝為一個(gè)包裝類對(duì)象,例如向一個(gè)泛型為 Integer 類型的集合添加 int 類型的元素。自動(dòng)拆箱指的是將一個(gè)包裝類對(duì)象轉(zhuǎn)換為一個(gè)基本數(shù)據(jù)類型,例如將一個(gè)包裝類對(duì)象賦值給一個(gè)基本數(shù)據(jù)類型的變量。要比較兩個(gè)包裝類的數(shù)值需要使用 equals 方法,而不能使用 == 比較運(yùn)算符。
所有的 POJO 類屬性必須使用包裝數(shù)據(jù)類型,RPC 方法的返回值和參數(shù)必須使用包裝數(shù)據(jù)類型,所有局部變量推薦使用基本數(shù)據(jù)類型。
引用數(shù)據(jù)類型
引用數(shù)據(jù)類型分為引用變量本身和引用指向的對(duì)象,引用變量稱為 refvar,引用指向的實(shí)際對(duì)象稱為 refobj。
refvar 是基本的數(shù)據(jù)類型,它的默認(rèn)值是 null,存儲(chǔ) refobj 的首地址,可以直接使用雙等號(hào) == 進(jìn)行等值判斷。作為一個(gè)引用變量,不管它指向包裝類、集合類、字符串還是自定義類,均占 4B 空間。無論 refobj 是多么小的對(duì)象,最小的占用空間是 12B(用于存儲(chǔ)基本信息,稱為對(duì)象頭),但由于存儲(chǔ)空間分配必須是 8B 的倍數(shù),所以初始分配空間至少是 16B。
一個(gè) refvar 至多存儲(chǔ)一個(gè) refobj 的首地址,一個(gè) refobj 可以被多個(gè) refvar 存儲(chǔ)下它的首地址,即一個(gè)堆內(nèi)對(duì)象可以被多個(gè) refvar 引用指向。如果 refobj 沒有被任何 refvar 指向,那么遲早會(huì)被垃圾回收。
P3:String
String 是 final 修飾的不可變只讀字符串類,存儲(chǔ)數(shù)據(jù)的 value 字符數(shù)組也是 final 修飾的不可變數(shù)組。String 對(duì)象是不可變對(duì)象,對(duì)它的任何修改實(shí)際上都是創(chuàng)建一個(gè)新對(duì)象,再把引用指向該對(duì)象。String 對(duì)象賦值操作后,會(huì)在常量池中進(jìn)行緩沖,如果下次申請(qǐng)創(chuàng)建對(duì)象時(shí),緩存中已經(jīng)存在就直接返回相應(yīng)引用給創(chuàng)建者。
直接使用 + 進(jìn)行字符串拼接,如果是字面量會(huì)自動(dòng)拼接為一個(gè)新的常量,但如果在循環(huán)體內(nèi)拼接效率就很低。要提升拼接效率可以使用 StringBuilder 或 StringBuffer 可變字符串,可以直接在原對(duì)象上進(jìn)行修改,區(qū)別是 StringBuffer 使用了 synchronized 保證線程安全性,但一般字符串拼接都是單線程操作,所以使用 StringBuilder 較多。常量和常量的拼接,結(jié)果也在常量池中,且不存在兩個(gè)相同的常量。只要參與拼接的字符串里有變量,結(jié)果就在堆中。
如果是通過字符串常量賦值的形式,例如 String s = “s”,字符串常量?jī)?nèi)容存于常量池,變量存于棧中并直接引用常量池中的字符串。如果是通過new 的形式,例如 String s = new String(“s”),會(huì)先在堆中創(chuàng)建實(shí)例對(duì)象,然后再去常量池尋找需要的字符串常量,如果找到了則直接使用,沒找到則開辟新的空間并存儲(chǔ)內(nèi)容,最后棧中變量引用堆中對(duì)象,對(duì)象再引用常量池中的字符串。
P4:值調(diào)用和引用調(diào)用
按值調(diào)用指的是方法接收的是調(diào)用者提供的值,而按引用調(diào)用指的是方法接收的是調(diào)用者提供的變量地址。Java 總是采用按值調(diào)用,也就是說方法得到的是所有參數(shù)值的一個(gè)副本,當(dāng)傳遞對(duì)象時(shí)實(shí)際上方法接收的是這個(gè)對(duì)象引用的副本。方法不能修改基本數(shù)據(jù)類型的參數(shù),可以改變對(duì)象參數(shù)的狀態(tài),但不能讓對(duì)象參數(shù)引用一個(gè)新的對(duì)象。
舉例來說,如果傳遞了一個(gè) int 類型的值 ,改變?cè)撝挡粫?huì)影響實(shí)參,因?yàn)楦淖兊氖窃撝档囊粋€(gè)副本。如果傳遞了一個(gè) int[] 類型的數(shù)組,改變數(shù)組的內(nèi)容會(huì)影響實(shí)參,而如果改變這個(gè)參數(shù)的引用,并不會(huì)讓實(shí)參引用新的數(shù)組對(duì)象。
P5:面向?qū)ο?/font>
概念
面向過程讓計(jì)算機(jī)有步驟地順序做一件事,是一種過程化思維,在使用面向過程語言開發(fā)大型項(xiàng)目時(shí),軟件復(fù)用和維護(hù)存在很大問題,模塊之間耦合嚴(yán)重。面向?qū)ο笙鄬?duì)于面向過程而言更適合解決規(guī)模較大的問題,可以拆解問題復(fù)雜度,對(duì)現(xiàn)實(shí)事物進(jìn)行抽象并映射為開發(fā)對(duì)象,更接近人的思維。
例如開門這個(gè)動(dòng)作,面向過程是"open(Door door)",是一種動(dòng)賓結(jié)構(gòu),“door"是被作為操作對(duì)象的參數(shù)傳入方法的,方法內(nèi)定義開門的具體步驟實(shí)現(xiàn)。而使用面向?qū)ο蟮姆绞?#xff0c;首先會(huì)定義一個(gè)類"Door”,然后抽象出門的屬性(如尺寸、顏色)和行為(如 open 和 close),屬于主謂結(jié)構(gòu)。面向過程的代碼結(jié)構(gòu)松散,強(qiáng)調(diào)如何流程化地解決問題。面向?qū)ο蟮拇a強(qiáng)調(diào)高內(nèi)聚、低耦合,先抽象模型,定義共性行為,再解決實(shí)際問題。
封裝
封裝是一種對(duì)象功能內(nèi)聚的表現(xiàn)形式,在抽象基礎(chǔ)上決定信息是否公開,以及公開等級(jí),核心問題是以什么樣的方式暴漏哪些信息。封裝的主要任務(wù)是對(duì)屬性、數(shù)據(jù)、部分內(nèi)部敏感行為實(shí)現(xiàn)隱藏,對(duì)熟悉的訪問和修改必須通過公共接口來實(shí)現(xiàn),某些敏感方法或外部不需要感知的復(fù)雜邏輯一般也會(huì)進(jìn)行封裝。封裝使對(duì)象之間的關(guān)系變得簡(jiǎn)單,降低了代碼耦合度,有利維護(hù)。
設(shè)計(jì)模式原則的迪米特原則就是對(duì)封裝的具體要求,即 A 模塊使用 B 模塊的某個(gè)接口行為,對(duì) B 模塊中除此行為之外的其他信息知道的應(yīng)該盡可能少。之所以不直接對(duì) public 的屬性進(jìn)行讀取和修改,而是使用對(duì)應(yīng)的 getter/setter 方法,是因?yàn)榧僭O(shè)想在修改屬性時(shí)進(jìn)行權(quán)限控制、日志記錄等操作,在直接訪問屬性的情況下是無法做到的。如果將公開的屬性和行為修改為 private 則依賴模塊都會(huì)報(bào)錯(cuò),因此在不知道使用哪種訪問控制權(quán)限時(shí)應(yīng)當(dāng)優(yōu)先使用 private。
繼承
可以通過繼承來擴(kuò)展一個(gè)類,子類可以繼承父類的部分屬性和行為,使模塊具有復(fù)用性。繼承是一種"is-a"的關(guān)系,可以使用里氏替換原則判斷是否滿足"is-a"關(guān)系,即任何父類出現(xiàn)的地方子類都可以出現(xiàn)。如果父類引用直接使用子類引用來代替,可以正確編譯并執(zhí)行,輸出結(jié)果符合子類場(chǎng)景的預(yù)期,那么說明兩個(gè)類符合里氏替換原則,可以使用繼承關(guān)系。
多態(tài)
多態(tài)是以封裝和繼承為基礎(chǔ),根據(jù)運(yùn)行時(shí)的實(shí)際對(duì)象類型,同一個(gè)方法產(chǎn)生不同的運(yùn)行結(jié)果,使同一個(gè)行為具有不同的表現(xiàn)形式。多態(tài)是指在編譯層面無法確定最終調(diào)用的方法體,以重寫為基礎(chǔ)來實(shí)現(xiàn)面向?qū)ο筇匦?#xff0c;在運(yùn)行期由 JVM 進(jìn)行動(dòng)態(tài)綁定,調(diào)用合適的重寫方法體來執(zhí)行。由于重載是編譯期確定方法調(diào)用,屬于靜態(tài)綁定,本質(zhì)上重載的結(jié)果是完全不同的方法,因此一般多態(tài)專指重寫。
-
重載
重載是指方法名稱相同,但是參數(shù)類型或參數(shù)個(gè)數(shù)不相同,是水平方向上行為的不同實(shí)現(xiàn)。對(duì)于編譯器來說,方法名稱和參數(shù)列表組成了一個(gè)唯一鍵,稱為方法簽名,JVM 通過這個(gè)唯一鍵決定調(diào)用哪種重載的方法。
JVM 在重載方法中,選擇合適的目標(biāo)方法的順序:① 精確匹配。② 如果是基本數(shù)據(jù)類型,自動(dòng)轉(zhuǎn)換成更大表示范圍的基本類型。③ 通過自動(dòng)拆箱與裝箱。④ 通過子類向上轉(zhuǎn)型繼承路線依次匹配。⑤ 通過可變參數(shù)匹配。
不管繼承關(guān)系如何復(fù)雜,重載在編譯時(shí)可以根據(jù)規(guī)則知道調(diào)用哪種目標(biāo)方法,因此重載屬于靜態(tài)綁定。 -
重寫
重寫是指子類實(shí)現(xiàn)接口或者繼承父類時(shí),保持方法簽名完全相同,實(shí)現(xiàn)不同的方法體,是垂直方向上行為的不同實(shí)現(xiàn)。
元空間有一個(gè)方法表保存著每個(gè)可以實(shí)例化類的方法信息,JVM 可以通過方法表快速激活實(shí)例方法。如果某個(gè)類重寫了父類的某個(gè)方法,則方法表中的方法指向引用會(huì)指向子類的實(shí)現(xiàn)處。需要注意父類引用執(zhí)行子類方法時(shí)無法調(diào)用子類存在而父類不存在的方法。
重寫的子類方法訪問權(quán)限不能變小,返回類型和拋出的異常類型不能變大,且必須加 @Override 注解。
P6:訪問權(quán)限控制符
P7:Object 類
Object 的類是所有類的父類,Object 類的方法:
- equals:用于檢測(cè)一個(gè)對(duì)象是否等于另一個(gè)對(duì)象,默認(rèn)使用 == 比較兩個(gè)對(duì)象的引用,可以重寫 equals 方法自定義比較規(guī)則。equals 方法需要滿足以下規(guī)范:自反性、對(duì)稱性、傳遞性、一致性并對(duì)于任何非空引用 x,x.equals(null) 返回 false。
- hashCode:散列碼是由對(duì)象導(dǎo)出的一個(gè)整型值,是沒有規(guī)律的,每個(gè)對(duì)象都有一個(gè)默認(rèn)的散列碼,值由對(duì)象的存儲(chǔ)地址得出。字符串可能有相同的散列碼,因?yàn)樽址纳⒘写a是由內(nèi)容導(dǎo)出的。為了在集合中正確使用對(duì)象,一般需要同時(shí)重寫 equals 和 hashCode 方法,要求是 equals 相同是 hashCode 必須相同,但 hashCode 相同時(shí) equals 未必相同,因此 hashCode 是兩個(gè)對(duì)象相同的必要不充分條件。
- toString:打印對(duì)象時(shí)默認(rèn)會(huì)調(diào)用它的 toString 方法,如果沒有重寫該方法默認(rèn)打印的是表示對(duì)象值的一個(gè)字符串,一般需要重寫該方法。打印數(shù)組時(shí)可以使用 Arrays.toString() 方法。
- clone:clone 方法聲明為 protected,類只能通過該方法克隆它自己的對(duì)象,如果希望其他類也能調(diào)用該方法必須定義該方法為 public。如果一個(gè)對(duì)象的類沒有實(shí)現(xiàn) Cloneable 接口,該對(duì)象調(diào)用 clone 方法會(huì)拋出一個(gè) CloneNotSupport 異常。默認(rèn)的 clone 方法是淺拷貝,一般重寫 clone 方法需要實(shí)現(xiàn) Cloneable 接口并指定訪問修飾符為 public。
- 淺拷貝:只復(fù)制當(dāng)前對(duì)象的基本數(shù)據(jù)類型以及相應(yīng)的引用變量,但沒有復(fù)制引用變量指向的實(shí)際對(duì)象。對(duì)克隆對(duì)象的修改可能會(huì)影響原對(duì)象,是不安全的。
- 深拷貝:會(huì)完全拷貝基本數(shù)據(jù)類型和引用數(shù)據(jù)類型,深拷貝是安全的。
- finalize:要確定一個(gè)對(duì)象死亡至少要經(jīng)過兩次標(biāo)記,如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與 GC Roots 連接的引用鏈,會(huì)被第一次標(biāo)記,隨后進(jìn)行一次篩選,篩選條件是此對(duì)象是否有必要執(zhí)行 finalize 方法。假如對(duì)象沒有重寫 finalize 方法或者該方法已經(jīng)被虛擬機(jī)調(diào)用過,這兩種情況視為沒有必要執(zhí)行。如果判斷為有必要執(zhí)行,對(duì)象就會(huì)被放置在一個(gè)叫做 F-Queue 的隊(duì)列中,由一條虛擬機(jī)自動(dòng)建立的低調(diào)度優(yōu)先級(jí)的 Finalizer 線程去執(zhí)行其 finalize 方法。虛擬機(jī)會(huì)觸發(fā)該方法但不保證它會(huì)運(yùn)行結(jié)束,這是為了防止某個(gè)對(duì)象的 finalize 方法執(zhí)行緩慢或發(fā)生死循環(huán)。只要對(duì)象在 finalize 方法中重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)就會(huì)在第二次標(biāo)記時(shí)被移出即將回收集合。由于運(yùn)行代價(jià)高昂,不確定性大,無法保證各個(gè)對(duì)象的調(diào)用順序,在 JDK 9 已經(jīng)被標(biāo)記為過時(shí)方法,因此它并不適合釋放資源,釋放資源可以使用 try-finally 代碼塊。
- getClass:返回包含對(duì)象信息的類對(duì)象。
- wait / notify / notifyAll:阻塞或喚醒持有該對(duì)象鎖的線程。
P8:內(nèi)部類
使用內(nèi)部類主要有兩個(gè)原因:內(nèi)部類可以對(duì)同一個(gè)包中的其他類隱藏。內(nèi)部類方法可以訪問定義這個(gè)內(nèi)部類的作用域中的數(shù)據(jù),包括原本私有的數(shù)據(jù)。內(nèi)部類是一個(gè)編譯器現(xiàn)象,與虛擬機(jī)無關(guān)。編譯器會(huì)把內(nèi)部類轉(zhuǎn)換成常規(guī)的類文件,用美元符號(hào) $ 分隔外部類名與內(nèi)部類名,其中匿名內(nèi)部類使用數(shù)字進(jìn)行編號(hào),虛擬機(jī)對(duì)此一無所知。
靜態(tài)內(nèi)部類:由 static 修飾,屬于外部類本身,只加載一次。靜態(tài)內(nèi)部類的好處是作用域僅在包內(nèi),可以通過 外部類.內(nèi)部類 直接訪問,并且靜態(tài)內(nèi)部類可以訪問外部類中所有靜態(tài)屬性和方法。例如 HashMap 中的 Node 節(jié)點(diǎn),ReentrantLock 中繼承自 AQS 的 Sync 類,ArrayList 中的 SubList 都是靜態(tài)內(nèi)部類。內(nèi)部類中還可以定義內(nèi)部類,例如 ThreadLoacl 靜態(tài)內(nèi)部類 ThreadLoaclMap 中還定義了一個(gè)內(nèi)部類 Entry。
成員內(nèi)部類:屬于外部類的每個(gè)對(duì)象,隨對(duì)象一起加載。不可以定義靜態(tài)成員和方法,可以訪問外部類的所有內(nèi)容。
局部?jī)?nèi)部類:定義在方法或者表達(dá)式內(nèi)部。不能聲明訪問修飾符,只能定義實(shí)例成員變量和實(shí)例方法,作用范圍僅在聲明這個(gè)局部類的代碼塊中。
匿名內(nèi)部類:沒有名字的局部?jī)?nèi)部類,可以簡(jiǎn)化代碼,匿名內(nèi)部類會(huì)立即創(chuàng)建一個(gè)匿名內(nèi)部類的對(duì)象返回,對(duì)象類型相當(dāng)于當(dāng)前 new 的類的子類類型。匿名內(nèi)部類一般用于實(shí)現(xiàn)事件監(jiān)聽器和其他回調(diào)。
class OuterClass{static class StaticInnerClass {}class NormalInnerClass {}public void test() {class LocalClass {}// 靜態(tài)內(nèi)部類創(chuàng)建對(duì)象new OuterClass.StaticInnerClass();// 成員內(nèi)部類創(chuàng)建對(duì)象new OuterClass().new NormalInnerClass();// 局部?jī)?nèi)部類創(chuàng)建對(duì)象new LocalClass();// 匿名內(nèi)部類創(chuàng)建對(duì)象Runnable runnable = () -> {};}}P9:接口和抽象類
定義類的過程就是抽象和封裝的過程,而接口和抽象類則是對(duì)實(shí)體類進(jìn)行更高層次的抽象,僅定義公共行為和特征。
抽象類在被繼承時(shí)體現(xiàn)的是 is-a 關(guān)系,接口在被實(shí)現(xiàn)時(shí)體現(xiàn)的是 can-do 關(guān)系。與接口相比,抽象類通常是對(duì)同類事物相對(duì)具體的抽象,通常包含抽象方法、實(shí)體方法、屬性變量。is-a 關(guān)系需要符合里氏替換原則,can-do 關(guān)系要符合接口隔離原則,實(shí)現(xiàn)類要有能力去實(shí)現(xiàn)并執(zhí)行接口定義的行為。例如 Plane 和 Bird 都具有 fly 方法,應(yīng)該把 fly 定義為一個(gè)接口,而不是作為某個(gè)抽象類的方法,再利用 is-a 關(guān)系去繼承抽象類,因?yàn)槌?fly 這個(gè)行為外,Plane 和 Bird 之間很難找到其他共同特征。
抽象類是模板式設(shè)計(jì),包含一組具體的特征,例如某品牌特定型號(hào)的汽車,底盤、控制電路、剎車系統(tǒng)等是抽象出來的共同特征,但根據(jù)不同級(jí)別的配置,內(nèi)飾,顯示屏,座椅材質(zhì)可以存在不同版本的實(shí)現(xiàn)。
接口是契約式設(shè)計(jì),是開放的,就像一份合同,定義了方法名、參數(shù)、返回值甚至拋出的異常類型,誰都可以實(shí)現(xiàn)它,但必須要遵守這個(gè)接口的約定。例如所有車輛都必須實(shí)現(xiàn)剎車這個(gè)強(qiáng)制行為規(guī)范。
接口是頂級(jí)的類,雖然關(guān)鍵詞是 interface,但編譯之后的字節(jié)碼擴(kuò)展名還是 .class,抽象類在接口下面的第二層,對(duì)各個(gè)接口進(jìn)行了組合,然后實(shí)現(xiàn)部分接口。當(dāng)糾結(jié)定義接口和抽象類時(shí),推薦定義為接口,遵循接口隔離原則,按某個(gè)維度劃分成多個(gè)接口,然后再利用抽象類去實(shí)現(xiàn)這些接口,這樣做有利于后續(xù)的擴(kuò)展和重構(gòu)。
P10:static 關(guān)鍵字
static 關(guān)鍵字主要有兩個(gè)作用:(1)為某特定數(shù)據(jù)類型或?qū)ο蠓峙鋯我坏拇鎯?chǔ)空間,而與創(chuàng)建對(duì)象的個(gè)數(shù)無關(guān)。(2)讓某個(gè)屬性或方法與類而不是對(duì)象關(guān)聯(lián)在一起,可以在不創(chuàng)建對(duì)象的情況下通過類名來訪問。
作用范圍
static 修飾的變量稱為靜態(tài)變量,也叫類變量,可以直接通過類名訪問,靜態(tài)變量存儲(chǔ)在 JVM 的方法區(qū)中。
static 修飾的方法稱為靜態(tài)方法,也叫類方法,可以直接通過類名訪問,靜態(tài)方法只能訪問靜態(tài)變量或靜態(tài)方法,不能訪問實(shí)例成員變量和實(shí)例方法,也不能使用 this 和 super 關(guān)鍵字,通常用于定義工具類方法。
static 修飾的代碼塊稱為靜態(tài)代碼塊,只能定義在類下,在類加載時(shí)執(zhí)行且只會(huì)執(zhí)行一次,通常用于初始化屬性和環(huán)境配置等。
static 修飾的類稱為靜態(tài)內(nèi)部類,可以訪問外部類的靜態(tài)變量和方法。
static 也可以用來導(dǎo)入包下的靜態(tài)變量。
類初始化的順序
(1)父類靜態(tài)代碼塊和靜態(tài)變量
(2)子類靜態(tài)代碼塊和靜態(tài)變量
(3)父類普通代碼塊和普通變量
(4)父類構(gòu)造方法
(5)子類普通代碼塊和普通變量
(6)子類構(gòu)造方法
其中代碼塊和變量的初始化順序按照類中聲明的順序執(zhí)行。
P11:序列化和反序列化
Java 對(duì)象在 JVM 運(yùn)行時(shí)被創(chuàng)建,當(dāng) JVM 退出時(shí)存活對(duì)象都會(huì)銷毀,如果需要將對(duì)象及其狀態(tài)持久化,就需要通過序列化來實(shí)現(xiàn),將內(nèi)存中的對(duì)象保存在二進(jìn)制流中,在需要時(shí)再將二進(jìn)制流反序列化為對(duì)象。對(duì)象序列化保存的是對(duì)象的狀態(tài),因此類中的靜態(tài)變量不會(huì)被序列化,因?yàn)殪o態(tài)變量是類屬性。除了靜態(tài)變量外,transient 修飾的變量也不會(huì)被序列化。transient 的作用就是把這字段的生命周期僅限于內(nèi)存中而不會(huì)寫到磁盤里持久化,被 transient 修飾的變量會(huì)被設(shè)為對(duì)應(yīng)數(shù)據(jù)類型的默認(rèn)初始值。
序列化的常用常見是 RPC 框架的數(shù)據(jù)傳輸,常見的序列化有三種:
- Java 原生序列化
實(shí)現(xiàn) Serializabale 標(biāo)記接口,Java 序列化保留了對(duì)象類的元數(shù)據(jù)(如類、成員變量、繼承類信息等),以及對(duì)象數(shù)據(jù)等,兼容性最好,但不支持跨語言,性能一般。序列化和反序列化必須保持序列化 ID 的一致,一般使用 private static final long serialVersionUID 定義序列化 ID,如果不設(shè)置編譯器會(huì)根據(jù)類的內(nèi)部實(shí)現(xiàn)自動(dòng)生成該值。如果是兼容升級(jí)不應(yīng)該修改序列化 ID,如果是不兼容升級(jí)則需要修改。使用 Java 原生序列化不會(huì)調(diào)用類的無參構(gòu)造方法,而是調(diào)用本地方法將成員變量賦值為對(duì)應(yīng)類型的初始值,基于性能一般不推薦使用。 - Hessian 序列化
Hessian 序列化是一種支持動(dòng)態(tài)類型、跨語言、基于對(duì)象傳輸?shù)木W(wǎng)絡(luò)協(xié)議。Java 對(duì)象序列化的二進(jìn)制流可以被其它語言反序列化。Hessian 協(xié)議的特性:① 自描述序列化類型,不依賴外部描述文件或接口定義,用一個(gè)字節(jié)表示常用基礎(chǔ)類型,極大縮短二進(jìn)制流。② 語言無關(guān),支持腳本語言。③ 協(xié)議簡(jiǎn)單,比 Java 原生序列化高效。Hessian 會(huì)把復(fù)雜對(duì)象所有屬性存儲(chǔ)在一個(gè) Map 中進(jìn)行序列化,當(dāng)父類和子類存在同名成員變量時(shí)會(huì)先序列化子類,再序列化父類,因此子類值會(huì)被父類覆蓋。 - JSON 序列化
JSON 是一種輕量級(jí)的數(shù)據(jù)格式,JSON 序列化就是將數(shù)據(jù)對(duì)象轉(zhuǎn)換為 JSON 字符串,在序列化過程中拋棄了類型信息,所以反序列化時(shí)只有提供類型信息才能準(zhǔn)確進(jìn)行。相比前兩種方式可讀性更好,方便調(diào)試。
序列化通常會(huì)使用網(wǎng)絡(luò)傳輸對(duì)象,而對(duì)象中往往有敏感數(shù)據(jù),容易遭受攻擊,Jackson 和 fastjson 等都出現(xiàn)過反序列化漏洞,因此有些對(duì)象的敏感屬性不需要進(jìn)行序列化傳輸時(shí)應(yīng)該加上 transient 關(guān)鍵字。如果一定要傳遞敏感屬性可以使用對(duì)稱與非對(duì)稱加密方式獨(dú)立傳輸,再使用某個(gè)方法把屬性還原到對(duì)象中。
P12:反射
在運(yùn)行狀態(tài)中,對(duì)于任意一個(gè)類,都能夠知道這個(gè)類的所有屬性和方法,對(duì)于任意一個(gè)對(duì)象,都能夠調(diào)用它的任意一個(gè)方法和屬性;這種動(dòng)態(tài)獲取的信息以及動(dòng)態(tài)調(diào)用對(duì)象的方法的功能稱為Java的反射機(jī)制。優(yōu)點(diǎn)是運(yùn)行時(shí)動(dòng)態(tài)獲取類的全部信息,缺點(diǎn)是破壞了類的封裝性,泛型的約束性。反射是框架的核心靈魂,動(dòng)態(tài)代理設(shè)計(jì)模式采用了反射機(jī)制,還有 Spring、Hibernate 等框架也大量使用到了反射機(jī)制。
在程序運(yùn)行期間,Java 運(yùn)行時(shí)系統(tǒng)始終為所有對(duì)象維護(hù)一個(gè)運(yùn)行時(shí)類型標(biāo)識(shí),這個(gè)信息會(huì)跟蹤每個(gè)對(duì)象所屬的類,虛擬機(jī)利用運(yùn)行時(shí)類型信息選擇要執(zhí)行的正確方法,保存這些信息的類名為 Class。
獲取 Class 實(shí)例的方法有三種:① 直接通過 類名.class 。②通過對(duì)象的 getClass()方法。③通過 Class.forName(類的全限定名)。Class 類中的 getFields、getMethods 和 getConstructors 方法分別返回這個(gè)類支持的公共字段、方法和構(gòu)造方法的數(shù)組,其中包括父類的公共成員。Class 類中的 getDeclaredFields、getDeclaredMethods 和 getDeclaredConstructors方法分別返回這個(gè)類聲明的全部字段、方法和構(gòu)造方法的數(shù)組,其中包括私有成員、包成員和受保護(hù)成員,但不包括父類的成員。
Field、Method、Constructor 分別用于描述類的字段、方法和構(gòu)造方法。這三個(gè)類都有一個(gè) getName 方法返回字段、方法或構(gòu)造方法的名稱。Field 類有一個(gè) getType 方法用來返回描述字段類型的一個(gè)對(duì)象,這個(gè)對(duì)象的類型也是 Class。Method 和 Constructor 類有報(bào)告參數(shù)類型的方法,Method 類還有一個(gè)報(bào)告返回類型的方法。這三個(gè)類都有一個(gè) getModifiers 方法,它返回一個(gè)整數(shù),用不同的 0/1 位描述所使用的修飾符。
P13:注解
注解是一種標(biāo)記,可以使類或接口附加額外的信息,是幫助編譯器和 JVM 完成一些特定功能的,例如常用注解 @Override 標(biāo)識(shí)一個(gè)方法是重寫方法。
元注解就是自定義注解的注解,包括:
- @Target:用來約束注解作用的位置,值是枚舉類 ElementType 的枚舉實(shí)例,包括 METHOD 方法、VARIABLE 變量、TYPE 類/接口、PARAMETER 方法參數(shù)、CONSTRUCTORS 構(gòu)造方法和 LOACL_VARIABLE 局部變量等。
- @Rentention:用來約束注解的生命周期,值是 RetentionPolicy 枚舉類實(shí)例,包括:SOURCE 源碼、CLASS 字節(jié)碼和 RUNTIME 運(yùn)行時(shí)。
- @Documented:表明這個(gè)注解應(yīng)該被 javadoc 工具記錄。
- @Inherited:表面某個(gè)被標(biāo)注的類型是被繼承的。
P14:異常
所有的異常都是 Throwable 的子類,分為 Error 和 Exception。Error 描述了 Java 運(yùn)行時(shí)系統(tǒng)的內(nèi)部錯(cuò)誤和資源耗盡錯(cuò)誤,例如 StackOverFlowException 和 OutOfMemoryException,這種異常程序無法處理。Exception 又分為受檢異常和非受檢異常,受檢異常是需要在代碼中顯式處理的異常,否則會(huì)編譯出錯(cuò),非受檢異常是運(yùn)行時(shí)異常,繼承自 RuntimeException。
受檢異常又可以分為:① 無能為力型,程序無法處理,例如字段超長(zhǎng)導(dǎo)致的 SQLException。一般處理的方法是完整保存異?,F(xiàn)場(chǎng),供開發(fā)人員解決。② 力所能及型,如發(fā)生未授權(quán)異常 UnAuthorizedException,程序可跳轉(zhuǎn)至權(quán)限申請(qǐng)頁面。常見的受檢異常還有FileNotFoundException、ClassNotFoundException、IOException等。
非受檢異常又可以分為:① 可預(yù)測(cè)的異常,例如 IndexOutOfBoundsException、NullPointerException 等,這類異常應(yīng)該提前做好處理。② 需捕捉異常,例如進(jìn)行 RPC 調(diào)用時(shí)產(chǎn)生的遠(yuǎn)程服務(wù)超時(shí)異常,這類異常是客戶端必須顯式處理的。③ 可透出異常,主要指框架或系統(tǒng)產(chǎn)生的且會(huì)自行處理的異常,而程序無需關(guān)心,例如 Spring 中拋出的 NoSuchRequestHandingMethodException,Spring 會(huì)自動(dòng)完成異常處理,默認(rèn)將拋出的異常自動(dòng)映射到合適的狀態(tài)碼。
以坐飛機(jī)為例,機(jī)場(chǎng)地震屬于不可抗力,對(duì)應(yīng)了 Error。飛機(jī)延誤屬于受檢異常,應(yīng)對(duì)這種異常無能為力,堵車也屬于受檢異常,應(yīng)對(duì)這種異??梢蕴崆俺腔蚋暮灐]有帶身份證屬于可預(yù)測(cè)的異常,汽車突然拋錨屬于需捕捉異常,雖然難以預(yù)料但必須處理,檢票機(jī)器故障屬于可透出異常,由機(jī)場(chǎng)處理而無需我們關(guān)心。
異常處理
拋出異常:遇到異常不進(jìn)行具體處理,而是將異常拋出給調(diào)用者,由調(diào)用者根據(jù)情況處理。拋出異常有2種形式,一種是 throws 關(guān)鍵字聲明拋出的異常,作用在方法上,一種是使用throw 語句直接拋出異常,作用在方法內(nèi)。
捕獲異常:使用 try/catch 進(jìn)行異常的捕獲,try 中發(fā)生的異常會(huì)被 catch 代碼塊捕獲,根據(jù)情況進(jìn)行處理,如果有 finally 代碼塊無論是否發(fā)生異常都會(huì)執(zhí)行,一般用于釋放資源,Java 7 開始可以將資源定義在 try 代碼塊中自動(dòng)釋放資源。
P15:泛型
泛型的本質(zhì)是參數(shù)化類型,解決不確定具體對(duì)象類型的問題。泛型在定義處只具備執(zhí)行 Object 方法的能力。泛型的好處:① 類型安全,放置的是說明取出來就是什么,不存在 ClassCastException 異常。② 提升可讀性,從編碼階段就顯式地知道泛型集合、泛型方法等處理的對(duì)象類型是什么。③ 代碼重用,泛型合并了同類型的處理代碼。
類型擦除
虛擬機(jī)沒有泛型類型對(duì)象,所有對(duì)象都屬于普通類。無論何時(shí)定義一個(gè)泛型類型,都會(huì)自動(dòng)提供一個(gè)相應(yīng)的原始類型,原始類型的名字就是去掉類型參數(shù)后的泛型類型名。類型變量會(huì)被擦除,如果沒有限定類型就會(huì)替換為 Object,如果有限定類型就會(huì)替換為第一個(gè)限定類型,例如 <T extends A & B> 會(huì)使用 A 類型替換 T。
泛型主要用于編譯階段,在編譯后生成的 Java 字節(jié)代碼文件中不包含泛型中的類型信息。
泛型規(guī)范
泛型限定
對(duì)泛型上限的限定使用<? extends T>,它表示該通配符所代表的類型是 T 類的子類型或 T 接口的子接口。
對(duì)泛型下限的限定使用<? super T>,它表示該通配符所代表的類型是 T 類的父類型或 T 接口的父接口。
P16:Java 8 新特性
lambda 表達(dá)式:lambda 表達(dá)式允許把函數(shù)作為一個(gè)方法的參數(shù)傳遞到方法中,主要用來簡(jiǎn)化匿名內(nèi)部類的代碼。
函數(shù)式接口:使用 @FunctionalInterface 注解標(biāo)識(shí),有且僅有一個(gè)抽象方法,可以被隱式轉(zhuǎn)換為 lambda 表達(dá)式。
方法引用:可以直接引用已有類或?qū)ο蟮姆椒ɑ驑?gòu)造方法,進(jìn)一步簡(jiǎn)化 lambda 表達(dá)式。方法引用有四種形式:引用構(gòu)造方法、引用類的靜態(tài)方法、引用特定類的任意對(duì)象方法、引用某個(gè)對(duì)象的方法。
接口中的方法:接口中可以定義 default 修飾的默認(rèn)方法,降低了接口升級(jí)的復(fù)雜性,還可以定義靜態(tài)方法。
注解:Java 8 引入了重復(fù)注解機(jī)制,相同的注解在同一個(gè)地方可以聲明多次。注解的作用范圍也進(jìn)行了擴(kuò)展,可以作用于局部變量、泛型、方法異常等。
類型推測(cè):加強(qiáng)了類型推測(cè)機(jī)制,可以使代碼更加簡(jiǎn)潔,例如在定義泛型集合時(shí)可以省略對(duì)象中的泛型參數(shù)。
Optional 類:用來處理空指針異常,提高代碼可讀性。
Stream 類:把函數(shù)式編程風(fēng)格引入 Java 語言,提供了很多功能,可以使代碼更加簡(jiǎn)潔。方法包括 forEach 遍歷、count 統(tǒng)計(jì)個(gè)數(shù)、filter 按條件過濾、limit 取前 n 個(gè)元素、skip 跳過前 n 個(gè)元素、map 映射加工、concat 合并stream流等。
日期:增強(qiáng)了日期和時(shí)間的 API,新的 java.time 包主要包含了處理日期、時(shí)間、日期/時(shí)間、時(shí)區(qū)、時(shí)刻和時(shí)鐘等操作。
JavaScript:Java 8 提供了一個(gè)新的 JavaScript 引擎,它允許在 JVM上運(yùn)行特定的 JavaScript 應(yīng)用。
P17:Java IO
同步和異步是通信機(jī)制,阻塞和非阻塞是調(diào)用狀態(tài)。
同步 IO 是用戶線程發(fā)起 I/O 請(qǐng)求后需要等待或者輪詢內(nèi)核 I/O 操作完成后才能繼續(xù)執(zhí)行。
異步 IO 是用戶線程發(fā)起 I/O 請(qǐng)求后仍可以繼續(xù)執(zhí)行,當(dāng)內(nèi)核 I/O 操作完成后會(huì)通知用戶線程,或者調(diào)用用戶線程注冊(cè)的回調(diào)函數(shù)。
阻塞 IO 是指 I/O 操作需要徹底完成后才能返回用戶空間 。
非阻塞 IO 是指 I/O 操作被調(diào)用后立即返回一個(gè)狀態(tài)值,無需等 I/O 操作徹底完成。
BIO
同步阻塞式 IO,服務(wù)器實(shí)現(xiàn)模式為一個(gè)連接請(qǐng)求對(duì)應(yīng)一個(gè)線程,即客戶端有連接請(qǐng)求時(shí)服務(wù)器端就需要啟動(dòng)一個(gè)線程進(jìn)行處理,如果這個(gè)連接不做任何事情會(huì)造成不必要的線程開銷。可以通過線程池機(jī)制改善,這種 IO 稱為偽異步 IO。
主要分為字符流和字節(jié)流,字符流包括字符輸入流 Reader 和字符輸出流 Writer,字節(jié)流包括字節(jié)輸入流 InputStream 和 字節(jié)輸出流 OutputStream,字節(jié)流和字符流都有對(duì)應(yīng)的緩沖流和過濾流,也可以將字節(jié)流包裝為字符流。
適用場(chǎng)景:連接數(shù)目少、服務(wù)器資源多、開發(fā)難度低。
NIO
同步非阻塞 IO,服務(wù)器實(shí)現(xiàn)模式為多個(gè)連接請(qǐng)求對(duì)應(yīng)一個(gè)線程,客戶端發(fā)送的連接請(qǐng)求都會(huì)注冊(cè)到一個(gè)多路復(fù)用器 Selector 上,多路復(fù)用器輪詢到連接有I/O請(qǐng)求時(shí)才啟動(dòng)一個(gè)線程進(jìn)行處理,有數(shù)據(jù)才會(huì)開啟線程處理,性能比較好。
同步是指線程還是要不斷接收客戶端連接并處理數(shù)據(jù),非阻塞是指如果一個(gè)管道沒有數(shù)據(jù),不需要等待,可以輪詢下一個(gè)管道。
有三個(gè)核心組件:
Selector
選擇器或多路復(fù)用器,主要作用是輪詢檢查多個(gè) Channel 的狀態(tài),判斷 Channel 注冊(cè)的事件是否發(fā)生,即判斷 Channel 是否處于可讀或可寫狀態(tài)。在使用之前需要將 Channel 注冊(cè)到 Selector 上,注冊(cè)之后會(huì)得到一個(gè) SelectionKey,通過 SelectionKey 可以獲取 Channel 和 Selector 的相關(guān)信息。
Channel
雙向通道,替換了 IO 中的 Stream,不能直接訪問數(shù)據(jù),要通過 Buffer 來讀寫數(shù)據(jù),也可以和其他 Channel 交互。
FileChannel 處理文件、DatagramChannel 處理 UDP 數(shù)據(jù)、SocketChannel 處理 TCP 數(shù)據(jù),用作客戶端、ServerSocketChannel 處理 TCP 數(shù)據(jù),用作服務(wù)器端。
Buffer
緩沖區(qū),本質(zhì)是一塊可讀寫數(shù)據(jù)的內(nèi)存,這塊內(nèi)存被包裝成 NIO 的 Buffer 對(duì)象,用來簡(jiǎn)化數(shù)據(jù)的讀寫。Buffer 的三個(gè)重要屬性:position 表示下一次讀寫數(shù)據(jù)的位置,limit 表示本次讀寫的極限位置,capacity 表示最大容量。
- flip() 將寫轉(zhuǎn)為讀,底層實(shí)現(xiàn)原理是把 position 置 0,并把 limit 設(shè)為當(dāng)前的 position 值。
- 通過 clear() 將讀轉(zhuǎn)為寫模式(用于讀完全部數(shù)據(jù)的情況,把 position 置 0,limit 設(shè)為 capacity)。
- 通過 compact() 將讀轉(zhuǎn)為寫模式(用于沒有讀完全部數(shù)據(jù),存在未讀數(shù)據(jù)的情況,讓 position 指向未讀數(shù)據(jù)的下一個(gè))。
- 通道的方向和 Buffer 的方向是相反的,讀取數(shù)據(jù)相當(dāng)于向 Buffer 寫入,寫出數(shù)據(jù)相當(dāng)于從 Buffer 讀取。
使用步驟:向 Buffer 寫入數(shù)據(jù),調(diào)用 flip 方法將 Buffer 從寫模式切換為讀模式,從 Buffer 中讀取數(shù)據(jù),調(diào)用 clear 或 compact 方法來清空 Buffer。
適應(yīng)場(chǎng)景:連接數(shù)目多、連接時(shí)間短、開發(fā)難度高。
AIO
異步非阻塞 IO,服務(wù)器實(shí)現(xiàn)模式為一個(gè)有效請(qǐng)求對(duì)應(yīng)一個(gè)線程,客戶端的 I/O 請(qǐng)求都是由操作系統(tǒng)先完成 IO 操作后再通知服務(wù)器應(yīng)用來啟動(dòng)線程直接使用數(shù)據(jù)。
異步是指服務(wù)端線程接收到客戶端管道后就交給底層處理IO通信,自己可以做其他事情,非阻塞是指客戶端有數(shù)據(jù)才會(huì)處理,處理好再通知服務(wù)器。
AsynchronousServerSocketChannel 異步服務(wù)器端通道,通過靜態(tài)方法 open() 獲取實(shí)例,通過 accept 方法獲取客戶端連接通道。
AsynchronousSocketChannel 異步客戶端通道,通過靜態(tài)方法 open() 獲取實(shí)例,過 connect 方法連接服務(wù)器通道。
AsynchronousChannelGroup 異步通道分組管理器,它可以實(shí)現(xiàn)資源共享。創(chuàng)建時(shí)需要傳入一個(gè)ExecutorService,也就是綁定一個(gè)線程池,該線程池負(fù)責(zé)兩個(gè)任務(wù):處理 IO 事件和觸發(fā) CompletionHandler 回調(diào)接口。
實(shí)現(xiàn)方式:
通過 Future 的 get 方法進(jìn)行阻塞式調(diào)用。
通過實(shí)現(xiàn) CompletionHandler 接口,重寫請(qǐng)求成功的回調(diào)方法 completed() 和 請(qǐng)求失敗回調(diào)方法 failed()。
適用場(chǎng)景:連接數(shù)目多、連接時(shí)間長(zhǎng)、開發(fā)難度高。
P18:List
List 集合是線性數(shù)據(jù)結(jié)構(gòu)的主要實(shí)現(xiàn),集合元素通常存在明確的前驅(qū)和后繼元素以及首尾元素。List 集合的遍歷結(jié)果是穩(wěn)定的,最常用的是 ArrayList 和 LinkedList。
ArrayList
ArrayList 是容量可以改變的非線程安全集合,內(nèi)部使用數(shù)組進(jìn)行存儲(chǔ),集合擴(kuò)容時(shí)會(huì)創(chuàng)建更大的數(shù)組空間,把原有數(shù)組復(fù)制到新數(shù)組中。ArrayList 支持對(duì)元素的快速隨機(jī)訪問,但是插入與刪除時(shí)速度通常很慢。ArrayList 實(shí)現(xiàn)了 RandomAcess 標(biāo)記接口,如果一個(gè)類實(shí)現(xiàn)了該接口,那么表示這個(gè)類使用索引遍歷比迭代器更快。
三個(gè)重要的成員變量:
- elementData:ArrayList 的數(shù)據(jù)域,被 transient 修飾,表示在類的序列化時(shí)被忽視,集合序列化時(shí)會(huì)調(diào)用 writeObject 寫入流中,在網(wǎng)絡(luò)客戶端反序列化的 readObject 中會(huì)重新賦值到新對(duì)象的 elementData 中。之所以這樣做的原因是 elementData 容量通常會(huì)大于實(shí)際存儲(chǔ)元素的數(shù)量,所以只需發(fā)送真正有實(shí)際值的數(shù)組元素即可。
- size:表示當(dāng)前實(shí)際大小,elementData 的大小是大于等于 size 的。
- modCount:繼承自 AbstractList,記錄了結(jié)構(gòu)性變化的次數(shù),所有涉及結(jié)構(gòu)變化的方法都會(huì)增加該值。expectedModCount 是在迭代器初始化時(shí)記錄的 modCount 值,每次訪問新元素時(shí)都會(huì)檢查 modCount 和 expectedModCount 是否相等,如果不相等就會(huì)拋出異常。這種機(jī)制叫做 fail-fast,所有集合類都有這種機(jī)制。
可以使用線程安全的 CopyOnWriteArrayList 代替 ArrayList,它實(shí)現(xiàn)了讀寫分離。如果是寫操作則復(fù)制一個(gè)新的集合,在新集合內(nèi)添加或刪除元素,待修改完成之后再將原集合的引用指向新集合。這樣做的好處是可以高并發(fā)地進(jìn)行讀寫操作而不需要加鎖,因?yàn)楫?dāng)前集合不會(huì)添加任何元素。使用時(shí)注意盡量設(shè)置容量初始值,避免多次擴(kuò)容,并且可以使用批量添加或刪除,比如只增加一個(gè)元素卻復(fù)制整個(gè)集合。CopyOnWriteArrayList 適合讀多寫少的場(chǎng)景,單個(gè)添加時(shí)效率極低。CopyOnWriteArrayList 是 fail-safe 的,并發(fā)包的集合都是這種機(jī)制,fail-safe 是在安全的副本上進(jìn)行遍歷,集合修改與副本的遍歷沒有任何關(guān)系,但缺點(diǎn)就是無法讀取到最新的數(shù)據(jù)。這也是 CAP 理論中 C 和 A 的矛盾,即一致性與可用性的矛盾。
LinkedList
LinkedList 本質(zhì)是雙向鏈表,與 ArrayList 相比插入和刪除速度更快,但是隨機(jī)訪問元素則很慢。除了繼承自 AbstractList 外,它還實(shí)現(xiàn)了 Deque 接口,這個(gè)接口同時(shí)具有隊(duì)列和棧的性質(zhì)。成員變量被 transient 修飾,序列化原理和 ArrayList 類似。
LinkedList 包含三個(gè)重要的成員:size、first 和 last。size 是雙向鏈表中節(jié)點(diǎn)的個(gè)數(shù)。first 和 last 分別指向首尾節(jié)點(diǎn)的引用。
LinkedList 的優(yōu)點(diǎn)在于可以將零散的內(nèi)存單元通過附加引用的方式關(guān)聯(lián)起來,形成按鏈路順序查找的線性結(jié)構(gòu),內(nèi)存利用率較高。
Vector 和 Stack
Vector 的實(shí)現(xiàn)和 ArrayList 基本一致,底層使用的也是數(shù)組,它和 ArrayList 的區(qū)別主要在于:① Vector 的所有公有方法都使用了 synchronized 修飾保證線程安全性。② 增長(zhǎng)策略不同,Vector 多了一個(gè)成員變量 capacityIncrement 用于標(biāo)明擴(kuò)容的增量。
Stack 是 Vector 的子類,實(shí)現(xiàn)和 Vector基本一致,與之相比多提供了一些方法表達(dá)棧的含義。
P19:Set
Set 是不允許出現(xiàn)重復(fù)元素的集合類型,常用的是 HashSet、TreeSet 和 LinkedHashSet。
HashSet 的是通過 HashMap 實(shí)現(xiàn)的,HashMap 的 Key 值即 HashSet 存儲(chǔ)的元素,所有 Key 都使用相同的 Value ,一個(gè) static final 修飾的變量名為 PRESENT 的 Object 類型的對(duì)象。使用 Key 保證集合元素的唯一性,但它不保證集合元素的有序性。由于 HashSet 的底層是 HashMap 實(shí)現(xiàn)的,HashMap 是線程不安全的,因此 HashSet 也是線程不安全的。
HashSet 判斷元素是否相同時(shí),對(duì)于基本類型的包裝類,直接按值進(jìn)行比較。對(duì)于引用數(shù)據(jù)類型,會(huì)先比較 hashCode 返回值是否相同,如果不同則代表不是同一個(gè)對(duì)象,如果相同則繼續(xù)比較 equals 方法返回值是否相同,都相同說明是同一個(gè)對(duì)象。
TreeSet 的是使用 TreeMap 實(shí)現(xiàn)的,底層為樹結(jié)構(gòu),在添加新元素到集合中時(shí),按照某種比較規(guī)則將其插入合適的位置,保證插入后的集合仍然是有序的。LinkedHashSet 繼承自 HashSet,內(nèi)部使用鏈表維護(hù)了元素插入的順序。
P20:紅黑樹
AVL 樹是一種平衡二叉查找樹,增加和刪除節(jié)點(diǎn)后通過樹形旋轉(zhuǎn)重新達(dá)到平衡。右旋是以某個(gè)節(jié)點(diǎn)為中心,將它沉入當(dāng)前右子節(jié)點(diǎn)的位置,而讓當(dāng)前的左子節(jié)點(diǎn)作為新樹的根節(jié)點(diǎn),也稱為順時(shí)針旋轉(zhuǎn)。同理左旋是以某個(gè)節(jié)點(diǎn)為中心,將它沉入當(dāng)前左子節(jié)點(diǎn)的位置,而讓當(dāng)前的右子節(jié)點(diǎn)作為新樹的根節(jié)點(diǎn),也稱為逆時(shí)針旋轉(zhuǎn)。
紅黑樹是 1972 年發(fā)明的,當(dāng)時(shí)稱為對(duì)稱二叉 B 樹,1978 年正式命名為紅黑樹,它的主要特征是在每個(gè)節(jié)點(diǎn)上增加一個(gè)屬性來表示節(jié)點(diǎn)的顏色,可以是紅色也可以是黑色。紅黑樹和 AVL 樹類似,都是在進(jìn)行插入和刪除元素時(shí),通過特定的旋轉(zhuǎn)來保持自身平衡的,從而獲得較高的查找性能。與 AVL 樹相比,紅黑樹不追求所有遞歸子樹的高度差不超過 1,而是保證從根節(jié)點(diǎn)到葉尾的最長(zhǎng)路徑不超過最短路徑的 2 倍,所以它的最差時(shí)間復(fù)雜度也是 O(logn)。紅黑樹通過重新著色和左右旋轉(zhuǎn),更加高效地完成了插入和刪除之后的自平衡調(diào)整。
紅黑樹在本質(zhì)上還是二叉查找樹,它額外引入了 5 個(gè)約束條件:① 節(jié)點(diǎn)只能是紅色或黑色。② 根節(jié)點(diǎn)必須是黑色。③ 所有 NIL 節(jié)點(diǎn)都是黑色的。④ 一條路徑上不能出現(xiàn)相鄰的兩個(gè)紅色節(jié)點(diǎn)。⑤ 在任何遞歸子樹中,根節(jié)點(diǎn)到葉子節(jié)點(diǎn)的所有路徑上包含相同數(shù)目的黑色節(jié)點(diǎn)。這五個(gè)約束條件保證了紅黑樹的新增、刪除、查找的最壞時(shí)間復(fù)雜度均為 O(logn)。如果一個(gè)樹的左子節(jié)點(diǎn)或右子節(jié)點(diǎn)不存在,則均認(rèn)定為黑色。紅黑樹的任何旋轉(zhuǎn)在 3 次之內(nèi)均可完成。
紅黑樹的平衡性不如 AVL 樹,它維持的只是一種大致的平衡,并不嚴(yán)格保證左右子樹的高度差不超過 1。這導(dǎo)致節(jié)點(diǎn)數(shù)相同的情況下,紅黑樹的高度可能更高,也就是說平均查找次數(shù)會(huì)高于相同情況的 AVL 樹。在插入時(shí),紅黑樹和 AVL 樹都能在至多兩次旋轉(zhuǎn)內(nèi)恢復(fù)平衡,在刪除時(shí)由于紅黑樹只追求大致平衡,因此紅黑樹至多三次旋轉(zhuǎn)可以恢復(fù)平衡,而 AVL 樹最多需要 O(logn) 次。AVL 樹在插入和刪除時(shí),將向上回溯確定是否需要旋轉(zhuǎn),這個(gè)回溯的時(shí)間成本最差為 O(logn),而紅黑樹每次向上回溯的步長(zhǎng)為 2,回溯成本低。因此面對(duì)頻繁地插入與刪除紅黑樹更加合適。
P21:TreeMap
基于紅黑樹實(shí)現(xiàn)的 TreeMap 提供了平均和最差時(shí)間復(fù)雜度均為 O(logn) 的增刪改查操作,該集合最大的特點(diǎn)就是 Key 的有序性。TreeMap 繼承了 SortedMap 和 NavigableMap,SortedMap 表示 Key 是有序不可重復(fù)的,支持獲取頭尾 Key-Value 元素,或者根據(jù) Key 指定范圍獲取子集合等。插入的 Key 必須實(shí)現(xiàn) Comparable 接口或提供額外的 Comparator 比較器,所以 Key 不允許為 null。NavigableMap 繼承了 SortedMap,根據(jù)指定的搜索條件返回最匹配的 Key-Value 元素。
不同于 HashMap 的是 TreeMap 并非一定要重寫 hashCode 和 equals 方法來達(dá)到 Key 去重的目的。HashMap 是依靠 hashCode 和 equals 來去重的,而 TreeMap 依靠 Comparable 或 Comparator 來實(shí)現(xiàn)去重。 TreeMap 對(duì) Key 進(jìn)行排序時(shí),如果比較器不為空就會(huì)優(yōu)先使用比較器的 compare 方法,如果比較器為空就會(huì)使用 Key 實(shí)現(xiàn)的自然排序 Comparable 接口的 compareTo 方法,如果兩者都不滿足就會(huì)拋出異常。
TreeMap 通過 put 和 deleteEntry 實(shí)現(xiàn)紅黑樹增加和刪除節(jié)點(diǎn)的操作。插入新節(jié)點(diǎn)的規(guī)則有三個(gè):① 需要調(diào)整的新節(jié)點(diǎn)總是紅色的。② 如果插入新節(jié)點(diǎn)的父節(jié)點(diǎn)是黑色的,不需要調(diào)整。③ 如果插入新節(jié)點(diǎn)的父節(jié)點(diǎn)是紅色的,由于紅黑樹中不能出現(xiàn)相鄰的紅色,則進(jìn)入循環(huán)判斷,通過重新著色或左右旋轉(zhuǎn)來調(diào)整。TreeMap 的插入操作就是按照 Key 的對(duì)比往下遍歷,大于比較節(jié)點(diǎn)值的向右查找,小于的向左查找,先按照二叉查找樹的特性操作,無需關(guān)心節(jié)點(diǎn)顏色與樹的平衡,后續(xù)會(huì)重新著色和旋轉(zhuǎn),保持紅黑樹的特性。
TreeMap 是線程不安全的集合,在多線程操作時(shí)需要添加互斥機(jī)制,或者把對(duì)象放在 Collections.synchronizedMap() 中實(shí)現(xiàn)同步。在 JDK 7 之后的 HashMap、TreeSet、ConcurrentHashMap 也都是使用紅黑樹的方式管理節(jié)點(diǎn)。如果只是對(duì)單個(gè)元素進(jìn)行排序,使用 TreeSet 即可。TreeSet 的底層就是 TreeMap,Value 共享一個(gè)靜態(tài)的 Object 對(duì)象。
P22:HashMap
JDK 8 之前
底層實(shí)現(xiàn)是數(shù)組 + 鏈表,主要成員變量包括:存儲(chǔ)數(shù)據(jù)的 table 數(shù)組、鍵值對(duì)數(shù)量 size、加載因子 loadFactor。
table 數(shù)組用于記錄 HashMap 的所有數(shù)據(jù),它的每一個(gè)下標(biāo)都對(duì)應(yīng)一條鏈表,所有哈希沖突的數(shù)據(jù)都會(huì)被存放到同一條鏈表中,Entry 是鏈表的節(jié)點(diǎn)元素,包含四個(gè)成員變量:鍵 key、值 value、指向下一個(gè)節(jié)點(diǎn)的指針 next 和元素的散列值 hash。
在 HashMap 中數(shù)據(jù)都是以鍵值對(duì)的形式存在的,鍵對(duì)應(yīng)的 hash 值將會(huì)作為其在數(shù)組里的下標(biāo),如果兩個(gè)元素 key 的 hash 值一樣,就會(huì)發(fā)送哈希沖突,被放到同一個(gè)下標(biāo)中的鏈表上,為了使 HashMap 的查詢效率盡可能高,應(yīng)該使鍵的 hash 值盡可能分散。
HashMap 默認(rèn)初始化容量為 16,擴(kuò)容容量必須是 2 的冪次方、最大容量為 1<< 30 、默認(rèn)加載因子為 0.75。
1.put 方法:添加元素
① 如果 key 為 null 值,直接存入 table[0]。② 如果 key 不為 null 值,先計(jì)算 key 對(duì)應(yīng)的散列值。③ 調(diào)用 indexFor 方法根據(jù) key 的散列值和數(shù)組的長(zhǎng)度計(jì)算元素存放的下標(biāo) i。④ 遍歷 table[i] 對(duì)應(yīng)的鏈表,如果 key 已經(jīng)存在,就更新其 value 值然后返回舊的 value 值。⑤ 如果 key 不存在,就將 modCount 的值加 1,使用 addEntry 方法增加一個(gè)節(jié)點(diǎn),并返回 null 值。
2.hash 方法:計(jì)算元素 key 對(duì)應(yīng)的散列值
① 處理 String 類型的數(shù)據(jù)時(shí),直接調(diào)用對(duì)應(yīng)方法來獲取最終的hash值。② 處理其他類型數(shù)據(jù)時(shí),提供一個(gè)相對(duì)于 HashMap 實(shí)例唯一不變的隨機(jī)值 hashSeed 作為計(jì)算的初始量。③ 執(zhí)行異或和無符號(hào)右移操作使 hash 值更加離散,減小哈希沖突的概率。
3.indexFor 方法:計(jì)算元素下標(biāo)
直接將 hash 值和數(shù)組長(zhǎng)度 - 1 進(jìn)行與操作并返回,保證計(jì)算后的結(jié)果不會(huì)超過 table 數(shù)組的長(zhǎng)度范圍。
4.resize 方法:根據(jù) newCapacity 來確定新的擴(kuò)容閾值 threshold
① 如果當(dāng)前容量已經(jīng)達(dá)到了最大容量,就將閾值設(shè)置為 Integer 的最大值,之后擴(kuò)容就不會(huì)再觸發(fā)。② 創(chuàng)建一個(gè)新的容量為 newCapacity 的 Entry 數(shù)組,并調(diào)用 transfer 方法將舊數(shù)組的元素轉(zhuǎn)移到新數(shù)組.③ 將閾值設(shè)為 newCapacity x loadFactor 和 最大容量 + 1 的較小值。
5.transfer:轉(zhuǎn)移舊數(shù)組到新數(shù)組
① 遍歷舊數(shù)組的所有元素,調(diào)用 rehash 方法判斷是否需要哈希重構(gòu),如果需要就重新計(jì)算元素 key 的散列值。② 調(diào)用 indexFor 方法根據(jù) key 的散列值和數(shù)組的長(zhǎng)度計(jì)算元素存放的下標(biāo) i,利用頭插法將舊數(shù)組的元素轉(zhuǎn)移到新的數(shù)組。
6.get 方法:根據(jù) key 獲取元素的 value 值
① 如果 key 為 null 值,調(diào)用 getForNullKey 方法,如果 size 為 0 表示鏈表為空,返回 null 值。如果 size 不為 0,說明存在鏈表,遍歷 table[0] 的鏈表,如果找到了 key 為 null 的節(jié)點(diǎn)則返回其 value 值,否則返回 null 值。
② 調(diào)用 getEntry 方法,如果 size 為 0 表示鏈表為空,返回 null 值。如果 size 不為 0,首先計(jì)算 key 的散列值,然后遍歷該鏈表的所有節(jié)點(diǎn),如果節(jié)點(diǎn)的 key 值和 hash 值都和要查找的元素相同則返回其 Entry 節(jié)點(diǎn)。
③ 如果找到了對(duì)應(yīng)的 Entry 節(jié)點(diǎn),調(diào)用 getValue 方法獲取其 value 值并返回,否則返回 null 值。
JDK 8 開始
使用的是數(shù)組 + 鏈表/紅黑樹的形式,table 數(shù)組的元素?cái)?shù)據(jù)類型換成了 Entry 的靜態(tài)實(shí)現(xiàn)類 Node。
1.put 方法:添加元素
① 調(diào)用 putVal 方法添加元素。
② 如果 table 為空或沒有元素時(shí)就進(jìn)行擴(kuò)容,否則計(jì)算元素下標(biāo)位置,如果不存在就新創(chuàng)建一個(gè)節(jié)點(diǎn)存入。
③ 如果首節(jié)點(diǎn)和待插入元素的 hash值和 key 值都一樣,直接更新 value 值。
④ 如果首節(jié)點(diǎn)是 TreeNode 類型,調(diào)用 putTreeVal 方法增加一個(gè)樹節(jié)點(diǎn),每一次都比較插入節(jié)點(diǎn)和當(dāng)前節(jié)點(diǎn)的大小,待插入節(jié)點(diǎn)小就往左子樹查找,否則往右子樹查找,找到空位后執(zhí)行兩個(gè)方法:balanceInsert 方法,把節(jié)點(diǎn)插入紅黑樹并對(duì)紅黑樹進(jìn)行調(diào)整使之平衡; moveRootToFront 方法,由于調(diào)整平衡后根節(jié)點(diǎn)可能變化,table 里記錄的節(jié)點(diǎn)不再是根節(jié)點(diǎn),需要重置根節(jié)點(diǎn)。
⑤ 如果是鏈表節(jié)點(diǎn),就遍歷鏈表,根據(jù) hash 值和 key 值判斷是否重復(fù),決定更新值還是新增節(jié)點(diǎn)。如果遍歷到了鏈表末尾,添加鏈表元素,如果達(dá)到了建樹閾值,還需要調(diào)用 treeifyBin 方法把鏈表重構(gòu)為紅黑樹。
⑥ 存放元素后,將 modCount 值加 1,如果 節(jié)點(diǎn)數(shù) + 1 大于擴(kuò)容閾值,還需要進(jìn)行擴(kuò)容。
2.get 方法:根據(jù) key 獲取元素的 value 值
① 調(diào)用 getNode 方法獲取 Node 節(jié)點(diǎn),如果不是 null 值就返回 Node 節(jié)點(diǎn)的 value 值,否則返回 null。
② 如果數(shù)組不為空,先比較第一個(gè)節(jié)點(diǎn)和要查找元素的 hash 值和 key 值,如果都相同則直接返回。
③ 如果第二個(gè)節(jié)點(diǎn)是 TreeNode 節(jié)點(diǎn)則調(diào)用 getTreeNode 方法進(jìn)行查找,否則遍歷鏈表根據(jù) hash 值和 key 值進(jìn)行查找,如果沒有找到就返回 null。
3.hash 方法:計(jì)算元素 key 對(duì)應(yīng)的散列值
Java 8 的計(jì)算過程簡(jiǎn)單了許多,如果 key 非空就將 key 的 hashCode() 返回值的高低16位進(jìn)行異或操作,這主要是為了讓盡可能多的位參與運(yùn)算,讓結(jié)果中的 0 和 1 分布得更加均勻,從而降低哈希沖突的概率。
4.resize 方法:擴(kuò)容數(shù)組
重新規(guī)劃長(zhǎng)度和閾值,如果長(zhǎng)度發(fā)生了變化,部分?jǐn)?shù)據(jù)節(jié)點(diǎn)也要重新排列。
重新規(guī)劃長(zhǎng)度
① 如果 size 超出擴(kuò)容閾值,把 table 容量增加為之前的2倍。
② 如果新的 table 容量小于默認(rèn)的初始化容量16,那么將 table 容量重置為16。
③ 如果新的 table 容量大于等于最大容量,那么將閾值設(shè)為 Integer 的最大值,并且 return 終止擴(kuò)容,由于 size 不可能超過該值因此之后不會(huì)再發(fā)生擴(kuò)容。
重新排列數(shù)據(jù)節(jié)點(diǎn)
① 如果節(jié)點(diǎn)為 null 值則不進(jìn)行處理。
② 如果節(jié)點(diǎn)不為 null 值且沒有next節(jié)點(diǎn),那么重新計(jì)算其散列值然后存入新的 table 數(shù)組中。
③ 如果節(jié)點(diǎn)為 TreeNode 節(jié)點(diǎn),那么調(diào)用 split 方法進(jìn)行處理,該方法用于對(duì)紅黑樹調(diào)整,如果太小會(huì)退化回鏈表。
④ 如果節(jié)點(diǎn)是鏈表節(jié)點(diǎn),需要將鏈表拆分為 hashCode() 返回值超出舊容量的鏈表和未超出容量的鏈表。對(duì)于hash & oldCap == 0 的部分不需要做處理,反之需要放到新的下標(biāo)位置上,新下標(biāo) = 舊下標(biāo) + 舊容量。
線程安全
Java 7 的 HashMap 存在死循環(huán)和數(shù)據(jù)丟失問題。
并發(fā)賦值被覆蓋:在 createEntry 方法中,新添加的元素直接放在頭部,使得新添加的元素在下次提取時(shí)可以更快被訪問到,但是如果兩個(gè)線程如果同時(shí)執(zhí)行到此處,就會(huì)導(dǎo)致其中一個(gè)線程的賦值被覆蓋,這是數(shù)據(jù)丟失的原因之一。
已遍歷區(qū)間新增元素丟失:當(dāng)某個(gè)線程在 transfer 方法遷移時(shí),其他線程新增的元素可能落在已經(jīng)遍歷過的哈希槽上。在遍歷完成后,table 數(shù)組引用指向了 newTable,這時(shí)新增元素就會(huì)丟失,被垃圾收集器回收。
新表被覆蓋:如果擴(kuò)容完成,執(zhí)行了 table = newTable,則后續(xù)的元素就可以在新表上進(jìn)行插入操作。但是如果多線程同時(shí)擴(kuò)容,每個(gè)線程又都會(huì) new 一個(gè)數(shù)組對(duì)象,這是線程內(nèi)的局部數(shù)組對(duì)象,線程之間不可見。遷移完成后,resize 線程會(huì)賦值給 table,從而覆蓋其他線程的操作,因此在新表中進(jìn)行插入操作的對(duì)象都會(huì)被丟棄。
擴(kuò)容時(shí) resize 方法調(diào)用的 transfer 方法中使用頭插法遷移元素,雖然 newTable 是局部變量,但是原先 table 中的 Entry 鏈表是共享的,產(chǎn)生問題的根源是 Entry 的 next 指針被并發(fā)修改,這可能導(dǎo)致數(shù)據(jù)丟失、兩個(gè)對(duì)象互鏈或者對(duì)象自己互鏈,形成環(huán)路的原因是兩個(gè)線程都執(zhí)行完第一個(gè)節(jié)點(diǎn)的遍歷操作后,到第二個(gè)節(jié)點(diǎn)時(shí)產(chǎn)生互鏈。
JDK 8 的 HashMap 在 resize 方法中完成擴(kuò)容,并且改用了尾插法,不會(huì)產(chǎn)生死循環(huán)的問題,但是在多線程的情況下還是可能會(huì)導(dǎo)致數(shù)據(jù)覆蓋的問題,因此依舊線程不安全??梢允褂?ConcurrentHashMap 代替或者 Collections.synchronizedMap 包裝成同步集合。
JVM 15
P1:運(yùn)行時(shí)數(shù)據(jù)區(qū)
程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,可以看作當(dāng)前線程所執(zhí)行字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí)通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、線程恢復(fù)等功能都需要依賴計(jì)數(shù)器完成。程序計(jì)數(shù)器是線程私有的,各條線程之間互不影響。
如果線程正在執(zhí)行的是一個(gè) Java 方法,計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。如果正在執(zhí)行的是本地(Native)方法,計(jì)數(shù)器值則應(yīng)為空(Undefined)。
此區(qū)域是唯一在虛擬機(jī)規(guī)范中沒有規(guī)定任何內(nèi)存溢出情況的區(qū)域。
Java 虛擬機(jī)棧
Java 虛擬機(jī)棧是線程私有的,用來描述 Java 方法的內(nèi)存模型。每當(dāng)有新的線程創(chuàng)建時(shí)就會(huì)給它分配一個(gè)??臻g,當(dāng)線程結(jié)束后??臻g就被回收,因此棧與線程擁有相同的生命周期。棧中的元素用于支持虛擬機(jī)進(jìn)行方法調(diào)用,每個(gè)方法在執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀用來存儲(chǔ)這個(gè)方法的局部變量表、操作棧、動(dòng)態(tài)鏈接和方法出口等信息。每個(gè)方法從開始調(diào)用到執(zhí)行完成的過程,就是棧幀從入棧到出棧的過程。在活動(dòng)線程中,只有位于棧頂?shù)膸攀怯行У?#xff0c;稱為當(dāng)前棧幀。
該區(qū)域有兩類異常情況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出 StackOverflowError 異常。如果 JVM 棧容量可以動(dòng)態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存會(huì)拋出 OutOfMemoryError 異常(HotSpot 不可以動(dòng)態(tài)擴(kuò)展,不存在此問題)。
本地方法棧
本地方法棧與虛擬機(jī)棧的作用相似,不同的是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(字節(jié)碼)服務(wù),而本地方法棧是為虛擬機(jī)棧用到的本地(Native)方法服務(wù)。調(diào)用本地方法時(shí)虛擬機(jī)棧保持不變,動(dòng)態(tài)鏈接并直接調(diào)用指定的本地方法。
虛擬機(jī)規(guī)范對(duì)本地方法棧中方法所用語言、使用方式與數(shù)據(jù)結(jié)構(gòu)無強(qiáng)制規(guī)定,具體的虛擬機(jī)可根據(jù)需要自由實(shí)現(xiàn),例如 HotSpot 直接將虛擬機(jī)棧和本地方法棧合二為一。
與虛擬機(jī)棧一樣,本地方法棧也會(huì)在棧深度異常和棧擴(kuò)展失敗時(shí)分別拋出 StackOverflowError 和 OutOfMemoryError 異常。
Java 堆
堆是虛擬機(jī)所管理的內(nèi)存中最大的一塊,是被所有線程共享的,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。堆用來存放對(duì)象實(shí)例,Java 里幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的。但對(duì)于大對(duì)象(例如數(shù)組),多數(shù)虛擬機(jī)實(shí)現(xiàn)出于簡(jiǎn)單、存儲(chǔ)高效的考慮會(huì)要求連續(xù)的內(nèi)存空間。
堆既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,可以通過 -Xms 和 -Xmx 設(shè)置堆的最小和最大容量,當(dāng)前主流的 JVM 都是按照可擴(kuò)展來實(shí)現(xiàn)的。如果在堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),虛擬機(jī)將拋出 OutOfMemoryError 異常。
方法區(qū)
方法區(qū)和 Java 堆一樣是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。
JDK 8 之前使用永久代來實(shí)現(xiàn)方法區(qū),這種設(shè)計(jì)導(dǎo)致了容易發(fā)生內(nèi)存溢出,因?yàn)橛谰么?-XX:MaxPermSize的上限,即使不設(shè)置也有默認(rèn)大小。JDK 7 時(shí)把原本放在永久代的字符串常量池、靜態(tài)變量等移出,到了 JDK8 時(shí)永久代被完全廢棄,改用在本地內(nèi)存中實(shí)現(xiàn)的元空間代替,把 JDK 7 中永久代剩余內(nèi)容(主要是類型信息)全部移到元空間。
虛擬機(jī)規(guī)范對(duì)方法區(qū)的約束很寬松,除了和堆一樣不需要連續(xù)內(nèi)存和可以選擇固定大小/可擴(kuò)展外,還可以不實(shí)現(xiàn)垃圾回收。垃圾回收在該區(qū)域出現(xiàn)較少,主要目標(biāo)針對(duì)常量池和類型卸載。如果方法區(qū)無法滿足新的內(nèi)存分配需求,將拋出 OutOfMemoryError 異常。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表,用于存放編譯器生成的各種字面量與符號(hào)引用,這部分內(nèi)容將在類加載后存放到運(yùn)行時(shí)常量池。一般來說,除了保存 Class 文件中描述的符號(hào)引用外,還會(huì)把符號(hào)引用翻譯出來的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池中。
運(yùn)行時(shí)常量池相對(duì)于 Class 文件常量池的一個(gè)重要特征是具備動(dòng)態(tài)性,Java 并不要求常量一定只有編譯期才能產(chǎn)生,也就是說并非預(yù)置入 Class 文件常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可以將新的常量放入池中,例如 String 類的 intern 方法。
由于運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常。
直接內(nèi)存
直接內(nèi)存不是運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但這部分內(nèi)存被頻繁使用,而且可能導(dǎo)致內(nèi)存溢出。
JDK 1.4 中新加入了 NIO 模型,引入了一種基于通道與緩沖區(qū)的 IO 方式,它可以使用 Native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在 Java 堆里的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作,能在一些場(chǎng)景中顯著提高性能,避免了在 Java 堆和 Native堆中來回復(fù)制數(shù)據(jù)。
直接內(nèi)存的分配不會(huì)受到 Java 堆大小的限制,但還是會(huì)受到本機(jī)總內(nèi)存大小以及處理器尋址空間的限制,一般配置虛擬機(jī)參數(shù)時(shí)會(huì)根據(jù)實(shí)際內(nèi)存設(shè)置 -Xmx 等參數(shù)信息,但經(jīng)常忽略掉直接內(nèi)存,使各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制,導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn) OOM 異常。
P2:對(duì)象創(chuàng)建的過程
字節(jié)碼角度
- NEW:如果找不到 Class 對(duì)象則進(jìn)行類加載。加載成功后在堆中分配內(nèi)存,從 Object 到本類路徑上的所有屬性值都要分配內(nèi)存。分配完畢后進(jìn)行零值設(shè)置。這個(gè)指令完成后,將指向?qū)嵗龑?duì)象的引用變量壓入虛擬機(jī)棧頂。
- DUP:在棧頂復(fù)制該引用變量,這時(shí)棧頂有兩個(gè)指向堆內(nèi)實(shí)例的引用變量。如果 init 方法有參數(shù)還需要把參數(shù)壓入操作棧中。兩個(gè)引用變量的目的不同,棧底的引用用于賦值或保存到局部變量表,棧頂?shù)囊米鳛榫浔{(diào)用相關(guān)方法。
- INVOKESPECIAL:通過棧頂?shù)囊米兞空{(diào)用 init 方法。
執(zhí)行角度
當(dāng) JVM 遇到一條字節(jié)碼 new 指令時(shí),首先將檢查該指令的參數(shù)能否在常量池中定位到一個(gè)類的符號(hào)引用,并檢查這個(gè)引用代表的類是否已被加載、解析和初始化,如果沒有就必須先執(zhí)行類加載。
在類加載檢查通過后虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定,分配空間的任務(wù)實(shí)際上等于把一塊確定大小的內(nèi)存塊從 Java 堆中劃分出來。假設(shè) Java 堆內(nèi)存是規(guī)整的,被使用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界指示器,分配內(nèi)存就是把該指針向空閑方向挪動(dòng)一段與對(duì)象大小相等的距離,這種方式叫"指針碰撞"。
如果 Java 堆中的內(nèi)存不是規(guī)整的,那么虛擬機(jī)就必須維護(hù)一個(gè)列表記錄哪些內(nèi)存塊可用,在分配時(shí)從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例并更新列表上的記錄,這種方式叫做"空閑列表"。
選擇哪種分配方式由堆是否規(guī)整決定,堆是否規(guī)整又由所用垃圾回收器是否帶有空間壓縮整理能力決定。因此使用 Serial、ParNew 等帶壓縮整理的收集器時(shí),系統(tǒng)采用指針碰撞;當(dāng)使用 CMS 這種基于清除算法的垃圾收集器時(shí),采用空間列表。
對(duì)象創(chuàng)建十分頻繁,即使修改一個(gè)指針的位置在并發(fā)情況下也不是線程安全的,可能出現(xiàn)正給對(duì)象 A 分配內(nèi)存,指針還沒來得及修改,對(duì)象 B 又同時(shí)使用了指針來分配內(nèi)存的情況。解決該問題有兩個(gè)方法:① 采用 CAS 加失敗重試保證更新操作的原子性。② 把內(nèi)存分配的動(dòng)作按照線程劃分在不同空間進(jìn)行,即每個(gè)線程在 Java 堆中預(yù)先分配一小塊內(nèi)存,叫做本地線程分配緩沖 TLAB,哪個(gè)線程要分配內(nèi)存就在對(duì)應(yīng)的 TLAB 分配,只有 TLAB 用完了才需要同步。
內(nèi)存分配完成后虛擬機(jī)必須將成員變量設(shè)定為零值,保證對(duì)象的實(shí)例字段可以不賦初始值就直接使用。
初始化為零值后,設(shè)置對(duì)象頭,包括哈希碼、GC 信息、鎖信息、對(duì)象所屬類的類元信息等。
最后執(zhí)行 init 方法,初始化成員變量,執(zhí)行實(shí)例化代碼塊,調(diào)用類的構(gòu)造方法,并把堆內(nèi)對(duì)象的首地址賦值給引用變量。
P3:對(duì)象的內(nèi)存布局
在 HotSpot 虛擬機(jī)中,對(duì)象在堆內(nèi)存中的存儲(chǔ)布局可分為三個(gè)部分。
對(duì)象頭
對(duì)象頭占用 12B,存儲(chǔ)內(nèi)容包括對(duì)象標(biāo)記和類元信息。對(duì)象標(biāo)記存儲(chǔ)對(duì)象本身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC 標(biāo)記、鎖信息、線程關(guān)聯(lián)信息等,這部分?jǐn)?shù)據(jù)在 64 位 JVM 上占用 8B,稱為"Mark Word"。為了存儲(chǔ)更多的狀態(tài)信息,對(duì)象標(biāo)記的存儲(chǔ)格式是不固定的,與具體 JVM 實(shí)現(xiàn)有關(guān)。
類元信息存儲(chǔ)的是對(duì)象指向它的類元數(shù)據(jù)的首地址,占用 4B。JVM 通過該指針來確定對(duì)象是哪個(gè)類的實(shí)例。并非所有虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,查找對(duì)象的元數(shù)據(jù)不一定要經(jīng)過對(duì)象本身。此外如果對(duì)象是一個(gè) Java 數(shù)組,在對(duì)象頭還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)是對(duì)象真正存儲(chǔ)的有效信息,即本類對(duì)象的實(shí)例成員變量和所有可見的父類成員變量。存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)和字段在源碼中定義順序的影響。相同寬度的字段總是被分配到一起存放,在滿足該前提條件的情況下父類中定義的變量會(huì)出現(xiàn)在子類之前。
對(duì)齊填充
這部分不是必然存在的,僅起占位符的作用。由于 HotSpot 虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象的起始地址必須是 8 字節(jié)的整數(shù)倍,而對(duì)象頭已經(jīng)被設(shè)為正好是 8 字節(jié)的整數(shù)倍,因此如果對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊,就需要對(duì)齊填充來補(bǔ)全。
P4:對(duì)象的訪問定位
Java 程序會(huì)通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對(duì)象,而具體對(duì)象訪問方式是由虛擬機(jī)決定的,主流的訪問方式主要有使用句柄和直接指針兩種。
使用句柄
如果使用句柄訪問,Java 堆中將可能會(huì)劃分出一塊內(nèi)存作為句柄池,reference 中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自具體的地址信息。
優(yōu)點(diǎn)是 reference 中存儲(chǔ)的是穩(wěn)定句柄地址,在對(duì)象被移動(dòng)(處于垃圾收集過程中)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而 reference 本身不需要被修改。
直接指針
如果使用直接指針訪問的話,Java 堆中對(duì)象的內(nèi)存布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference中存儲(chǔ)的直接就是對(duì)象地址,如果只是訪問對(duì)象本身的話就不需要多一次間接訪問的開銷。
優(yōu)點(diǎn)就是速度更快,節(jié)省了一次指針定位的時(shí)間開銷,HotSpot 主要使用的就是直接指針來進(jìn)行對(duì)象訪問。
P5:內(nèi)存溢出異常
Java 堆溢出
Java 堆用于存儲(chǔ)對(duì)象實(shí)例,我們只要不斷創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象,那么隨著對(duì)象數(shù)量的增加,總?cè)萘坑|及最大堆容量的限制后就會(huì)產(chǎn)生OOM異常。例如在 while 死循環(huán)中一直 new 創(chuàng)建實(shí)例。
Java 堆內(nèi)存的 OOM 是實(shí)際應(yīng)用中最常見的 OOM 情況,常規(guī)的處理方法是先通過內(nèi)存映像分析工具對(duì) Dump 出來的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析,確認(rèn)內(nèi)存中導(dǎo)致 OOM 的對(duì)象是否是必要的,即分清楚到底是出現(xiàn)了內(nèi)存泄漏還是內(nèi)存溢出。
如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對(duì)象到 GC Roots 的引用鏈,找到泄露對(duì)象是通過怎樣的引用路徑、與哪些GC Roots相關(guān)聯(lián)才導(dǎo)致垃圾收集器無法回收它們,一般可以準(zhǔn)確定位到對(duì)象創(chuàng)建的位置進(jìn)而找出產(chǎn)生內(nèi)存泄漏代碼的具體位置。
如果不是內(nèi)存泄漏,即內(nèi)存中的對(duì)象確實(shí)都是必須存活的那就應(yīng)當(dāng)檢查 JVM 的堆參數(shù)設(shè)置,與機(jī)器的內(nèi)存相比是否還有向上調(diào)整的空間。再?gòu)拇a上檢查是否存在某些對(duì)象生命周期過長(zhǎng)、持有狀態(tài)時(shí)間過長(zhǎng)、存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)不合理等情況,盡量減少程序運(yùn)行期的內(nèi)存消耗。
虛擬機(jī)棧和本地方法棧溢出
由于HotSpot虛擬機(jī)不區(qū)分虛擬機(jī)和本地方法棧,因此設(shè)置本地方法棧大小的參數(shù)沒有意義,棧容量只能由 -Xss 參數(shù)來設(shè)定,存在兩種異常:
-
StackOverflowError:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常。例如一個(gè)遞歸方法不斷調(diào)用自己。
該異常有明確錯(cuò)誤堆??晒┓治?#xff0c;容易定位到問題所在。 -
OutOfMemoryError:如果 JVM 棧容量可以動(dòng)態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存會(huì)拋出OutOfMemoryError異常。HotSpot 虛擬機(jī)不支持虛擬機(jī)棧的擴(kuò)展,所以除非在創(chuàng)建線程申請(qǐng)內(nèi)存時(shí)就因無法獲得足夠內(nèi)存而出現(xiàn)OOM異常,否則在線程運(yùn)行時(shí)是不會(huì)因?yàn)閿U(kuò)展而導(dǎo)致內(nèi)存溢出的,只會(huì)因?yàn)闂H萘繜o法容納新的棧幀而導(dǎo)致StackOverflowError異常。
運(yùn)行時(shí)常量池溢出
String 類的 intern 方法是一個(gè)本地方法,作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此 String 對(duì)象的字符串,則返回代表池中這個(gè)字符串的 String 對(duì)象的引用,否則將此 String 對(duì)象包含的字符串添加到常量池并返回此 String 對(duì)象的引用。
在 JDK 6 及之前常量池都分配在永久代,因此可以通過 -XX:PermSize 和 -XX:MaxPermSize 限制永久代大小,間接限制常量池。在 while 死循環(huán)中不斷調(diào)用 intern 方法導(dǎo)致運(yùn)行時(shí)常量池溢出。
在 JDK 7 及之后不會(huì)出現(xiàn)該問題,因?yàn)榇娣旁谟谰么淖址A砍匾呀?jīng)被移至 Java 堆中。
方法區(qū)溢出
方法區(qū)的主要職責(zé)是存放類型信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。只要不斷在運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū),就會(huì)導(dǎo)致溢出。例如使用 JDK 反射或 CGLib 直接操作字節(jié)碼在運(yùn)行時(shí)生成大量的類。當(dāng)前的很多主流框架如 Spring、Hibernate 等對(duì)類增強(qiáng)時(shí)都會(huì)使用 CGLib 這類字節(jié)碼技術(shù),增強(qiáng)的類越多就需要越大的方法區(qū)保證動(dòng)態(tài)生成的新類型可以載入內(nèi)存,也就更容易導(dǎo)致方法區(qū)溢出。
JDK 8 之后永久代完全被廢棄,取而代之的是元空間,HotSpot 提供了一些參數(shù)作為元空間的防御措施:
-XX:MaxMetaspaceSize:設(shè)置元空間的最大值,默認(rèn) -1,表示不限制即只受限于本地內(nèi)存大小。
-XX:MetaspaceSize:指定元空間的初始大小,以字節(jié)為單位,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載,同時(shí)收集器會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量空間就適當(dāng)降低該值,如果釋放了很少的空間就適當(dāng)提高該值。
-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空間剩余容量百分比,可減少因?yàn)樵臻g不足導(dǎo)致的垃圾收集的頻率。類似的還有-XX:MinMetaspaceFreeRatio,用于控制最大的元空間剩余容量百分比。
本機(jī)直接內(nèi)存溢出
直接內(nèi)存的容量大小可通過 -XX:MaxDirectMemorySize 指定,如果不指定則默認(rèn)與 Java 堆的最大值一致。
由直接內(nèi)存導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在 Heap Dump 文件中不會(huì)看見明顯的異常,如果發(fā)現(xiàn)內(nèi)存溢出后產(chǎn)生的 Dump 文件很小,而程序中又直接或間接使用了直接內(nèi)存(典型的間接使用就是 NIO),那么就可以考慮檢查直接內(nèi)存方面的原因。
P6:判斷對(duì)象是否是垃圾
在堆中存放著所有對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,首先要判斷對(duì)象是否還存活著。
引用計(jì)數(shù)算法
在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,如果有一個(gè)地方引用它計(jì)數(shù)器就加1,引用失效時(shí)計(jì)數(shù)器就減1,如果計(jì)數(shù)器為0則該對(duì)象就是不再被使用的。該算法原理簡(jiǎn)單,效率也高,但是在 Java中很少使用,因?yàn)樗嬖趯?duì)象之間互相循環(huán)引用的問題,導(dǎo)致計(jì)數(shù)器無法清零。
可達(dá)性分析算法
當(dāng)前主流語言的內(nèi)存管理子系統(tǒng)都是使用可達(dá)性分析算法來判斷對(duì)象是否存活的。這個(gè)算法的基本思路就是通過一系列稱為 GC Roots 的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑稱為引用鏈,如果某個(gè)對(duì)象到 GC Roots之間沒有任何引用鏈相連,則此對(duì)象是不可能再被使用的。
可作為GC Roots的對(duì)象包括虛擬機(jī)棧中引用的對(duì)象、方法區(qū)中類靜態(tài)屬性引用的對(duì)象、方法區(qū)中常量引用的對(duì)象、本地方法棧中引用的對(duì)象等。
P7:引用類型
無論通過引用計(jì)數(shù)還是可達(dá)性分析判斷對(duì)象是否存活,都和引用離不開關(guān)系。在 JDK1.2 之前引用的定義是:如果 reference 類型數(shù)據(jù)存儲(chǔ)的數(shù)值代表另外一塊內(nèi)存的起始地址,那么就稱該 reference 數(shù)據(jù)是代表某塊內(nèi)存、某個(gè)對(duì)象的引用。在 JDK 1.2之后 Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,按強(qiáng)度分為四種:
強(qiáng)引用:最常見的引用,例如 Object obj = new Object 這樣的變量聲明和定義就會(huì)產(chǎn)生對(duì)該對(duì)象的強(qiáng)引用。只要對(duì)象有強(qiáng)引用指向,并且 GC Roots 可達(dá),那么進(jìn)行內(nèi)存回收時(shí)即使瀕臨內(nèi)存耗盡也不會(huì)回收該對(duì)象。
軟引用:弱于強(qiáng)引用,描述非必需對(duì)象。在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會(huì)把軟引用關(guān)聯(lián)的對(duì)象加入回收范圍以獲得更多的內(nèi)存空間。主要用來緩存服務(wù)器中間計(jì)算結(jié)果及不需要實(shí)時(shí)保存的用戶行為等。
弱引用:弱于軟引用,描述非必需對(duì)象。被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次 YGC 之前,當(dāng)垃圾收集器開始工作時(shí)無論當(dāng)前內(nèi)存是否足夠都會(huì)回收只被弱引用關(guān)聯(lián)的對(duì)象。由于 YGC 具有不確定性,因此弱引用何時(shí)被回收也有不確定性。
虛引用:是最弱的引用關(guān)系,定義完成后就無法通過該引用獲取指向的對(duì)象。一個(gè)對(duì)象是否有虛引用存在,完全不會(huì)對(duì)其生存時(shí)間造成影響。該引用的唯一目的就是為了能在這個(gè)對(duì)象被垃圾收集器回收時(shí)收到一個(gè)系統(tǒng)通知。虛引用必須與引用隊(duì)列聯(lián)合使用,當(dāng)垃圾回收時(shí)如果出現(xiàn)虛引用,就會(huì)在回收對(duì)象內(nèi)存之前把這個(gè)虛引用加入關(guān)聯(lián)的引用隊(duì)列。
P8:GC 算法
標(biāo)記-清除算法
- 原理:分為標(biāo)記和清除兩個(gè)階段,首先從每個(gè) GC Roots 出發(fā)依次標(biāo)記有引用關(guān)系的對(duì)象,最后將沒有被標(biāo)記的對(duì)象清除。
- 特點(diǎn):① 執(zhí)行效率不穩(wěn)定,如果堆中包含大量對(duì)象且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除,導(dǎo)致效率隨對(duì)象數(shù)量增長(zhǎng)而降低。② 內(nèi)存空間碎片化問題,會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行中需要分配較大對(duì)象時(shí)容易觸發(fā) Full GC。
標(biāo)記-復(fù)制算法
- 原理:為了解決內(nèi)存碎片問題,將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊。當(dāng)這一塊的空間用完了,就將還存活著的對(duì)象復(fù)制到另一塊,然后再把已使用過的內(nèi)存空間一次清理掉。主要用于進(jìn)行新生代的垃圾回收。
- 特點(diǎn):① 實(shí)現(xiàn)簡(jiǎn)單、運(yùn)行高效,解決了內(nèi)存碎片問題。② 代價(jià)是將可用內(nèi)存縮小為原來的一半,浪費(fèi)了過多空間。
- HotSpot 的新生代劃分
把新生代劃分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次分配內(nèi)存只使用 Eden 和其中一塊 Survivor。發(fā)生垃圾收集時(shí)將 Eden 和 Survivor 中仍然存活的對(duì)象一次性復(fù)制到另一塊 Survivor 上,然后直接清理掉 Eden 和已用過的那塊 Survivor 空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,即每次新生代中可用空間為整個(gè)新生代的90%。
標(biāo)記-整理算法
- 原理:標(biāo)記-復(fù)制算法在對(duì)象存活率較高時(shí)要進(jìn)行較多的復(fù)制操作,效率將會(huì)降低。并且如果不想浪費(fèi)空間,就需要有額外空間進(jìn)行分配擔(dān)保,應(yīng)對(duì)被使用內(nèi)存中所有對(duì)象都100%存活的極端情況,所以老年代一般不使用此算法。老年代使用標(biāo)記-整理算法,標(biāo)記過程與標(biāo)記-清除算法一樣,只是后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
- 特點(diǎn):標(biāo)記-清除與標(biāo)記-整理的差異在于前者是一種非移動(dòng)式回收算法而后者是移動(dòng)式的。是否移動(dòng)回收后的存活對(duì)象優(yōu)缺點(diǎn)并存:① 如果移動(dòng)存活對(duì)象,尤其是在老年代這種每次回收都有大量對(duì)象存活的區(qū)域,是一種極為負(fù)重的操作,而且這種移動(dòng)必須全程暫停用戶線程。② 如果不移動(dòng)對(duì)象就會(huì)導(dǎo)致空間碎片問題,只能依賴更復(fù)雜的內(nèi)存分配器和訪問器來解決。所以是否移動(dòng)對(duì)象都存在弊端,移動(dòng)則內(nèi)存回收更復(fù)雜,不移動(dòng)則內(nèi)存分配更復(fù)雜。
P9:垃圾收集器
經(jīng)典垃圾收集器:指 JDK 11之前的垃圾收集器。
Serial
最基礎(chǔ)、歷史最悠久的收集器,該收集器是一個(gè)使用復(fù)制算法的單線程工作收集器,單線程的意義不僅是說明它只會(huì)使用一個(gè)處理器或一條收集線程去完成垃圾收集工作,更重要的是強(qiáng)調(diào)它進(jìn)行垃圾收集時(shí)必須暫停其他所有工作線程直到收集結(jié)束。
Serial 是虛擬機(jī)運(yùn)行在客戶端模式下的默認(rèn)新生代收集器,優(yōu)點(diǎn)是簡(jiǎn)單高效,對(duì)于內(nèi)存受限的環(huán)境它是所有收集器中最小的;對(duì)于單核處理器或處理器核心較少的環(huán)境來說,Serial 收集器由于沒有線程交互開銷,因此可獲得最高的單線程收集效率。
ParNew
實(shí)質(zhì)上是 Serial 的多線程版本,除了使用多線程進(jìn)行垃圾收集外其余行為完全一致。
ParNew 是虛擬機(jī)運(yùn)行在服務(wù)端模式下的默認(rèn)新生代收集器,一個(gè)重要原因是除了 Serial 外只有它能與 CMS 配合。自從 JDK 9 開始,ParNew 加 CMS 收集器的組合就不再是官方推薦的服務(wù)端模式下的收集器解決方案了,官方希望他能被 G1 完全取代。
Parallel Scavenge
新生代收集器,基于標(biāo)記-復(fù)制算法,是可以并行的多線程收集器,與 ParNew 類似。
特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同,CMS 等收集器的關(guān)注點(diǎn)是盡可能縮短收集時(shí)用戶線程的停頓時(shí)間,而 Parallel Scavenge 的目標(biāo)是達(dá)到一個(gè)可控制的吞吐量,吞吐量就是處理器用于運(yùn)行用戶代碼的時(shí)間與處理器消耗總時(shí)間的比值。自適應(yīng)調(diào)節(jié)策略也是它區(qū)別于 ParNew 的一個(gè)重要特性。
Serial Old
Serial 的老年代版本,同樣是一個(gè)單線程收集器,使用標(biāo)記-整理算法。
Serial Old 是虛擬機(jī)在客戶端模式下的默認(rèn)老年代收集器,用于服務(wù)端有兩種用途:一種是 JDK 5 及之前與 Parallel Scavenge 搭配使用,另一種是作為CMS 發(fā)生失敗時(shí)的預(yù)案。
Parellel Old
Parallel Scavenge 的老年代版本,支持多線程收集,基于標(biāo)記-整理算法實(shí)現(xiàn)。這個(gè)收集器直到 JDK 6 才開始提供,在注重吞吐量?jī)?yōu)先的場(chǎng)景可以有效考慮Parallel Scavenge 加 Parallel Old 組合。
CMS
以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,如果希望系統(tǒng)停頓時(shí)間盡可能短以給用戶帶來更好的體驗(yàn)就可以使用 CMS。
基于標(biāo)記-清除算法,過程相對(duì)復(fù)雜,分為四個(gè)步驟:初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除。
其中初始標(biāo)記和重新標(biāo)記仍然需要 STW(Stop The World,表示系統(tǒng)停頓),初始標(biāo)記僅是標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對(duì)象,速度很快。并發(fā)標(biāo)記就是從 GC Roots 的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過程,耗時(shí)較長(zhǎng)但不需要停頓用戶線程,可以與垃圾收集線程并發(fā)運(yùn)行。重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間因用戶程序運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,該階段停頓時(shí)間比初始標(biāo)記稍長(zhǎng),但遠(yuǎn)比并發(fā)標(biāo)記短。最后是并發(fā)清除,清理標(biāo)記階段判斷的已死亡對(duì)象,由于不需要移動(dòng)存活對(duì)象,因此該階段也可以與用戶線程并發(fā)。
由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除階段中,垃圾收集器都可以和用戶線程一起工作,所以從總體上說 CMS 的內(nèi)存回收過程是與用戶線程并發(fā)執(zhí)行的。
CMS 是 HotSpot 追求低停頓的第一次成功嘗試,但還存在三個(gè)明顯缺點(diǎn):① 對(duì)處理器資源敏感,在并發(fā)階段雖然不會(huì)導(dǎo)致用戶線程暫停,但會(huì)降低總吞吐量。② 無法處理浮動(dòng)垃圾,有可能出現(xiàn)并發(fā)失敗而導(dǎo)致另一次 Full GC。③ 基于標(biāo)記-清除算法,會(huì)產(chǎn)生大量空間碎片,給大對(duì)象分配帶來麻煩。
G1
開創(chuàng)了收集器面向局部收集的設(shè)計(jì)思路和基于Region的內(nèi)存布局,是一款主要面向服務(wù)端的收集器,最初設(shè)計(jì)目標(biāo)是替換CMS。
G1 之前的收集器,垃圾收集的目標(biāo)要么是整個(gè)新生代,要么是整個(gè)老年代或整個(gè)堆。而 G1 可以面向堆內(nèi)存任何部分來組成回收集進(jìn)行回收,衡量標(biāo)準(zhǔn)不再是它屬于哪個(gè)分代,而是哪塊內(nèi)存中存放的垃圾數(shù)量最多,回收受益最大。
不再堅(jiān)持固定大小及數(shù)量的分代區(qū)域劃分,而是把 Java 堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),每一個(gè) Region 都可以根據(jù)需要扮演新生代的 Eden 空間、Survivor 空間或老年代空間。收集器能夠?qū)Π缪莶煌巧?Region 采用不同的策略處理,這樣無論是新創(chuàng)建的對(duì)象還是已經(jīng)存活了一段時(shí)間、熬過多次收集的舊對(duì)象都能獲取很好的收集效果。
跟蹤各個(gè) Region 里垃圾堆積的價(jià)值大小,價(jià)值即回收所獲得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值,在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表,每次根據(jù)用戶設(shè)定允許的收集停頓時(shí)間優(yōu)先處理回收價(jià)值收益最大的 Region。這種方式保證了 G1 在有限時(shí)間內(nèi)獲取盡可能高的收集效率。
G1的運(yùn)作過程:
- 初始標(biāo)記:標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對(duì)象,讓下一階段用戶線程并發(fā)運(yùn)行時(shí)能正確地在可用 Region 中分配新對(duì)象。該階段需要 STW 但耗時(shí)很短,是借用進(jìn)行 Minor GC 時(shí)同步完成的。
- 并發(fā)標(biāo)記:從 GC Roots 開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,遞歸掃描整個(gè)堆的對(duì)象圖,找出需要回收的對(duì)象。該階段耗時(shí)長(zhǎng),但可與用戶線程并發(fā)執(zhí)行,當(dāng)對(duì)掃描完成后還要重新處理 SATB 記錄的在并發(fā)時(shí)有引用變動(dòng)的對(duì)象。
- 最終標(biāo)記:對(duì)用戶線程做一個(gè)短暫暫停,用于處理并發(fā)階段結(jié)束后仍遺留下來的少量 SATB 記錄。
- 篩選回收:對(duì)各個(gè) Region 的回收價(jià)值和成本排序,根據(jù)用戶期望的停頓時(shí)間制定回收計(jì)劃,可自由選擇任意多個(gè) Region 構(gòu)成回收集然后把決定回收的那部分存活對(duì)象復(fù)制到空的 Region 中,再清理掉整個(gè)舊 Region 的全部空間。該操作必須暫停用戶線程,由多條收集器線程并行完成。
可以由用戶指定期望停頓時(shí)間是 G1 的一個(gè)強(qiáng)大功能,但該值不能設(shè)得太低,一般設(shè)置為100~300毫秒比較合適。G1不存在內(nèi)存空間碎片的問題,但為了垃圾收集產(chǎn)生的內(nèi)存占用和程序運(yùn)行時(shí)的額外執(zhí)行負(fù)載都高于 CMS。
低延遲垃圾收集器:指 Shenandoah 和 ZGC,這兩個(gè)收集器幾乎整個(gè)工作過程都是并發(fā)的,只有初始標(biāo)記、最終標(biāo)記這些階段有短暫停頓,停頓時(shí)間基本上固定。
Shenandoah
相比 G1 內(nèi)存布局同樣基于 Region,默認(rèn)回收策略也是優(yōu)先處理回收價(jià)值最大的 Region。但在管理堆內(nèi)存方面與 G1 有不同:① 支持并發(fā)整理,G1 的回收階段不能與用戶線程并發(fā)。② 默認(rèn)不使用分代收集,不會(huì)有專門的新生代 Region 或老年代 Region。③ 摒棄了在 G1 中耗費(fèi)大量?jī)?nèi)存和計(jì)算資源維護(hù)的記憶集,改用名為連接矩陣的全局?jǐn)?shù)據(jù)結(jié)構(gòu)記錄跨 Region 的引用關(guān)系。
ZGC
JDK 11中新加入的具有實(shí)驗(yàn)性質(zhì)的低延遲垃圾收集器,和 Shenandoah 的目標(biāo)相似,都希望在盡可能對(duì)吞吐量影響不大的前提下實(shí)現(xiàn)把停頓時(shí)間限制在 10ms 以內(nèi)的低延遲。
基于 Region 內(nèi)存布局,不設(shè)分代,使用了讀屏障、染色指針和內(nèi)存多重映射等技術(shù)實(shí)現(xiàn)可并發(fā)的標(biāo)記-整理,以低延遲為首要目標(biāo)。 ZGC 的 Region 具有動(dòng)態(tài)性,是動(dòng)態(tài)創(chuàng)建和銷毀的,并且區(qū)容量大小也是動(dòng)態(tài)變化的。
P10:內(nèi)存分配與回收策略
以 Seial + Serial Old 客戶端默認(rèn)收集器組合為例:
對(duì)象優(yōu)先在 Eden 區(qū)分配
大多數(shù)情況下對(duì)象在新生代 Eden 區(qū)分配,當(dāng) Eden 區(qū)沒有足夠空間進(jìn)行分配時(shí)將發(fā)起一次 Minor GC。
可通過 -XX:Xms 和 -XX:Xmx 設(shè)置堆大小, -Xmn 設(shè)置新生代的大小, -XX:SurvivorRatio 設(shè)置新生代中 Eden 和 Survivor 的比例。
大對(duì)象直接進(jìn)入老年代
大對(duì)象指需要大量連續(xù)內(nèi)存空間的對(duì)象,典型的是很長(zhǎng)的字符串或元素?cái)?shù)量龐大的數(shù)組。大對(duì)象容易導(dǎo)致內(nèi)存明明還有不少空間就提前觸發(fā)垃圾收集以獲得足夠的連續(xù)空間,復(fù)制對(duì)象時(shí)大對(duì)象就意味著高額內(nèi)存復(fù)制開銷。
HotSpot 提供了 -XX:PretenureSizeThreshold 參數(shù),大于該值的對(duì)象直接在老年代分配,避免在 Eden 和 Survivor 之間來回復(fù)制產(chǎn)生大量?jī)?nèi)存復(fù)制操作。
長(zhǎng)期存活對(duì)象進(jìn)入老年代
虛擬機(jī)給每一個(gè)對(duì)象定義了一個(gè)對(duì)象年齡計(jì)數(shù)器,存儲(chǔ)在對(duì)象頭。對(duì)象通常在 Eden 誕生,如果經(jīng)歷過第一次 MinorGC 仍然存活并且能被 Survivor 容納,該對(duì)象就會(huì)被移動(dòng)到 Survivor 中并將年齡設(shè)置為 1。對(duì)象在 Survivor 中每熬過一次 MinorGC 年齡就加 1 ,當(dāng)增加到一定程度(默認(rèn)15)就會(huì)被晉升到老年代。對(duì)象晉升老年代的年齡閾值可通過 -XX:MaxTenuringThreshold 設(shè)置。
動(dòng)態(tài)對(duì)象年齡判定
為了適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不永遠(yuǎn)要求對(duì)象年齡達(dá)到閾值才能晉升老年代,如果在 Survivor 中相同年齡所有對(duì)象大小的總和大于 Survivor 的一半,年齡不小于該年齡的對(duì)象就可以直接進(jìn)入老年代,無需等到 -XX:MaxTenuringThreshold 參數(shù)設(shè)置的年齡。
空間分配擔(dān)保
發(fā)生 MinorGC 前,虛擬機(jī)必須先檢查老年代最大可用連續(xù)空間是否大于新生代所有對(duì)象總空間,如果這個(gè)條件成立,那這一次 MinorGC可以確定是安全的。
如果不成立,虛擬機(jī)會(huì)先查看 -XX:HandlePromotionFailure 參數(shù)的值是否允許擔(dān)保失敗,如果允許會(huì)繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升老年代對(duì)象的平均大小,如果滿足將冒險(xiǎn)嘗試一次 MinorGC,如果不滿足或不允許擔(dān)保失敗就會(huì)改成一次 FullGC。
之所以說冒險(xiǎn)是因?yàn)樾律褂脧?fù)制算法,為了內(nèi)存利用率只使用其中一個(gè) Survivor 作為備份,因此當(dāng)出現(xiàn)大量對(duì)象在 MinorGC 后仍然存活的情況時(shí)需要老年代進(jìn)行分配擔(dān)保,把 Survivor 無法容納的對(duì)象直接送入老年代。
P11:故障處理工具
jps:虛擬機(jī)進(jìn)程狀況工具
jps 即 JVM Process Status,參考了 UNIX 命令的命名格式,功能和 ps 命令類似:可以列出正在運(yùn)行的虛擬機(jī)進(jìn)程,并顯示虛擬機(jī)執(zhí)行主類名稱以及這些進(jìn)程的本地虛擬機(jī)唯一 ID(LVMID)。LVMID 與操作系統(tǒng)的進(jìn)程 ID(PID)是一致的,使用 Windows 的任務(wù)管理器或 UNIX 的 ps 命令也可以查詢到虛擬機(jī)進(jìn)程的 LVMID,但如果同時(shí)啟動(dòng)了多個(gè)虛擬機(jī)進(jìn)程,無法根據(jù)進(jìn)程名稱定位就必須依賴 jps 命令。
jps 還可以通過 RMI 協(xié)議查詢開啟了 RMI 服務(wù)的遠(yuǎn)程虛擬機(jī)進(jìn)程狀態(tài),參數(shù) hostid 為 RMI 注冊(cè)表中注冊(cè)的主機(jī)名。
jstat:虛擬機(jī)統(tǒng)計(jì)信息監(jiān)視工具
jstat 即 JVM Statistic Monitoring Tool,是用于監(jiān)視虛擬機(jī)各種運(yùn)行狀態(tài)信息的命令行工具。它可以顯示本地或者遠(yuǎn)程虛擬機(jī)進(jìn)程中的類加載、內(nèi)存、垃圾收集、即時(shí)編譯器等運(yùn)行時(shí)數(shù)據(jù),在沒有 GUI 界面的服務(wù)器上是運(yùn)行期定位虛擬機(jī)性能問題的常用工具。
一些參數(shù)的含義:S0 和 S1 表示兩個(gè) Survivor 區(qū),E 表示新生代,O 表示老年代,YGC 表示 Young GC 次數(shù),YGCT 表示Young GC 耗時(shí),FGC 表示 Full GC 次數(shù),FGCT 表示 Full GC 耗時(shí),GCT 表示所有 GC 總耗時(shí)。
jinfo:Java 配置信息工具
jinfo 表示 Configuration Info for Java,作用是實(shí)時(shí)查看和調(diào)整虛擬機(jī)各項(xiàng)參數(shù),使用 jps 的 -v 參數(shù)可以查看虛擬機(jī)啟動(dòng)時(shí)顯式指定的參數(shù)列表,但如果想知道未被顯式指定的參數(shù)的系統(tǒng)默認(rèn)值就只能使用 jinfo 的 -flag 選項(xiàng)進(jìn)行查詢。jinfo 還可以把虛擬機(jī)進(jìn)程的 System.getProperties() 的內(nèi)容打印出來。
jmap:Java 內(nèi)存映像工具
jmap 表示 Memory Map for Java,jamp 命令用于生成堆轉(zhuǎn)儲(chǔ)快照,還可以查詢 finalize 執(zhí)行隊(duì)列、Java 堆和方法區(qū)的詳細(xì)信息,如空間使用率,當(dāng)前使用的是哪種收集器等。和 jinfo 命令一樣,有部分功能在 Windows 平臺(tái)下受限,除了生成堆轉(zhuǎn)儲(chǔ)快照的 -dump 選項(xiàng)和用于查看每個(gè)類實(shí)例的 -histo 選項(xiàng)外,其余選項(xiàng)都只能在 Linux 使用。
jhat:虛擬機(jī)堆轉(zhuǎn)儲(chǔ)快照分析工具
jhat 表示 JVM Heap Analysis Tool,JDK 提供 jhat 命令與 jmap 搭配使用來分析 jamp 生成的堆轉(zhuǎn)儲(chǔ)快照。jhat 內(nèi)置了一個(gè)微型的 HTTP/Web 服務(wù)器,生成堆轉(zhuǎn)儲(chǔ)快照的分析結(jié)果后可以在瀏覽器中查看。
jstack:Java 堆棧跟蹤工具
jstack 表示 Stack Trace for Java,jstack 命令用于生成虛擬機(jī)當(dāng)前時(shí)刻的線程快照。線程快照就是當(dāng)前虛擬機(jī)內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的目的通常是定位線程出現(xiàn)長(zhǎng)時(shí)間停頓的原因,如線程間死鎖、死循環(huán)、請(qǐng)求外部資源導(dǎo)致的長(zhǎng)時(shí)間掛起等。線程出現(xiàn)停頓時(shí)通過 jstack 查看各個(gè)線程的調(diào)用堆棧,就可以獲知沒有響應(yīng)的現(xiàn)場(chǎng)到底在后臺(tái)做什么或等待什么資源。
除了上述的基礎(chǔ)故障處理工具,還有一些可視化故障處理工具,例如 JHSDB 基于服務(wù)性代理的調(diào)試工具、JConsole Java 監(jiān)視與管理控制臺(tái)、VisualVM 多合一故障處理工具、Java Mission Control 可持續(xù)在線監(jiān)控工具。
P12:Java 程序運(yùn)行的過程
通過 Javac 編譯器將 .java 代碼轉(zhuǎn)為 JVM 可以加載的 .class 字節(jié)碼文件。
Javac 編譯器是由 Java 語言編寫的程序,從 Javac 代碼的總體結(jié)構(gòu)看,編譯過程可以分為: ① 詞法解析,通過空格分割處單詞、操作符、控制符等信息,將其形成 token 信息流,傳遞給語法解析器。② 語法解析,把詞法解析得到的 token 信息流按照 Java 語法規(guī)則組裝成一顆語法樹。④ 語義分析,檢查關(guān)鍵字的使用是否合理、類型是否匹配、作用域是否正確等。④ 字節(jié)碼生成,將前面各個(gè)步驟的信息轉(zhuǎn)換為字節(jié)碼。
字節(jié)碼必須通過類加載過程加載到 JVM 環(huán)境后才可以執(zhí)行,執(zhí)行有三種模式,解釋執(zhí)行、JIT 編譯執(zhí)行、JIT 編譯與解釋器混合執(zhí)行(主流 JVM 默認(rèn)執(zhí)行的方式)。混合模式的優(yōu)勢(shì)在于解釋器在啟動(dòng)時(shí)先解釋執(zhí)行,省去編譯時(shí)間。
通過即時(shí)編譯器 JIT 把字節(jié)碼文件編譯成本地機(jī)器碼。
Java 程序最初都是通過解釋器進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁,就會(huì)把這些代碼認(rèn)定為"熱點(diǎn)代碼",熱點(diǎn)代碼的檢測(cè)主要有基于采樣和基于計(jì)數(shù)器兩種方式,為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時(shí)虛擬機(jī)會(huì)把這些代碼編譯成本地機(jī)器碼,并盡可能對(duì)代碼優(yōu)化,在運(yùn)行時(shí)完成這個(gè)任務(wù)的后端編譯器被稱為即時(shí)編譯器。
還可以通過靜態(tài)的提前編譯器 AOT 直接把程序編譯成與目標(biāo)機(jī)器指令集相關(guān)的二進(jìn)制代碼。
P13:類加載機(jī)制和初始化時(shí)機(jī)
在 Class 文件中描述的各類信息最終都需要加載到虛擬機(jī)后才能運(yùn)行和使用。JVM 把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java類型,這個(gè)過程被稱為虛擬機(jī)的類加載機(jī)制。與其他在編譯時(shí)需要連接的語言不同,Java 中類型的加載、連接和初始化都是在程序運(yùn)行期間完成的,這種策略讓 Java 進(jìn)行類加載時(shí)增加了性能開銷,但卻為 Java 應(yīng)用提供了極高的擴(kuò)展性和靈活性,Java 可以動(dòng)態(tài)擴(kuò)展的語言特性就是依賴運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的。
一個(gè)類型從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,整個(gè)生命周期將會(huì)經(jīng)歷加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段,其中驗(yàn)證、解析和初始化三個(gè)部分統(tǒng)稱為連接。加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持 Java 語言的動(dòng)態(tài)綁定特性。
關(guān)于何shuyu時(shí)需要開始類加載的第一個(gè)階段"加載",《 Java 虛擬機(jī)規(guī)范》沒有強(qiáng)制約束,但對(duì)于初始化嚴(yán)格規(guī)定了有且只有6種情況:
- 遇到 new、getstatic、putstatic 或 invokestatic 這四條字節(jié)碼指令時(shí),如果類型沒有初始化則需要先觸發(fā)初始化。典型場(chǎng)景有:① 使用new關(guān)鍵字實(shí)例化對(duì)象。② 讀取或設(shè)置一個(gè)類型的靜態(tài)字段。③ 調(diào)用一個(gè)類型的靜態(tài)方法。
- 對(duì)類型進(jìn)行反射調(diào)用時(shí),如果類型沒有初始化則需要先觸發(fā)初始化。
- 當(dāng)初始化類時(shí),如果其父類沒有初始化則需要先觸發(fā)父類的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類即包含 main 方法的類,虛擬機(jī)會(huì)先初始化該類。
- 當(dāng)使用 JDK 7 新加入的動(dòng)態(tài)語言支持時(shí),如果 MethodHandle 實(shí)例的解析結(jié)果為指定類型的方法句柄且這個(gè)句柄對(duì)應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
- 當(dāng)一個(gè)接口定義了默認(rèn)方法時(shí),如果該接口的實(shí)現(xiàn)類發(fā)生初始化,那接口要在其之前初始化。
除了這六種情況外其余所有引用類型的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。被動(dòng)引用的實(shí)例:① 子類使用父類的靜態(tài)字段時(shí),只有直接定義這個(gè)字段的父類會(huì)被初始化。② 通過數(shù)組定義使用類。③ 常量在編譯期會(huì)存入調(diào)用類的常量池,不會(huì)初始化定義常量的類。
接口的加載過程和類真正有所區(qū)別的是當(dāng)初始化類時(shí),如果其父類沒有初始化則需要先觸發(fā)其父類的初始化,但在一個(gè)接口初始化時(shí)并不要求其父接口全部完成了初始化,只有在真正使用到父接口時(shí)(如引用接口中定義的常量)才會(huì)初始化。
P14:類加載過程
加載
加載是類加載的第一個(gè)階段,在該階段虛擬機(jī)需要完成三件事:① 通過一個(gè)類的全限定類名來獲取定義此類的二進(jìn)制字節(jié)流。② 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)區(qū)結(jié)構(gòu)。③ 在內(nèi)存中生成對(duì)應(yīng)該類的 Class 實(shí)例,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。加載與連接的部分動(dòng)作是交叉進(jìn)行的,加載尚未完成時(shí)連接可能已經(jīng)開始。
驗(yàn)證
驗(yàn)證是連接的第一步,目的是確保 Class 文件的字節(jié)流中包含的信息符合約束要求,保證這些信息不會(huì)危害虛擬機(jī)的安全。如果虛擬機(jī)不檢查輸入的字節(jié)流,很可能因?yàn)檩d入了有錯(cuò)誤或惡意企圖的字節(jié)流而導(dǎo)致整個(gè)系統(tǒng)受攻擊。驗(yàn)證主要包含了四個(gè)階段:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證。
驗(yàn)證對(duì)于虛擬機(jī)的類加載機(jī)制來說是一個(gè)非常重要但非必需的階段,因?yàn)轵?yàn)證只有通過與否的區(qū)別,只要通過了驗(yàn)證其后就對(duì)程序運(yùn)行期沒有任何影響了。如果程序運(yùn)行的全部代碼都已被反復(fù)使用和驗(yàn)證過,在生產(chǎn)環(huán)境的就可以考慮關(guān)閉大部分類驗(yàn)證措施縮短類加載時(shí)間。
準(zhǔn)備
準(zhǔn)備是正式為類靜態(tài)變量分配內(nèi)存并設(shè)置零值的階段,該階段進(jìn)行的內(nèi)存分配僅包括類變量,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在 Java 堆中。如果變量被final修飾,編譯時(shí) Javac 會(huì)為變量生成 ConstantValue 屬性,那么在準(zhǔn)備階段虛擬機(jī)就會(huì)將該變量的值設(shè)為程序員指定的值。
解析
解析是將常量池內(nèi)的符號(hào)引用替換為直接引用的過程。
- 符號(hào)引用:符號(hào)引用以一組符號(hào)描述引用目標(biāo),可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。與虛擬機(jī)內(nèi)存布局無關(guān),引用目標(biāo)并不一定是已經(jīng)加載到虛擬機(jī)內(nèi)存中的內(nèi)容。
- 直接引用:直接引用是可以直接指向目標(biāo)的指針、相對(duì)偏移量或者能間接定位到目標(biāo)的句柄。和虛擬機(jī)的內(nèi)存布局直接相關(guān),引用目標(biāo)必須已在虛擬機(jī)的內(nèi)存中存在。
解析部分主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符這7類符合引用進(jìn)行。
初始化
初始化是類加載過程的最后一步,直到該階段,JVM 才真正開始執(zhí)行類中編寫的代碼。準(zhǔn)備階段時(shí)變量已經(jīng)賦過一次系統(tǒng)零值,而在初始化階段會(huì)根據(jù)程序員的編碼去初始化類變量和其他資源。初始化階段就是執(zhí)行類構(gòu)造方法中的 方法,該方法是 Javac 編譯器自動(dòng)生成的。
P15:類加載器和雙親委派模型
類加載階段中"通過一個(gè)類的全限定名來獲取描述該類的二進(jìn)制字節(jié)流"的動(dòng)作被設(shè)計(jì)為放到 JVM 外部實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何獲取所需的類,實(shí)現(xiàn)這個(gè)動(dòng)作的代碼就是類加載器。
自 JDK1.2 起 Java 一直保持著三層類加載器、雙親委派的類加載模型:
- 啟動(dòng)類加載器
啟動(dòng)類加載器在 JVM 啟動(dòng)時(shí)創(chuàng)建,負(fù)責(zé)加載最核心的類,例如 Object、System、String 等。啟動(dòng)類加載器無法被 Java 程序直接引用,如果用戶需要把加載請(qǐng)求委派給啟動(dòng)類加載器,直接使用 null 代替即可,因?yàn)閱?dòng)類加載器通常由操作系統(tǒng)相關(guān)的本地代碼實(shí)現(xiàn),并不存在于 JVM 體系中。 - 平臺(tái)類加載器
從 JDK9 開始從擴(kuò)展類加載器更換為平臺(tái)類加載器,負(fù)載加載一些擴(kuò)展的系統(tǒng)類,比如 XML、加密、壓縮相關(guān)的功能類等。 - 應(yīng)用類加載器
也稱系統(tǒng)類加載器,負(fù)載加載用戶類路徑上的所有類庫(kù),可以直接在代碼中使用。如果應(yīng)用程序中沒有自定義類加載器,一般情況下應(yīng)用類加載器就是中默認(rèn)的類加載器。自定義類加載器通過繼承 ClassLoader 并重寫 findClass 方法,調(diào)用 defineClass 方法實(shí)現(xiàn)。
雙親委派模型
類加載器具有等級(jí)制度,但并非繼承關(guān)系,以組合的方式來復(fù)用父加載器的功能,這也符合組合優(yōu)先原則。雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)該有自己的父加載器。
如果一個(gè)類加載器收到了類加載請(qǐng)求,它不會(huì)自己去嘗試加載這個(gè)類,而首先將該請(qǐng)求委派給自己的父加載器完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到最頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成請(qǐng)求時(shí),子加載器才會(huì)嘗試自己完成加載。
好處是 Java 中的類跟隨它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,可以保證某個(gè)類在程序的各個(gè)類加載器環(huán)境中都是同一個(gè)類,對(duì)于保證程序的穩(wěn)定運(yùn)行極為重要。
比較兩個(gè)類是否相等
對(duì)于任意一個(gè)類,都必須由加載它的類加載器和這個(gè)類本身一起共同確立其在虛擬機(jī)中的唯一性,每一個(gè)類加載器都擁有一個(gè)獨(dú)立的類名稱空間。只有在兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則即使兩個(gè)類來源于同一個(gè) Class 文件,被同一個(gè) JVM 加載,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。
總結(jié)
以上是生活随笔為你收集整理的【备战秋招系列-3】Java高频知识点——排序、设计模式、JavaSE、JVM的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle sqlcode 多条,or
- 下一篇: Java基础总结(上)