资料搜集-JAVA系统的梳理知识5-JAVA基础篇JVM
生活随笔
收集整理的這篇文章主要介紹了
资料搜集-JAVA系统的梳理知识5-JAVA基础篇JVM
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
?
<!-- TOC -->- [Collections 工具類和 Arrays 工具類常見方法](#collections-工具類和-arrays-工具類常見方法)- [Collections](#collections)- [排序操作](#排序操作)- [查找,替換操作](#查找替換操作)- [同步控制](#同步控制)- [Arrays類的常見操作](#arrays類的常見操作)- [排序 : `sort()`](#排序--sort)- [查找 : `binarySearch()`](#查找--binarysearch)- [比較: `equals()`](#比較-equals)- [填充 : `fill()`](#填充--fill)- [轉列表 `asList()`](#轉列表-aslist)- [轉字符串 `toString()`](#轉字符串-tostring)- [復制 `copyOf()`](#復制-copyof)<!-- /TOC --> # Collections 工具類和 Arrays 工具類常見方法## CollectionsCollections 工具類常用方法:1. 排序 2. 查找,替換操作 3. 同步控制(不推薦,需要線程安全的集合類型時請考慮使用 JUC 包下的并發集合)### 排序操作```java void reverse(List list)//反轉 void shuffle(List list)//隨機排序 void sort(List list)//按自然排序的升序排序 void sort(List list, Comparator c)//定制排序,由Comparator控制排序邏輯 void swap(List list, int i , int j)//交換兩個索引位置的元素 void rotate(List list, int distance)//旋轉。當distance為正數時,將list后distance個元素整體移到前面。當distance為負數時,將 list的前distance個元素整體移到后面。 ```**示例代碼:**```javaArrayList<Integer> arrayList = new ArrayList<Integer>();arrayList.add(-1);arrayList.add(3);arrayList.add(3);arrayList.add(-5);arrayList.add(7);arrayList.add(4);arrayList.add(-9);arrayList.add(-7);System.out.println("原始數組:");System.out.println(arrayList);// void reverse(List list):反轉Collections.reverse(arrayList);System.out.println("Collections.reverse(arrayList):");System.out.println(arrayList);Collections.rotate(arrayList, 4);System.out.println("Collections.rotate(arrayList, 4):");System.out.println(arrayList);// void sort(List list),按自然排序的升序排序Collections.sort(arrayList);System.out.println("Collections.sort(arrayList):");System.out.println(arrayList);// void shuffle(List list),隨機排序Collections.shuffle(arrayList);System.out.println("Collections.shuffle(arrayList):");System.out.println(arrayList);// void swap(List list, int i , int j),交換兩個索引位置的元素Collections.swap(arrayList, 2, 5);System.out.println("Collections.swap(arrayList, 2, 5):");System.out.println(arrayList);// 定制排序的用法Collections.sort(arrayList, new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);}});System.out.println("定制排序后:");System.out.println(arrayList); ```### 查找,替換操作```java int binarySearch(List list, Object key)//對List進行二分查找,返回索引,注意List必須是有序的 int max(Collection coll)//根據元素的自然順序,返回最大的元素。 類比int min(Collection coll) int max(Collection coll, Comparator c)//根據定制排序,返回最大元素,排序規則由Comparatator類控制。類比int min(Collection coll, Comparator c) void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。 int frequency(Collection c, Object o)//統計元素出現次數 int indexOfSubList(List list, List target)//統計target在list中第一次出現的索引,找不到則返回-1,類比int lastIndexOfSubList(List source, list target). boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替換舊元素 ```**示例代碼:**```javaArrayList<Integer> arrayList = new ArrayList<Integer>();arrayList.add(-1);arrayList.add(3);arrayList.add(3);arrayList.add(-5);arrayList.add(7);arrayList.add(4);arrayList.add(-9);arrayList.add(-7);ArrayList<Integer> arrayList2 = new ArrayList<Integer>();arrayList2.add(-3);arrayList2.add(-5);arrayList2.add(7);System.out.println("原始數組:");System.out.println(arrayList);System.out.println("Collections.max(arrayList):");System.out.println(Collections.max(arrayList));System.out.println("Collections.min(arrayList):");System.out.println(Collections.min(arrayList));System.out.println("Collections.replaceAll(arrayList, 3, -3):");Collections.replaceAll(arrayList, 3, -3);System.out.println(arrayList);System.out.println("Collections.frequency(arrayList, -3):");System.out.println(Collections.frequency(arrayList, -3));System.out.println("Collections.indexOfSubList(arrayList, arrayList2):");System.out.println(Collections.indexOfSubList(arrayList, arrayList2));System.out.println("Collections.binarySearch(arrayList, 7):");// 對List進行二分查找,返回索引,List必須是有序的Collections.sort(arrayList);System.out.println(Collections.binarySearch(arrayList, 7)); ```### 同步控制Collections提供了多個`synchronizedXxx()`方法·,該方法可以將指定集合包裝成線程同步的集合,從而解決多線程并發訪問集合時的線程安全問題。我們知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是線程不安全的。Collections提供了多個靜態方法可以把他們包裝成線程同步的集合。**最好不要用下面這些方法,效率非常低,需要線程安全的集合類型時請考慮使用 JUC 包下的并發集合。**方法如下:```java synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(線程安全的)collection。 synchronizedList(List<T> list)//返回指定列表支持的同步(線程安全的)List。 synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(線程安全的)Map。 synchronizedSet(Set<T> s) //返回指定 set 支持的同步(線程安全的)set。 ```### Collections還可以設置不可變集合,提供了如下三類方法:```java emptyXxx(): 返回一個空的、不可變的集合對象,此處的集合既可以是List,也可以是Set,還可以是Map。 singletonXxx(): 返回一個只包含指定對象(只有一個或一個元素)的不可變的集合對象,此處的集合可以是:List,Set,Map。 unmodifiableXxx(): 返回指定集合對象的不可變視圖,此處的集合可以是:List,Set,Map。 上面三類方法的參數是原有的集合對象,返回值是該集合的”只讀“版本。 ```**示例代碼:**```javaArrayList<Integer> arrayList = new ArrayList<Integer>();arrayList.add(-1);arrayList.add(3);arrayList.add(3);arrayList.add(-5);arrayList.add(7);arrayList.add(4);arrayList.add(-9);arrayList.add(-7);HashSet<Integer> integers1 = new HashSet<>();integers1.add(1);integers1.add(3);integers1.add(2);Map scores = new HashMap();scores.put("語文" , 80);scores.put("Java" , 82);//Collections.emptyXXX();創建一個空的、不可改變的XXX對象List<Object> list = Collections.emptyList();System.out.println(list);//[]Set<Object> objects = Collections.emptySet();System.out.println(objects);//[]Map<Object, Object> objectObjectMap = Collections.emptyMap();System.out.println(objectObjectMap);//{}//Collections.singletonXXX();List<ArrayList<Integer>> arrayLists = Collections.singletonList(arrayList);System.out.println(arrayLists);//[[-1, 3, 3, -5, 7, 4, -9, -7]]//創建一個只有一個元素,且不可改變的Set對象Set<ArrayList<Integer>> singleton = Collections.singleton(arrayList);System.out.println(singleton);//[[-1, 3, 3, -5, 7, 4, -9, -7]]Map<String, String> nihao = Collections.singletonMap("1", "nihao");System.out.println(nihao);//{1=nihao}//unmodifiableXXX();創建普通XXX對象對應的不可變版本List<Integer> integers = Collections.unmodifiableList(arrayList);System.out.println(integers);//[-1, 3, 3, -5, 7, 4, -9, -7]Set<Integer> integers2 = Collections.unmodifiableSet(integers1);System.out.println(integers2);//[1, 2, 3]Map<Object, Object> objectObjectMap2 = Collections.unmodifiableMap(scores);System.out.println(objectObjectMap2);//{Java=82, 語文=80}//添加出現異常:java.lang.UnsupportedOperationException // list.add(1); // arrayLists.add(arrayList); // integers.add(1); ```## Arrays類的常見操作 1. 排序 : `sort()` 2. 查找 : `binarySearch()` 3. 比較: `equals()` 4. 填充 : `fill()` 5. 轉列表: `asList()` 6. 轉字符串 : `toString()` 7. 復制: `copyOf()`### 排序 : `sort()````java// *************排序 sort****************int a[] = { 1, 3, 2, 7, 6, 5, 4, 9 };// sort(int[] a)方法按照數字順序排列指定的數組。Arrays.sort(a);System.out.println("Arrays.sort(a):");for (int i : a) {System.out.print(i);}// 換行System.out.println();// sort(int[] a,int fromIndex,int toIndex)按升序排列數組的指定范圍int b[] = { 1, 3, 2, 7, 6, 5, 4, 9 };Arrays.sort(b, 2, 6);System.out.println("Arrays.sort(b, 2, 6):");for (int i : b) {System.out.print(i);}// 換行System.out.println();int c[] = { 1, 3, 2, 7, 6, 5, 4, 9 };// parallelSort(int[] a) 按照數字順序排列指定的數組(并行的)。同sort方法一樣也有按范圍的排序Arrays.parallelSort(c);System.out.println("Arrays.parallelSort(c):");for (int i : c) {System.out.print(i);}// 換行System.out.println();// parallelSort給字符數組排序,sort也可以char d[] = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };Arrays.parallelSort(d);System.out.println("Arrays.parallelSort(d):");for (char d2 : d) {System.out.print(d2);}// 換行System.out.println();```在做算法面試題的時候,我們還可能會經常遇到對字符串排序的情況,`Arrays.sort()` 對每個字符串的特定位置進行比較,然后按照升序排序。```java String[] strs = { "abcdehg", "abcdefg", "abcdeag" }; Arrays.sort(strs); System.out.println(Arrays.toString(strs));//[abcdeag, abcdefg, abcdehg] ```### 查找 : `binarySearch()````java// *************查找 binarySearch()****************char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };// 排序后再進行二分查找,否則找不到Arrays.sort(e);System.out.println("Arrays.sort(e)" + Arrays.toString(e));System.out.println("Arrays.binarySearch(e, 'c'):");int s = Arrays.binarySearch(e, 'c');System.out.println("字符c在數組的位置:" + s); ```### 比較: `equals()````java// *************比較 equals****************char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };char[] f = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };/** 元素數量相同,并且相同位置的元素相同。 另外,如果兩個數組引用都是null,則它們被認為是相等的 。*/// 輸出trueSystem.out.println("Arrays.equals(e, f):" + Arrays.equals(e, f)); ```### 填充 : `fill()````java// *************填充fill(批量初始化)****************int[] g = { 1, 2, 3, 3, 3, 3, 6, 6, 6 };// 數組中所有元素重新分配值Arrays.fill(g, 3);System.out.println("Arrays.fill(g, 3):");// 輸出結果:333333333for (int i : g) {System.out.print(i);}// 換行System.out.println();int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, };// 數組中指定范圍元素重新分配值Arrays.fill(h, 0, 2, 9);System.out.println("Arrays.fill(h, 0, 2, 9);:");// 輸出結果:993333666for (int i : h) {System.out.print(i);} ```### 轉列表 `asList()````java// *************轉列表 asList()****************/** 返回由指定數組支持的固定大小的列表。* (將返回的列表更改為“寫入數組”。)該方法作為基于數組和基于集合的API之間的橋梁,與Collection.toArray()相結合 。* 返回的列表是可序列化的,并實現RandomAccess 。* 此方法還提供了一種方便的方式來創建一個初始化為包含幾個元素的固定大小的列表如下:*/List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");System.out.println(stooges); ```### 轉字符串 `toString()````java// *************轉字符串 toString()****************/** 返回指定數組的內容的字符串表示形式。*/char[] k = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };System.out.println(Arrays.toString(k));// [a, f, b, c, e, A, C, B] ```### 復制 `copyOf()````java// *************復制 copy****************// copyOf 方法實現數組復制,h為數組,6為復制的長度int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, };int i[] = Arrays.copyOf(h, 6);System.out.println("Arrays.copyOf(h, 6);:");// 輸出結果:123333for (int j : i) {System.out.print(j);}// 換行System.out.println();// copyOfRange將指定數組的指定范圍復制到新數組中int j[] = Arrays.copyOfRange(h, 6, 11);System.out.println("Arrays.copyOfRange(h, 6, 11):");// 輸出結果66600(h數組只有9個元素這里是從索引6到索引11復制所以不足的就為0)for (int j2 : j) {System.out.print(j2);}// 換行System.out.println(); ``` <!-- MarkdownTOC -->- [final,static,this,super 關鍵字總結](#finalstaticthissuper-關鍵字總結)- [final 關鍵字](#final-關鍵字)- [static 關鍵字](#static-關鍵字)- [this 關鍵字](#this-關鍵字)- [super 關鍵字](#super-關鍵字)- [參考](#參考) - [static 關鍵字詳解](#static-關鍵字詳解)- [static 關鍵字主要有以下四種使用場景](#static-關鍵字主要有以下四種使用場景)- [修飾成員變量和成員方法\(常用\)](#修飾成員變量和成員方法常用)- [靜態代碼塊](#靜態代碼塊)- [靜態內部類](#靜態內部類)- [靜態導包](#靜態導包)- [補充內容](#補充內容)- [靜態方法與非靜態方法](#靜態方法與非靜態方法)- [static{}靜態代碼塊與{}非靜態代碼塊\(構造代碼塊\)](#static靜態代碼塊與非靜態代碼塊構造代碼塊)- [參考](#參考-1)<!-- /MarkdownTOC --># final,static,this,super 關鍵字總結## final 關鍵字**final關鍵字主要用在三個地方:變量、方法、類。**1. **對于一個final變量,如果是基本數據類型的變量,則其數值一旦在初始化之后便不能更改;如果是引用類型的變量,則在對其初始化之后便不能再讓其指向另一個對象。**2. **當用final修飾一個類時,表明這個類不能被繼承。final類中的所有成員方法都會被隱式地指定為final方法。**3. 使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。在早期的Java實現版本中,會將final方法轉為內嵌調用。但是如果方法過于龐大,可能看不到內嵌調用帶來的任何性能提升(現在的Java版本已經不需要使用final方法進行這些優化了)。類中所有的private方法都隱式地指定為final。## static 關鍵字**static 關鍵字主要有以下四種使用場景:**1. **修飾成員變量和成員方法:** 被 static 修飾的成員屬于類,不屬于單個這個類的某個對象,被類中所有對象共享,可以并且建議通過類名調用。被static 聲明的成員變量屬于靜態成員變量,靜態變量 存放在 Java 內存區域的方法區。調用格式:`類名.靜態變量名` `類名.靜態方法名()` 2. **靜態代碼塊:** 靜態代碼塊定義在類中方法外, 靜態代碼塊在非靜態代碼塊之前執行(靜態代碼塊—>非靜態代碼塊—>構造方法)。 該類不管創建多少對象,靜態代碼塊只執行一次. 3. **靜態內部類(static修飾類的話只能修飾內部類):** 靜態內部類與非靜態內部類之間存在一個最大的區別: 非靜態內部類在編譯完成之后會隱含地保存著一個引用,該引用是指向創建它的外圍類,但是靜態內部類卻沒有。沒有這個引用就意味著:1. 它的創建是不需要依賴外圍類的創建。2. 它不能使用任何外圍類的非static成員變量和方法。 4. **靜態導包(用來導入類中的靜態資源,1.5之后的新特性):** 格式為:`import static` 這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,并且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法。## this 關鍵字this關鍵字用于引用類的當前實例。 例如:```java class Manager {Employees[] employees;void manageEmployees() {int totalEmp = this.employees.length;System.out.println("Total employees: " + totalEmp);this.report();}void report() { } } ```在上面的示例中,this關鍵字用于兩個地方:- this.employees.length:訪問類Manager的當前實例的變量。 - this.report():調用類Manager的當前實例的方法。此關鍵字是可選的,這意味著如果上面的示例在不使用此關鍵字的情況下表現相同。 但是,使用此關鍵字可能會使代碼更易讀或易懂。## super 關鍵字super關鍵字用于從子類訪問父類的變量和方法。 例如:```java public class Super {protected int number;protected showNumber() {System.out.println("number = " + number);} }public class Sub extends Super {void bar() {super.number = 10;super.showNumber();} } ```在上面的例子中,Sub 類訪問父類成員變量 number 并調用其其父類 Super 的 `showNumber()` 方法。**使用 this 和 super 要注意的問題:**- 在構造器中使用 `super()` 調用父類中的其他構造方法時,該語句必須處于構造器的首行,否則編譯器會報錯。另外,this 調用本類中的其他構造方法時,也要放在首行。 - this、super不能用在static方法中。**簡單解釋一下:**被 static 修飾的成員屬于類,不屬于單個這個類的某個對象,被類中所有對象共享。而 this 代表對本類對象的引用,指向本類對象;而 super 代表對父類對象的引用,指向父類對象;所以, **this和super是屬于對象范疇的東西,而靜態方法是屬于類范疇的東西**。## 參考- https://www.codejava.net/java-core/the-java-language/java-keywords - https://blog.csdn.net/u013393958/article/details/79881037# static 關鍵字詳解## static 關鍵字主要有以下四種使用場景1. 修飾成員變量和成員方法 2. 靜態代碼塊 3. 修飾類(只能修飾內部類) 4. 靜態導包(用來導入類中的靜態資源,1.5之后的新特性)### 修飾成員變量和成員方法(常用)被 static 修飾的成員屬于類,不屬于單個這個類的某個對象,被類中所有對象共享,可以并且建議通過類名調用。被static 聲明的成員變量屬于靜態成員變量,靜態變量 存放在 Java 內存區域的方法區。方法區與 Java 堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。HotSpot 虛擬機中方法區也常被稱為 “永久代”,本質上兩者并不等價。僅僅是因為 HotSpot 虛擬機設計團隊用永久代來實現方法區而已,這樣 HotSpot 虛擬機的垃圾收集器就可以像管理 Java 堆一樣管理這部分內存了。但是這并不是一個好主意,因為這樣更容易遇到內存溢出問題。調用格式:- 類名.靜態變量名 - 類名.靜態方法名()如果變量或者方法被 private 則代表該屬性或者該方法只能在類的內部被訪問而不能在類的外部被訪問。測試方法:```java public class StaticBean {String name;靜態變量static int age;public StaticBean(String name) {this.name = name;}靜態方法static void SayHello() {System.out.println(Hello i am java);}@Overridepublic String toString() {return StaticBean{ +name=' + name + ''' + age + age +'}';} } ``````java public class StaticDemo {public static void main(String[] args) {StaticBean staticBean = new StaticBean(1);StaticBean staticBean2 = new StaticBean(2);StaticBean staticBean3 = new StaticBean(3);StaticBean staticBean4 = new StaticBean(4);StaticBean.age = 33;StaticBean{name='1'age33} StaticBean{name='2'age33} StaticBean{name='3'age33} StaticBean{name='4'age33}System.out.println(staticBean+ +staticBean2+ +staticBean3+ +staticBean4);StaticBean.SayHello();Hello i am java}} ```### 靜態代碼塊靜態代碼塊定義在類中方法外, 靜態代碼塊在非靜態代碼塊之前執行(靜態代碼塊—非靜態代碼塊—構造方法)。 該類不管創建多少對象,靜態代碼塊只執行一次.靜態代碼塊的格式是 ``` static { 語句體; } ```一個類中的靜態代碼塊可以有多個,位置可以隨便放,它不在任何的方法體內,JVM加載類時會執行這些靜態的代碼塊,如果靜態代碼塊有多個,JVM將按照它們在類中出現的先后順序依次執行它們,每個代碼塊只會被執行一次。靜態代碼塊對于定義在它之后的靜態變量,可以賦值,但是不能訪問.### 靜態內部類靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之后會隱含地保存著一個引用,該引用是指向創建它的外圍類,但是靜態內部類卻沒有。沒有這個引用就意味著:1. 它的創建是不需要依賴外圍類的創建。 2. 它不能使用任何外圍類的非static成員變量和方法。Example(靜態內部類實現單例模式)```java public class Singleton {聲明為 private 避免調用默認構造方法創建對象private Singleton() {}聲明為 private 表明靜態內部該類只能在該 Singleton 類中被訪問private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getUniqueInstance() {return SingletonHolder.INSTANCE;} } ```當 Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 `getUniqueInstance() `方法從而觸發 `SingletonHolder.INSTANCE` 時 SingletonHolder 才會被加載,此時初始化 INSTANCE 實例,并且 JVM 能確保 INSTANCE 只被實例化一次。這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對線程安全的支持。### 靜態導包格式為:import static 這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,并且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法```javaMath. --- 將Math中的所有靜態資源導入,這時候可以直接使用里面的靜態方法,而不用通過類名進行調用如果只想導入單一某個靜態方法,只需要將換成對應的方法名即可import static java.lang.Math.;換成import static java.lang.Math.max;具有一樣的效果public class Demo {public static void main(String[] args) {int max = max(1,2);System.out.println(max);} }```## 補充內容### 靜態方法與非靜態方法靜態方法屬于類本身,非靜態方法屬于從該類生成的每個對象。 如果您的方法執行的操作不依賴于其類的各個變量和方法,請將其設置為靜態(這將使程序的占用空間更小)。 否則,它應該是非靜態的。Example```java class Foo {int i;public Foo(int i) { this.i = i;}public static String method1() {return An example string that doesn't depend on i (an instance variable);}public int method2() {return this.i + 1; Depends on i}} ``` 你可以像這樣調用靜態方法:`Foo.method1()`。 如果您嘗試使用這種方法調用 method2 將失敗。 但這樣可行:`Foo bar = new Foo(1);bar.method2();`總結:- 在外部調用靜態方法時,可以使用”類名.方法名”的方式,也可以使用”對象名.方法名”的方式。而實例方法只有后面這種方式。也就是說,調用靜態方法可以無需創建對象。 - 靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變量和靜態方法),而不允許訪問實例成員變量和實例方法;實例方法則無此限制 ### static{}靜態代碼塊與{}非靜態代碼塊(構造代碼塊)相同點: 都是在JVM加載類時且在構造方法執行之前執行,在類中都可以定義多個,定義多個時按定義的順序執行,一般在代碼塊中對一些static變量進行賦值。 不同點: 靜態代碼塊在非靜態代碼塊之前執行(靜態代碼塊—非靜態代碼塊—構造方法)。靜態代碼塊只在第一次new執行一次,之后不再執行,而非靜態代碼塊在每new一次就執行一次。 非靜態代碼塊可在普通方法中定義(不過作用不大);而靜態代碼塊不行。 一般情況下,如果有些代碼比如一些項目最常用的變量或對象必須在項目啟動的時候就執行的時候,需要使用靜態代碼塊,這種代碼是主動執行的。如果我們想要設計不需要創建對象就可以調用類中的方法,例如:Arrays類,Character類,String類等,就需要使用靜態方法, 兩者的區別是 靜態代碼塊是自動執行的而靜態方法是被調用的時候才執行的. Example```java public class Test {public Test() {System.out.print(默認構造方法!--);}非靜態代碼塊{System.out.print(非靜態代碼塊!--);}靜態代碼塊static {System.out.print(靜態代碼塊!--);}public static void test() {System.out.print(靜態方法中的內容! --);{System.out.print(靜態方法中的代碼塊!--);}}public static void main(String[] args) {Test test = new Test(); Test.test();靜態代碼塊!--靜態方法中的內容! --靜態方法中的代碼塊!--} ```當執行 `Test.test();` 時輸出:``` 靜態代碼塊!--靜態方法中的內容! --靜態方法中的代碼塊!-- ```當執行 `Test test = new Test();` 時輸出:``` 靜態代碼塊!--非靜態代碼塊!--默認構造方法!-- ```非靜態代碼塊與構造函數的區別是: 非靜態代碼塊是給所有對象進行統一初始化,而構造函數是給對應的對象初始化,因為構造函數是可以多個的,運行哪個構造函數就會建立什么樣的對象,但無論建立哪個對象,都會先執行相同的構造代碼塊。也就是說,構造代碼塊中定義的是不同對象共性的初始化內容。 ### 參考- httpsblog.csdn.netchen13579867831articledetails78995480 - httpwww.cnblogs.comchenssyp3388487.html - httpwww.cnblogs.comQian123p5713440.html <!-- MarkdownTOC -->- [ArrayList簡介](#arraylist簡介) - [ArrayList核心源碼](#arraylist核心源碼) - [ArrayList源碼分析](#arraylist源碼分析)- [System.arraycopy\(\)和Arrays.copyOf\(\)方法](#systemarraycopy和arrayscopyof方法)- [兩者聯系與區別](#兩者聯系與區別)- [ArrayList核心擴容技術](#arraylist核心擴容技術)- [內部類](#內部類) - [ArrayList經典Demo](#arraylist經典demo)<!-- /MarkdownTOC -->### ArrayList簡介ArrayList 的底層是數組隊列,相當于動態數組。與 Java 中的數組相比,它的容量能動態增長。在添加大量元素前,應用程序可以使用`ensureCapacity`操作來增加 ArrayList 實例的容量。這可以減少遞增式再分配的數量。它繼承于 **AbstractList**,實現了 **List**, **RandomAccess**, **Cloneable**, **java.io.Serializable** 這些接口。在我們學數據結構的時候就知道了線性表的順序存儲,插入刪除元素的時間復雜度為**O(n)**,求表長以及增加元素,取第 i 元素的時間復雜度為**O(1)**ArrayList 繼承了AbstractList,實現了List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。ArrayList 實現了**RandomAccess 接口**, RandomAccess 是一個標志接口,表明實現這個這個接口的 List 集合是支持**快速隨機訪問**的。在 ArrayList 中,我們即可以通過元素的序號快速獲取元素對象,這就是快速隨機訪問。ArrayList 實現了**Cloneable 接口**,即覆蓋了函數 clone(),**能被克隆**。ArrayList 實現**java.io.Serializable 接口**,這意味著ArrayList**支持序列化**,**能通過序列化去傳輸**。和 Vector 不同,**ArrayList 中的操作不是線程安全的**!所以,建議在單線程中才使用 ArrayList,而在多線程中可以選擇 Vector 或者 CopyOnWriteArrayList。 ### ArrayList核心源碼```java package java.util;import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator;public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {private static final long serialVersionUID = 8683452581122892189L;/*** 默認初始容量大小*/private static final int DEFAULT_CAPACITY = 10;/*** 空數組(用于空實例)。*/private static final Object[] EMPTY_ELEMENTDATA = {};//用于默認大小空實例的共享空數組實例。//我們把它從EMPTY_ELEMENTDATA數組中區分出來,以知道在添加第一個元素時容量需要增加多少。private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** 保存ArrayList數據的數組*/transient Object[] elementData; // non-private to simplify nested class access/*** ArrayList 所包含的元素個數*/private int size;/*** 帶初始容量參數的構造函數。(用戶自己指定容量)*/public ArrayList(int initialCapacity) {if (initialCapacity > 0) {//創建initialCapacity大小的數組this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {//創建空數組this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}/***默認構造函數,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 為0.初始化為10,也就是說初始其實是空數組 當添加第一個元素的時候數組容量才變成10*/public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/*** 構造一個包含指定集合的元素的列表,按照它們由集合的迭代器返回的順序。*/public ArrayList(Collection<? extends E> c) {//elementData = c.toArray();//如果指定集合元素個數不為0if ((size = elementData.length) != 0) {// c.toArray 可能返回的不是Object類型的數組所以加上下面的語句用于判斷,//這里用到了反射里面的getClass()方法if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// 用空數組代替this.elementData = EMPTY_ELEMENTDATA;}}/*** 修改這個ArrayList實例的容量是列表的當前大小。 應用程序可以使用此操作來最小化ArrayList實例的存儲。 */public void trimToSize() {modCount++;if (size < elementData.length) {elementData = (size == 0)? EMPTY_ELEMENTDATA: Arrays.copyOf(elementData, size);}} //下面是ArrayList的擴容機制 //ArrayList的擴容機制提高了性能,如果每次只擴充一個, //那么頻繁的插入會導致頻繁的拷貝,降低性能,而ArrayList的擴容機制避免了這種情況。/*** 如有必要,增加此ArrayList實例的容量,以確保它至少能容納元素的數量* @param minCapacity 所需的最小容量*/public void ensureCapacity(int minCapacity) {int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. It's already// supposed to be at default size.: DEFAULT_CAPACITY;if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}}//得到最小擴容量private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 獲取默認的容量和傳入參數的較大值minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);}//判斷是否需要擴容private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)//調用grow方法進行擴容,調用此方法代表已經開始擴容了grow(minCapacity);}/*** 要分配的最大數組大小*/private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** ArrayList擴容的核心方法。*/private void grow(int minCapacity) {// oldCapacity為舊容量,newCapacity為新容量int oldCapacity = elementData.length;//將oldCapacity 右移一位,其效果相當于oldCapacity /2,//我們知道位運算的速度遠遠快于整除運算,整句運算式的結果就是將新容量更新為舊容量的1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1);//然后檢查新容量是否大于最小需要容量,若還是小于最小需要容量,那么就把最小需要容量當作數組的新容量,if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//再檢查新容量是否超出了ArrayList所定義的最大容量,//若超出了,則調用hugeCapacity()來比較minCapacity和 MAX_ARRAY_SIZE,//如果minCapacity大于MAX_ARRAY_SIZE,則新容量則為Interger.MAX_VALUE,否則,新容量大小則為 MAX_ARRAY_SIZE。if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);}//比較minCapacity和 MAX_ARRAY_SIZEprivate static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}/***返回此列表中的元素數。 */public int size() {return size;}/*** 如果此列表不包含元素,則返回 true 。*/public boolean isEmpty() {//注意=和==的區別return size == 0;}/*** 如果此列表包含指定的元素,則返回true 。*/public boolean contains(Object o) {//indexOf()方法:返回此列表中指定元素的首次出現的索引,如果此列表不包含此元素,則為-1 return indexOf(o) >= 0;}/***返回此列表中指定元素的首次出現的索引,如果此列表不包含此元素,則為-1 */public int indexOf(Object o) {if (o == null) {for (int i = 0; i < size; i++)if (elementData[i]==null)return i;} else {for (int i = 0; i < size; i++)//equals()方法比較if (o.equals(elementData[i]))return i;}return -1;}/*** 返回此列表中指定元素的最后一次出現的索引,如果此列表不包含元素,則返回-1。.*/public int lastIndexOf(Object o) {if (o == null) {for (int i = size-1; i >= 0; i--)if (elementData[i]==null)return i;} else {for (int i = size-1; i >= 0; i--)if (o.equals(elementData[i]))return i;}return -1;}/*** 返回此ArrayList實例的淺拷貝。 (元素本身不被復制。) */public Object clone() {try {ArrayList<?> v = (ArrayList<?>) super.clone();//Arrays.copyOf功能是實現數組的復制,返回復制后的數組。參數是被復制的數組和復制的長度v.elementData = Arrays.copyOf(elementData, size);v.modCount = 0;return v;} catch (CloneNotSupportedException e) {// 這不應該發生,因為我們是可以克隆的throw new InternalError(e);}}/***以正確的順序(從第一個到最后一個元素)返回一個包含此列表中所有元素的數組。 *返回的數組將是“安全的”,因為該列表不保留對它的引用。 (換句話說,這個方法必須分配一個新的數組)。*因此,調用者可以自由地修改返回的數組。 此方法充當基于陣列和基于集合的API之間的橋梁。*/public Object[] toArray() {return Arrays.copyOf(elementData, size);}/*** 以正確的順序返回一個包含此列表中所有元素的數組(從第一個到最后一個元素); *返回的數組的運行時類型是指定數組的運行時類型。 如果列表適合指定的數組,則返回其中。 *否則,將為指定數組的運行時類型和此列表的大小分配一個新數組。 *如果列表適用于指定的數組,其余空間(即數組的列表數量多于此元素),則緊跟在集合結束后的數組中的元素設置為null 。*(這僅在調用者知道列表不包含任何空元素的情況下才能確定列表的長度。) */@SuppressWarnings("unchecked")public <T> T[] toArray(T[] a) {if (a.length < size)// 新建一個運行時類型的數組,但是ArrayList數組的內容return (T[]) Arrays.copyOf(elementData, size, a.getClass());//調用System提供的arraycopy()方法實現數組之間的復制System.arraycopy(elementData, 0, a, 0, size);if (a.length > size)a[size] = null;return a;}// Positional Access Operations@SuppressWarnings("unchecked")E elementData(int index) {return (E) elementData[index];}/*** 返回此列表中指定位置的元素。*/public E get(int index) {rangeCheck(index);return elementData(index);}/*** 用指定的元素替換此列表中指定位置的元素。 */public E set(int index, E element) {//對index進行界限檢查rangeCheck(index);E oldValue = elementData(index);elementData[index] = element;//返回原來在這個位置的元素return oldValue;}/*** 將指定的元素追加到此列表的末尾。 */public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!//這里看到ArrayList添加元素的實質就相當于為數組賦值elementData[size++] = e;return true;}/*** 在此列表中的指定位置插入指定的元素。 *先調用 rangeCheckForAdd 對index進行界限檢查;然后調用 ensureCapacityInternal 方法保證capacity足夠大;*再將從index開始之后的所有成員后移一個位置;將element插入index位置;最后size加1。*/public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1); // Increments modCount!!//arraycopy()這個實現數組之間復制的方法一定要看一下,下面就用到了arraycopy()方法實現數組自己復制自己System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++;}/*** 刪除該列表中指定位置的元素。 將任何后續元素移動到左側(從其索引中減去一個元素)。 */public E remove(int index) {rangeCheck(index);modCount++;E oldValue = elementData(index);int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--size] = null; // clear to let GC do its work//從列表中刪除的元素 return oldValue;}/*** 從列表中刪除指定元素的第一個出現(如果存在)。 如果列表不包含該元素,則它不會更改。*返回true,如果此列表包含指定的元素*/public boolean remove(Object o) {if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;}/** Private remove method that skips bounds checking and does not* return the value removed.*/private void fastRemove(int index) {modCount++;int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--size] = null; // clear to let GC do its work}/*** 從列表中刪除所有元素。 */public void clear() {modCount++;// 把數組中所有的元素的值設為nullfor (int i = 0; i < size; i++)elementData[i] = null;size = 0;}/*** 按指定集合的Iterator返回的順序將指定集合中的所有元素追加到此列表的末尾。*/public boolean addAll(Collection<? extends E> c) {Object[] a = c.toArray();int numNew = a.length;ensureCapacityInternal(size + numNew); // Increments modCountSystem.arraycopy(a, 0, elementData, size, numNew);size += numNew;return numNew != 0;}/*** 將指定集合中的所有元素插入到此列表中,從指定的位置開始。*/public boolean addAll(int index, Collection<? extends E> c) {rangeCheckForAdd(index);Object[] a = c.toArray();int numNew = a.length;ensureCapacityInternal(size + numNew); // Increments modCountint numMoved = size - index;if (numMoved > 0)System.arraycopy(elementData, index, elementData, index + numNew,numMoved);System.arraycopy(a, 0, elementData, index, numNew);size += numNew;return numNew != 0;}/*** 從此列表中刪除所有索引為fromIndex (含)和toIndex之間的元素。*將任何后續元素移動到左側(減少其索引)。*/protected void removeRange(int fromIndex, int toIndex) {modCount++;int numMoved = size - toIndex;System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);// clear to let GC do its workint newSize = size - (toIndex-fromIndex);for (int i = newSize; i < size; i++) {elementData[i] = null;}size = newSize;}/*** 檢查給定的索引是否在范圍內。*/private void rangeCheck(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}/*** add和addAll使用的rangeCheck的一個版本*/private void rangeCheckForAdd(int index) {if (index > size || index < 0)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}/*** 返回IndexOutOfBoundsException細節信息*/private String outOfBoundsMsg(int index) {return "Index: "+index+", Size: "+size;}/*** 從此列表中刪除指定集合中包含的所有元素。 */public boolean removeAll(Collection<?> c) {Objects.requireNonNull(c);//如果此列表被修改則返回truereturn batchRemove(c, false);}/*** 僅保留此列表中包含在指定集合中的元素。*換句話說,從此列表中刪除其中不包含在指定集合中的所有元素。 */public boolean retainAll(Collection<?> c) {Objects.requireNonNull(c);return batchRemove(c, true);}/*** 從列表中的指定位置開始,返回列表中的元素(按正確順序)的列表迭代器。*指定的索引表示初始調用將返回的第一個元素為next 。 初始調用previous將返回指定索引減1的元素。 *返回的列表迭代器是fail-fast 。 */public ListIterator<E> listIterator(int index) {if (index < 0 || index > size)throw new IndexOutOfBoundsException("Index: "+index);return new ListItr(index);}/***返回列表中的列表迭代器(按適當的順序)。 *返回的列表迭代器是fail-fast 。*/public ListIterator<E> listIterator() {return new ListItr(0);}/***以正確的順序返回該列表中的元素的迭代器。 *返回的迭代器是fail-fast 。 */public Iterator<E> iterator() {return new Itr();}``` ### <font face="楷體" id="1" id="5">ArrayList源碼分析</font> #### System.arraycopy()和Arrays.copyOf()方法通過上面源碼我們發現這兩個實現數組復制的方法被廣泛使用而且很多地方都特別巧妙。比如下面<font color="red">add(int index, E element)</font>方法就很巧妙的用到了<font color="red">arraycopy()方法</font>讓數組自己復制自己實現讓index開始之后的所有成員后移一個位置: ```java /*** 在此列表中的指定位置插入指定的元素。 *先調用 rangeCheckForAdd 對index進行界限檢查;然后調用 ensureCapacityInternal 方法保證capacity足夠大;*再將從index開始之后的所有成員后移一個位置;將element插入index位置;最后size加1。*/public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1); // Increments modCount!!//arraycopy()方法實現數組自己復制自己//elementData:源數組;index:源數組中的起始位置;elementData:目標數組;index + 1:目標數組中的起始位置; size - index:要復制的數組元素的數量;System.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element;size++;} ``` 又如toArray()方法中用到了copyOf()方法 ```java/***以正確的順序(從第一個到最后一個元素)返回一個包含此列表中所有元素的數組。 *返回的數組將是“安全的”,因為該列表不保留對它的引用。 (換句話說,這個方法必須分配一個新的數組)。*因此,調用者可以自由地修改返回的數組。 此方法充當基于陣列和基于集合的API之間的橋梁。*/public Object[] toArray() {//elementData:要復制的數組;size:要復制的長度return Arrays.copyOf(elementData, size);} ``` ##### 兩者聯系與區別 **聯系:** 看兩者源代碼可以發現`copyOf()`內部調用了`System.arraycopy()`方法 **區別:** 1. arraycopy()需要目標數組,將原數組拷貝到你自己定義的數組里,而且可以選擇拷貝的起點和長度以及放入新數組中的位置 2. copyOf()是系統自動在內部新建一個數組,并返回該數組。 #### ArrayList 核心擴容技術 ```java //下面是ArrayList的擴容機制 //ArrayList的擴容機制提高了性能,如果每次只擴充一個, //那么頻繁的插入會導致頻繁的拷貝,降低性能,而ArrayList的擴容機制避免了這種情況。/*** 如有必要,增加此ArrayList實例的容量,以確保它至少能容納元素的數量* @param minCapacity 所需的最小容量*/public void ensureCapacity(int minCapacity) {int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. It's already// supposed to be at default size.: DEFAULT_CAPACITY;if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}}//得到最小擴容量private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 獲取默認的容量和傳入參數的較大值minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);}//判斷是否需要擴容,上面兩個方法都要調用private void ensureExplicitCapacity(int minCapacity) {modCount++;// 如果說minCapacity也就是所需的最小容量大于保存ArrayList數據的數組的長度的話,就需要調用grow(minCapacity)方法擴容。//這個minCapacity到底為多少呢?舉個例子在添加元素(add)方法中這個minCapacity的大小就為現在數組的長度加1if (minCapacity - elementData.length > 0)//調用grow方法進行擴容,調用此方法代表已經開始擴容了grow(minCapacity);}``` ```java/*** ArrayList擴容的核心方法。*/private void grow(int minCapacity) {//elementData為保存ArrayList數據的數組///elementData.length求數組長度elementData.size是求數組中的元素個數// oldCapacity為舊容量,newCapacity為新容量int oldCapacity = elementData.length;//將oldCapacity 右移一位,其效果相當于oldCapacity /2,//我們知道位運算的速度遠遠快于整除運算,整句運算式的結果就是將新容量更新為舊容量的1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1);//然后檢查新容量是否大于最小需要容量,若還是小于最小需要容量,那么就把最小需要容量當作數組的新容量,if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//再檢查新容量是否超出了ArrayList所定義的最大容量,//若超出了,則調用hugeCapacity()來比較minCapacity和 MAX_ARRAY_SIZE,//如果minCapacity大于MAX_ARRAY_SIZE,則新容量則為Interger.MAX_VALUE,否則,新容量大小則為 MAX_ARRAY_SIZE。if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);}```擴容機制代碼已經做了詳細的解釋。另外值得注意的是大家很容易忽略的一個運算符:**移位運算符****簡介**:移位運算符就是在二進制的基礎上對數字進行平移。按照平移的方向和填充數字的規則分為三種:<font color="red"><<(左移)</font>、<font color="red">>>(帶符號右移)</font>和<font color="red">>>>(無符號右移)</font>。**作用**:**對于大數據的2進制運算,位移運算符比那些普通運算符的運算要快很多,因為程序僅僅移動一下而已,不去計算,這樣提高了效率,節省了資源**比如這里:int newCapacity = oldCapacity + (oldCapacity >> 1); 右移一位相當于除2,右移n位相當于除以 2 的 n 次方。這里 oldCapacity 明顯右移了1位所以相當于oldCapacity /2。**另外需要注意的是:**1. java 中的**length 屬性**是針對數組說的,比如說你聲明了一個數組,想知道這個數組的長度則用到了 length 這個屬性.2. java 中的**length()方法**是針對字 符串String說的,如果想看這個字符串的長度則用到 length()這個方法.3. .java 中的**size()方法**是針對泛型集合說的,如果想看這個泛型有多少個元素,就調用此方法來查看!#### 內部類 ```java(1)private class Itr implements Iterator<E> (2)private class ListItr extends Itr implements ListIterator<E> (3)private class SubList extends AbstractList<E> implements RandomAccess (4)static final class ArrayListSpliterator<E> implements Spliterator<E> ```ArrayList有四個內部類,其中的**Itr是實現了Iterator接口**,同時重寫了里面的**hasNext()**, **next()**, **remove()** 等方法;其中的**ListItr** 繼承 **Itr**,實現了**ListIterator接口**,同時重寫了**hasPrevious()**, **nextIndex()**, **previousIndex()**, **previous()**, **set(E e)**, **add(E e)** 等方法,所以這也可以看出了 **Iterator和ListIterator的區別:** ListIterator在Iterator的基礎上增加了添加對象,修改對象,逆向遍歷等方法,這些是Iterator不能實現的。 ### <font face="楷體" id="6"> ArrayList經典Demo</font>```java package list; import java.util.ArrayList; import java.util.Iterator;public class ArrayListDemo {public static void main(String[] srgs){ArrayList<Integer> arrayList = new ArrayList<Integer>();System.out.printf("Before add:arrayList.size() = %d\n",arrayList.size());arrayList.add(1);arrayList.add(3);arrayList.add(5);arrayList.add(7);arrayList.add(9);System.out.printf("After add:arrayList.size() = %d\n",arrayList.size());System.out.println("Printing elements of arrayList");// 三種遍歷方式打印元素// 第一種:通過迭代器遍歷System.out.print("通過迭代器遍歷:");Iterator<Integer> it = arrayList.iterator();while(it.hasNext()){System.out.print(it.next() + " ");}System.out.println();// 第二種:通過索引值遍歷System.out.print("通過索引值遍歷:");for(int i = 0; i < arrayList.size(); i++){System.out.print(arrayList.get(i) + " ");}System.out.println();// 第三種:for循環遍歷System.out.print("for循環遍歷:");for(Integer number : arrayList){System.out.print(number + " ");}// toArray用法// 第一種方式(最常用)Integer[] integer = arrayList.toArray(new Integer[0]);// 第二種方式(容易理解)Integer[] integer1 = new Integer[arrayList.size()];arrayList.toArray(integer1);// 拋出異常,java不支持向下轉型//Integer[] integer2 = new Integer[arrayList.size()];//integer2 = arrayList.toArray();System.out.println();// 在指定位置添加元素arrayList.add(2,2);// 刪除指定位置上的元素arrayList.remove(2); // 刪除指定元素arrayList.remove((Object)3);// 判斷arrayList是否包含5System.out.println("ArrayList contains 5 is: " + arrayList.contains(5));// 清空ArrayListarrayList.clear();// 判斷ArrayList是否為空System.out.println("ArrayList is empty: " + arrayList.isEmpty());} } ``` ## 一 先從 ArrayList 的構造函數說起**ArrayList有三種方式來初始化,構造方法源碼如下:**```java/*** 默認初始容量大小*/private static final int DEFAULT_CAPACITY = 10;private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/***默認構造函數,使用初始容量10構造一個空列表(無參數構造)*/public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/*** 帶初始容量參數的構造函數。(用戶自己指定容量)*/public ArrayList(int initialCapacity) {if (initialCapacity > 0) {//初始容量大于0//創建initialCapacity大小的數組this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {//初始容量等于0//創建空數組this.elementData = EMPTY_ELEMENTDATA;} else {//初始容量小于0,拋出異常throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}/***構造包含指定collection元素的列表,這些元素利用該集合的迭代器按順序返回*如果指定的集合為null,throws NullPointerException。 */public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// replace with empty array.this.elementData = EMPTY_ELEMENTDATA;}}```細心的同學一定會發現 :**以無參數構造方法創建 ArrayList 時,實際上初始化賦值的是一個空數組。當真正對數組進行添加元素操作時,才真正分配容量。即向數組中添加第一個元素時,數組容量擴為10。** 下面在我們分析 ArrayList 擴容時會講到這一點內容!## 二 一步一步分析 ArrayList 擴容機制這里以無參構造函數創建的 ArrayList 為例分析### 1. 先來看 `add` 方法```java/*** 將指定的元素追加到此列表的末尾。 */public boolean add(E e) {//添加元素之前,先調用ensureCapacityInternal方法ensureCapacityInternal(size + 1); // Increments modCount!!//這里看到ArrayList添加元素的實質就相當于為數組賦值elementData[size++] = e;return true;} ``` ### 2. 再來看看 `ensureCapacityInternal()` 方法可以看到 `add` 方法 首先調用了`ensureCapacityInternal(size + 1)````java//得到最小擴容量private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 獲取默認的容量和傳入參數的較大值minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);} ``` **當 要 add 進第1個元素時,minCapacity為1,在Math.max()方法比較后,minCapacity 為10。**### 3. `ensureExplicitCapacity()` 方法 如果調用 `ensureCapacityInternal()` 方法就一定會進過(執行)這個方法,下面我們來研究一下這個方法的源碼!```java//判斷是否需要擴容private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)//調用grow方法進行擴容,調用此方法代表已經開始擴容了grow(minCapacity);}```我們來仔細分析一下:- 當我們要 add 進第1個元素到 ArrayList 時,elementData.length 為0 (因為還是一個空的 list),因為執行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此時為10。此時,`minCapacity - elementData.length > 0 `成立,所以會進入 `grow(minCapacity)` 方法。 - 當add第2個元素時,minCapacity 為2,此時e lementData.length(容量)在添加第一個元素后擴容成 10 了。此時,`minCapacity - elementData.length > 0 ` 不成立,所以不會進入 (執行)`grow(minCapacity)` 方法。 - 添加第3、4···到第10個元素時,依然不會執行grow方法,數組容量都為10。直到添加第11個元素,minCapacity(為11)比elementData.length(為10)要大。進入grow方法進行擴容。### 4. `grow()` 方法 ```java/*** 要分配的最大數組大小*/private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** ArrayList擴容的核心方法。*/private void grow(int minCapacity) {// oldCapacity為舊容量,newCapacity為新容量int oldCapacity = elementData.length;//將oldCapacity 右移一位,其效果相當于oldCapacity /2,//我們知道位運算的速度遠遠快于整除運算,整句運算式的結果就是將新容量更新為舊容量的1.5倍,int newCapacity = oldCapacity + (oldCapacity >> 1);//然后檢查新容量是否大于最小需要容量,若還是小于最小需要容量,那么就把最小需要容量當作數組的新容量,if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 如果新容量大于 MAX_ARRAY_SIZE,進入(執行) `hugeCapacity()` 方法來比較 minCapacity 和 MAX_ARRAY_SIZE,//如果minCapacity大于最大容量,則新容量則為`Integer.MAX_VALUE`,否則,新容量大小則為 MAX_ARRAY_SIZE 即為 `Integer.MAX_VALUE - 8`。if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);} ```**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次擴容之后容量都會變為原來的 1.5 倍!(JDK1.6版本以后)** JDk1.6版本時,擴容之后容量為 1.5 倍+1!詳情請參考源碼> ">>"(移位運算符):>>1 右移一位相當于除2,右移n位相當于除以 2 的 n 次方。這里 oldCapacity 明顯右移了1位所以相當于oldCapacity /2。對于大數據的2進制運算,位移運算符比那些普通運算符的運算要快很多,因為程序僅僅移動一下而已,不去計算,這樣提高了效率,節省了資源 **我們再來通過例子探究一下`grow()` 方法 :**- 當add第1個元素時,oldCapacity 為0,經比較后第一個if判斷成立,newCapacity = minCapacity(為10)。但是第二個if判斷不會成立,即newCapacity 不比 MAX_ARRAY_SIZE大,則不會進入 `hugeCapacity` 方法。數組容量為10,add方法中 return true,size增為1。 - 當add第11個元素進入grow方法時,newCapacity為15,比minCapacity(為11)大,第一個if判斷不成立。新容量沒有大于數組最大size,不會進入hugeCapacity方法。數組容量擴為15,add方法中return true,size增為11。 - 以此類推······**這里補充一點比較重要,但是容易被忽視掉的知識點:**- java 中的 `length `屬性是針對數組說的,比如說你聲明了一個數組,想知道這個數組的長度則用到了 length 這個屬性. - java 中的 `length()` 方法是針對字符串說的,如果想看這個字符串的長度則用到 `length()` 這個方法. - java 中的 `size()` 方法是針對泛型集合說的,如果想看這個泛型有多少個元素,就調用此方法來查看!### 5. `hugeCapacity()` 方法。從上面 `grow()` 方法源碼我們知道: 如果新容量大于 MAX_ARRAY_SIZE,進入(執行) `hugeCapacity()` 方法來比較 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,則新容量則為`Integer.MAX_VALUE`,否則,新容量大小則為 MAX_ARRAY_SIZE 即為 `Integer.MAX_VALUE - 8`。 ```javaprivate static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();//對minCapacity和MAX_ARRAY_SIZE進行比較//若minCapacity大,將Integer.MAX_VALUE作為新數組的大小//若MAX_ARRAY_SIZE大,將MAX_ARRAY_SIZE作為新數組的大小//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;} ```## 三 `System.arraycopy()` 和 `Arrays.copyOf()`方法閱讀源碼的話,我們就會發現 ArrayList 中大量調用了這兩個方法。比如:我們上面講的擴容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了該方法!### 3.1 `System.arraycopy()` 方法```java/*** 在此列表中的指定位置插入指定的元素。 *先調用 rangeCheckForAdd 對index進行界限檢查;然后調用 ensureCapacityInternal 方法保證capacity足夠大;*再將從index開始之后的所有成員后移一個位置;將element插入index位置;最后size加1。*/public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1); // Increments modCount!!//arraycopy()方法實現數組自己復制自己//elementData:源數組;index:源數組中的起始位置;elementData:目標數組;index + 1:目標數組中的起始位置; size - index:要復制的數組元素的數量;System.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element;size++;} ```我們寫一個簡單的方法測試以下:```java public class ArraycopyTest {public static void main(String[] args) {// TODO Auto-generated method stubint[] a = new int[10];a[0] = 0;a[1] = 1;a[2] = 2;a[3] = 3;System.arraycopy(a, 2, a, 3, 3);a[2]=99;for (int i = 0; i < a.length; i++) {System.out.println(a[i]);}}} ```結果:``` 0 1 99 2 3 0 0 0 0 0 ```### 3.2 `Arrays.copyOf()`方法```java/**以正確的順序返回一個包含此列表中所有元素的數組(從第一個到最后一個元素); 返回的數組的運行時類型是指定數組的運行時類型。 */public Object[] toArray() {//elementData:要復制的數組;size:要復制的長度return Arrays.copyOf(elementData, size);} ```個人覺得使用 `Arrays.copyOf()`方法主要是為了給原有數組擴容,測試代碼如下:```java public class ArrayscopyOfTest {public static void main(String[] args) {int[] a = new int[3];a[0] = 0;a[1] = 1;a[2] = 2;int[] b = Arrays.copyOf(a, 10);System.out.println("b.length"+b.length);} } ```結果:``` 10 ```### 3.3 兩者聯系和區別**聯系:** 看兩者源代碼可以發現 copyOf() 內部實際調用了 `System.arraycopy()` 方法 **區別:**`arraycopy()` 需要目標數組,將原數組拷貝到你自己定義的數組里或者原數組,而且可以選擇拷貝的起點和長度以及放入新數組中的位置 `copyOf()` 是系統自動在內部新建一個數組,并返回該數組。## 四 `ensureCapacity`方法ArrayList 源碼中有一個 `ensureCapacity` 方法不知道大家注意到沒有,這個方法 ArrayList 內部沒有被調用過,所以很顯然是提供給用戶調用的,那么這個方法有什么作用呢?```java/**如有必要,增加此 ArrayList 實例的容量,以確保它至少可以容納由minimum capacity參數指定的元素數。** @param minCapacity 所需的最小容量*/public void ensureCapacity(int minCapacity) {int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)// any size if not default element table? 0// larger than default for default empty table. It's already// supposed to be at default size.: DEFAULT_CAPACITY;if (minCapacity > minExpand) {ensureExplicitCapacity(minCapacity);}}```**最好在 add 大量元素之前用 `ensureCapacity` 方法,以減少增量重新分配的次數**我們通過下面的代碼實際測試以下這個方法的效果:```java public class EnsureCapacityTest {public static void main(String[] args) {ArrayList<Object> list = new ArrayList<Object>();final int N = 10000000;long startTime = System.currentTimeMillis();for (int i = 0; i < N; i++) {list.add(i);}long endTime = System.currentTimeMillis();System.out.println("使用ensureCapacity方法前:"+(endTime - startTime));list = new ArrayList<Object>();long startTime1 = System.currentTimeMillis();list.ensureCapacity(N);for (int i = 0; i < N; i++) {list.add(i);}long endTime1 = System.currentTimeMillis();System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));} } ```運行結果:``` 使用ensureCapacity方法前:4637 使用ensureCapacity方法后:241 ```通過運行結果,我們可以很明顯的看出向 ArrayList 添加大量元素之前最好先使用`ensureCapacity` 方法,以減少增量重新分配的次數 <!-- MarkdownTOC -->- [HashMap 簡介](#hashmap-簡介) - [底層數據結構分析](#底層數據結構分析)- [JDK1.8之前](#jdk18之前)- [JDK1.8之后](#jdk18之后) - [HashMap源碼分析](#hashmap源碼分析)- [構造方法](#構造方法)- [put方法](#put方法)- [get方法](#get方法)- [resize方法](#resize方法) - [HashMap常用方法測試](#hashmap常用方法測試)<!-- /MarkdownTOC -->> 感謝 [changfubai](https://github.com/changfubai) 對本文的改進做出的貢獻!## HashMap 簡介 HashMap 主要用來存放鍵值對,它基于哈希表的Map接口實現</font>,是常用的Java集合之一。 JDK1.8 之前 HashMap 由 數組+鏈表 組成的,數組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的(“拉鏈法”解決沖突).JDK1.8 以后在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(默認為 8)時,將鏈表轉化為紅黑樹(將鏈表轉換成紅黑樹前會判斷,如果當前數組的長度小于 64,那么會選擇先進行數組擴容,而不是轉換為紅黑樹),以減少搜索時間,具體可以參考 `treeifyBin`方法。## 底層數據結構分析 ### JDK1.8之前 JDK1.8 之前 HashMap 底層是 **數組和鏈表** 結合在一起使用也就是 **鏈表散列**。**HashMap 通過 key 的 hashCode 經過擾動函數處理過后得到 hash 值,然后通過 `(n - 1) & hash` 判斷當前元素存放的位置(這里的 n 指的是數組的長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鏈法解決沖突。****所謂擾動函數指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函數是為了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函數之后可以減少碰撞。****JDK 1.8 HashMap 的 hash 方法源碼:**JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加簡化,但是原理不變。```javastatic final int hash(Object key) {int h;// key.hashCode():返回散列值也就是hashcode// ^ :按位異或// >>>:無符號右移,忽略符號位,空位都以0補齊return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}``` 對比一下 JDK1.7的 HashMap 的 hash 方法源碼.```java static int hash(int h) {// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4); } ```相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能會稍差一點點,因為畢竟擾動了 4 次。所謂 **“拉鏈法”** 就是:將鏈表和數組相結合。也就是說創建一個鏈表數組,數組中每一格就是一個鏈表。若遇到哈希沖突,則將沖突的值加到鏈表中即可。### JDK1.8之后 相比于之前的版本,jdk1.8在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(默認為8)時,將鏈表轉化為紅黑樹,以減少搜索時間。**類的屬性:** ```java public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {// 序列號private static final long serialVersionUID = 362498820763181265L; // 默認的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的填充因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 當桶(bucket)上的結點數大于這個值時會轉成紅黑樹static final int TREEIFY_THRESHOLD = 8; // 當桶(bucket)上的結點數小于這個值時樹轉鏈表static final int UNTREEIFY_THRESHOLD = 6;// 桶中結構轉化為紅黑樹對應的table的最小大小static final int MIN_TREEIFY_CAPACITY = 64;// 存儲元素的數組,總是2的冪次倍transient Node<k,v>[] table; // 存放具體元素的集transient Set<map.entry<k,v>> entrySet;// 存放元素的個數,注意這個不等于數組的長度。transient int size;// 每次擴容和更改map結構的計數器transient int modCount; // 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容int threshold;// 加載因子final float loadFactor; } ``` - **loadFactor加載因子**loadFactor加載因子是控制數組存放數據的疏密程度,loadFactor越趨近于1,那么 數組中存放的數據(entry)也就越多,也就越密,也就是會讓鏈表的長度增加,loadFactor越小,也就是趨近于0,數組中存放的數據(entry)也就越少,也就越稀疏。**loadFactor太大導致查找元素效率低,太小導致數組的利用率低,存放的數據會很分散。loadFactor的默認值為0.75f是官方給出的一個比較好的臨界值**。 給定的默認容量為 16,負載因子為 0.75。Map 在使用過程中不斷的往里面存放數據,當數量達到了 16 * 0.75 = 12 就需要將當前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、復制數據等操作,所以非常消耗性能。- **threshold****threshold = capacity * loadFactor**,**當Size>=threshold**的時候,那么就要考慮對數組的擴增了,也就是說,這個的意思就是 **衡量數組是否需要擴增的一個標準**。**Node節點類源碼:**```java // 繼承自 Map.Entry<K,V> static class Node<K,V> implements Map.Entry<K,V> {final int hash;// 哈希值,存放元素到hashmap中時用來與其他元素hash值比較final K key;//鍵V value;//值// 指向下一個節點Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey() { return key; }public final V getValue() { return value; }public final String toString() { return key + "=" + value; }// 重寫hashCode()方法public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}// 重寫 equals() 方法public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;} } ``` **樹節點類源碼:** ```java static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // 父TreeNode<K,V> left; // 左TreeNode<K,V> right; // 右TreeNode<K,V> prev; // needed to unlink next upon deletionboolean red; // 判斷顏色TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}// 返回根節點final TreeNode<K,V> root() {for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;} ``` ## HashMap源碼分析 ### 構造方法HashMap 中有四個構造方法,它們分別如下:```java// 默認構造函數。public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}// 包含另一個“Map”的構造函數public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);//下面會分析到這個方法}// 指定“容量大小”的構造函數public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}// 指定“容量大小”和“加載因子”的構造函數public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " + loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);} ```**putMapEntries方法:**```java final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {// 判斷table是否已經初始化if (table == null) { // pre-size// 未初始化,s為m的實際元素個數float ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);// 計算得到的t大于閾值,則初始化閾值if (t > threshold)threshold = tableSizeFor(t);}// 已初始化,并且m元素個數大于閾值,進行擴容處理else if (s > threshold)resize();// 將m中的所有元素添加至HashMap中for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}} } ``` ### put方法 HashMap只提供了put用于添加元素,putVal方法只是給put方法調用的一個方法,并沒有提供給用戶使用。**對putVal方法添加元素的分析如下:**- ①如果定位到的數組位置沒有元素 就直接插入。 - ②如果定位到的數組位置有元素就和要插入的key比較,如果key相同就直接覆蓋,如果key不相同,就判斷p是否是一個樹節點,如果是就調用`e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)`將元素添加進入。如果不是就遍歷鏈表插入(插入的是鏈表尾部)。```java public V put(K key, V value) {return putVal(hash(key), key, value, false, true); }final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// table未初始化或者長度為0,進行擴容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// (n - 1) & hash 確定元素存放在哪個桶中,桶為空,新生成結點放入桶中(此時,這個結點是放在數組中)if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);// 桶中已經存在元素else {Node<K,V> e; K k;// 比較桶中第一個元素(數組中的結點)的hash值相等,key相等if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 將第一個元素賦值給e,用e來記錄e = p;// hash值不相等,即key不相等;為紅黑樹結點else if (p instanceof TreeNode)// 放入樹中e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 為鏈表結點else {// 在鏈表最末插入結點for (int binCount = 0; ; ++binCount) {// 到達鏈表的尾部if ((e = p.next) == null) {// 在尾部插入新結點p.next = newNode(hash, key, value, null);// 結點數量達到閾值,轉化為紅黑樹if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);// 跳出循環break;}// 判斷鏈表中結點的key值與插入的元素的key值是否相等if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 相等,跳出循環break;// 用于遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表p = e;}}// 表示在桶中找到key值、hash值與插入元素相等的結點if (e != null) { // 記錄e的valueV oldValue = e.value;// onlyIfAbsent為false或者舊值為nullif (!onlyIfAbsent || oldValue == null)//用新值替換舊值e.value = value;// 訪問后回調afterNodeAccess(e);// 返回舊值return oldValue;}}// 結構性修改++modCount;// 實際大小大于閾值則擴容if (++size > threshold)resize();// 插入后回調afterNodeInsertion(evict);return null; } ```**我們再來對比一下 JDK1.7 put方法的代碼****對于put方法的分析如下:**- ①如果定位到的數組位置沒有元素 就直接插入。 - ②如果定位到的數組位置有元素,遍歷以這個元素為頭結點的鏈表,依次和插入的key比較,如果key相同就直接覆蓋,不同就采用頭插法插入元素。```java public V put(K key, V value)if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null)return putForNullKey(value);int hash = hash(key);int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 先遍歷Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue; }}modCount++;addEntry(hash, key, value, i); // 再插入return null; } ```### get方法 ```java public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value; }final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 數組元素相等if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 桶中不止一個節點if ((e = first.next) != null) {// 在樹中getif (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 在鏈表中getdo {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null; } ``` ### resize方法 進行擴容,會伴隨著一次重新hash分配,并且會遍歷hash表中所有的元素,是非常耗時的。在編寫程序中,要盡量避免resize。 ```java final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {// 超過最大值就不再擴充了,就只好隨你碰撞去吧if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 沒超過最大值,就擴充為原來的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else { // signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 計算新的resize上限if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {// 把每個bucket都移動到新的buckets中for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 原索引if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// 原索引+oldCapelse {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 原索引放到bucket里if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 原索引+oldCap放到bucket里if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab; } ``` ## HashMap常用方法測試 ```java package map;import java.util.Collection; import java.util.HashMap; import java.util.Set;public class HashMapDemo {public static void main(String[] args) {HashMap<String, String> map = new HashMap<String, String>();// 鍵不能重復,值可以重復map.put("san", "張三");map.put("si", "李四");map.put("wu", "王五");map.put("wang", "老王");map.put("wang", "老王2");// 老王被覆蓋map.put("lao", "老王");System.out.println("-------直接輸出hashmap:-------");System.out.println(map);/*** 遍歷HashMap*/// 1.獲取Map中的所有鍵System.out.println("-------foreach獲取Map中所有的鍵:------");Set<String> keys = map.keySet();for (String key : keys) {System.out.print(key+" ");}System.out.println();//換行// 2.獲取Map中所有值System.out.println("-------foreach獲取Map中所有的值:------");Collection<String> values = map.values();for (String value : values) {System.out.print(value+" ");}System.out.println();//換行// 3.得到key的值的同時得到key所對應的值System.out.println("-------得到key的值的同時得到key所對應的值:-------");Set<String> keys2 = map.keySet();for (String key : keys2) {System.out.print(key + ":" + map.get(key)+" ");}/*** 另外一種不常用的遍歷方式*/// 當我調用put(key,value)方法的時候,首先會把key和value封裝到// Entry這個靜態內部類對象中,把Entry對象再添加到數組中,所以我們想獲取// map中的所有鍵值對,我們只要獲取數組中的所有Entry對象,接下來// 調用Entry對象中的getKey()和getValue()方法就能獲取鍵值對了Set<java.util.Map.Entry<String, String>> entrys = map.entrySet();for (java.util.Map.Entry<String, String> entry : entrys) {System.out.println(entry.getKey() + "--" + entry.getValue());}/*** HashMap其他常用方法*/System.out.println("after map.size():"+map.size());System.out.println("after map.isEmpty():"+map.isEmpty());System.out.println(map.remove("san"));System.out.println("after map.remove():"+map);System.out.println("after map.get(si):"+map.get("si"));System.out.println("after map.containsKey(si):"+map.containsKey("si"));System.out.println("after containsValue(李四):"+map.containsValue("李四"));System.out.println(map.replace("si", "李四2"));System.out.println("after map.replace(si, 李四2):"+map);}}``` <!-- MarkdownTOC -->- [簡介](#簡介) - [內部結構分析](#內部結構分析) - [LinkedList源碼分析](#linkedlist源碼分析)- [構造方法](#構造方法)- [添加(add)方法](#add方法)- [根據位置取數據的方法](#根據位置取數據的方法)- [根據對象得到索引的方法](#根據對象得到索引的方法)- [檢查鏈表是否包含某對象的方法:](#檢查鏈表是否包含某對象的方法:)- [刪除(remove/pop)方法](#刪除方法) - [LinkedList類常用方法測試:](#linkedlist類常用方法測試)<!-- /MarkdownTOC -->## <font face="楷體" id="1">簡介</font> <font color="red">LinkedList</font>是一個實現了<font color="red">List接口</font>和<font color="red">Deque接口</font>的<font color="red">雙端鏈表</font>。 LinkedList底層的鏈表結構使它<font color="red">支持高效的插入和刪除操作</font>,另外它實現了Deque接口,使得LinkedList類也具有隊列的特性; LinkedList<font color="red">不是線程安全的</font>,如果想使LinkedList變成線程安全的,可以調用靜態類<font color="red">Collections類</font>中的<font color="red">synchronizedList</font>方法: ```java List list=Collections.synchronizedList(new LinkedList(...)); ``` ## <font face="楷體" id="2">內部結構分析</font> **如下圖所示:**  看完了圖之后,我們再看LinkedList類中的一個<font color="red">**內部私有類Node**</font>就很好理解了: ```java private static class Node<E> {E item;//節點值Node<E> next;//后繼節點Node<E> prev;//前驅節點Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}} ``` 這個類就代表雙端鏈表的節點Node。這個類有三個屬性,分別是前驅節點,本節點的值,后繼結點。## <font face="楷體" id="3">LinkedList源碼分析</font> ### <font face="楷體" id="3.1">構造方法</font> **空構造方法:** ```javapublic LinkedList() {} ``` **用已有的集合創建鏈表的構造方法:** ```javapublic LinkedList(Collection<? extends E> c) {this();addAll(c);} ``` ### <font face="楷體" id="3.2">add方法</font> **add(E e)** 方法:將元素添加到鏈表尾部 ```java public boolean add(E e) {linkLast(e);//這里就只調用了這一個方法return true;} ``````java/*** 鏈接使e作為最后一個元素。*/void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;//新建節點if (l == null)first = newNode;elsel.next = newNode;//指向后繼元素也就是指向下一個元素size++;modCount++;} ``` **add(int index,E e)**:在指定位置添加元素 ```java public void add(int index, E element) {checkPositionIndex(index); //檢查索引是否處于[0-size]之間if (index == size)//添加在鏈表尾部linkLast(element);else//添加在鏈表中間linkBefore(element, node(index));} ``` <font color="red">linkBefore方法</font>需要給定兩個參數,一個<font color="red">插入節點的值</font>,一個<font color="red">指定的node</font>,所以我們又調用了<font color="red">Node(index)去找到index對應的node</font>**addAll(Collection c ):將集合插入到鏈表尾部**```java public boolean addAll(Collection<? extends E> c) {return addAll(size, c);} ``` **addAll(int index, Collection c):** 將集合從指定位置開始插入 ```java public boolean addAll(int index, Collection<? extends E> c) {//1:檢查index范圍是否在size之內checkPositionIndex(index);//2:toArray()方法把集合的數據存到對象數組中Object[] a = c.toArray();int numNew = a.length;if (numNew == 0)return false;//3:得到插入位置的前驅節點和后繼節點Node<E> pred, succ;//如果插入位置為尾部,前驅節點為last,后繼節點為nullif (index == size) {succ = null;pred = last;}//否則,調用node()方法得到后繼節點,再得到前驅節點else {succ = node(index);pred = succ.prev;}// 4:遍歷數據將數據插入for (Object o : a) {@SuppressWarnings("unchecked") E e = (E) o;//創建新節點Node<E> newNode = new Node<>(pred, e, null);//如果插入位置在鏈表頭部if (pred == null)first = newNode;elsepred.next = newNode;pred = newNode;}//如果插入位置在尾部,重置last節點if (succ == null) {last = pred;}//否則,將插入的鏈表與先前鏈表連接起來else {pred.next = succ;succ.prev = pred;}size += numNew;modCount++;return true;} ``` 上面可以看出addAll方法通常包括下面四個步驟: 1. 檢查index范圍是否在size之內 2. toArray()方法把集合的數據存到對象數組中 3. 得到插入位置的前驅和后繼節點 4. 遍歷數據,將數據插入到指定位置**addFirst(E e):** 將元素添加到鏈表頭部 ```javapublic void addFirst(E e) {linkFirst(e);} ``` ```java private void linkFirst(E e) {final Node<E> f = first;final Node<E> newNode = new Node<>(null, e, f);//新建節點,以頭節點為后繼節點first = newNode;//如果鏈表為空,last節點也指向該節點if (f == null)last = newNode;//否則,將頭節點的前驅指針指向新節點,也就是指向前一個元素elsef.prev = newNode;size++;modCount++;} ``` **addLast(E e):** 將元素添加到鏈表尾部,與 **add(E e)** 方法一樣 ```java public void addLast(E e) {linkLast(e);} ``` ### <font face="楷體" id="3.3">根據位置取數據的方法</font> **get(int index):** 根據指定索引返回數據 ```java public E get(int index) {//檢查index范圍是否在size之內checkElementIndex(index);//調用Node(index)去找到index對應的node然后返回它的值return node(index).item;} ``` **獲取頭節點(index=0)數據方法:** ```java public E getFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return f.item;} public E element() {return getFirst();} public E peek() {final Node<E> f = first;return (f == null) ? null : f.item;}public E peekFirst() {final Node<E> f = first;return (f == null) ? null : f.item;} ``` **區別:** getFirst(),element(),peek(),peekFirst() 這四個獲取頭結點方法的區別在于對鏈表為空時的處理,是拋出異常還是返回null,其中**getFirst()** 和**element()** 方法將會在鏈表為空時,拋出異常element()方法的內部就是使用getFirst()實現的。它們會在鏈表為空時,拋出NoSuchElementException **獲取尾節點(index=-1)數據方法:** ```javapublic E getLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return l.item;}public E peekLast() {final Node<E> l = last;return (l == null) ? null : l.item;} ``` **兩者區別:** **getLast()** 方法在鏈表為空時,會拋出**NoSuchElementException**,而**peekLast()** 則不會,只是會返回 **null**。 ### <font face="楷體" id="3.4">根據對象得到索引的方法</font> **int indexOf(Object o):** 從頭遍歷找 ```java public int indexOf(Object o) {int index = 0;if (o == null) {//從頭遍歷for (Node<E> x = first; x != null; x = x.next) {if (x.item == null)return index;index++;}} else {//從頭遍歷for (Node<E> x = first; x != null; x = x.next) {if (o.equals(x.item))return index;index++;}}return -1;} ``` **int lastIndexOf(Object o):** 從尾遍歷找 ```java public int lastIndexOf(Object o) {int index = size;if (o == null) {//從尾遍歷for (Node<E> x = last; x != null; x = x.prev) {index--;if (x.item == null)return index;}} else {//從尾遍歷for (Node<E> x = last; x != null; x = x.prev) {index--;if (o.equals(x.item))return index;}}return -1;} ``` ### <font face="楷體" id="3.5">檢查鏈表是否包含某對象的方法:</font> **contains(Object o):** 檢查對象o是否存在于鏈表中 ```javapublic boolean contains(Object o) {return indexOf(o) != -1;} ``` ### <font face="楷體" id="3.6">刪除方法</font> **remove()** ,**removeFirst(),pop():** 刪除頭節點 ``` public E pop() {return removeFirst();} public E remove() {return removeFirst();} public E removeFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return unlinkFirst(f);} ``` **removeLast(),pollLast():** 刪除尾節點 ```java public E removeLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return unlinkLast(l);} public E pollLast() {final Node<E> l = last;return (l == null) ? null : unlinkLast(l);} ``` **區別:** removeLast()在鏈表為空時將拋出NoSuchElementException,而pollLast()方法返回null。**remove(Object o):** 刪除指定元素 ```java public boolean remove(Object o) {//如果刪除對象為nullif (o == null) {//從頭開始遍歷for (Node<E> x = first; x != null; x = x.next) {//找到元素if (x.item == null) {//從鏈表中移除找到的元素unlink(x);return true;}}} else {//從頭開始遍歷for (Node<E> x = first; x != null; x = x.next) {//找到元素if (o.equals(x.item)) {//從鏈表中移除找到的元素unlink(x);return true;}}}return false;} ``` 當刪除指定對象時,只需調用remove(Object o)即可,不過該方法一次只會刪除一個匹配的對象,如果刪除了匹配對象,返回true,否則false。unlink(Node<E> x) 方法: ```java E unlink(Node<E> x) {// assert x != null;final E element = x.item;final Node<E> next = x.next;//得到后繼節點final Node<E> prev = x.prev;//得到前驅節點//刪除前驅指針if (prev == null) {first = next;//如果刪除的節點是頭節點,令頭節點指向該節點的后繼節點} else {prev.next = next;//將前驅節點的后繼節點指向后繼節點x.prev = null;}//刪除后繼指針if (next == null) {last = prev;//如果刪除的節點是尾節點,令尾節點指向該節點的前驅節點} else {next.prev = prev;x.next = null;}x.item = null;size--;modCount++;return element;} ``` **remove(int index)**:刪除指定位置的元素 ```java public E remove(int index) {//檢查index范圍checkElementIndex(index);//將節點刪除return unlink(node(index));} ``` ## <font face="楷體" id="4">LinkedList類常用方法測試</font>```java package list;import java.util.Iterator; import java.util.LinkedList;public class LinkedListDemo {public static void main(String[] srgs) {//創建存放int類型的linkedListLinkedList<Integer> linkedList = new LinkedList<>();/************************** linkedList的基本操作 ************************/linkedList.addFirst(0); // 添加元素到列表開頭linkedList.add(1); // 在列表結尾添加元素linkedList.add(2, 2); // 在指定位置添加元素linkedList.addLast(3); // 添加元素到列表結尾System.out.println("LinkedList(直接輸出的): " + linkedList);System.out.println("getFirst()獲得第一個元素: " + linkedList.getFirst()); // 返回此列表的第一個元素System.out.println("getLast()獲得第最后一個元素: " + linkedList.getLast()); // 返回此列表的最后一個元素System.out.println("removeFirst()刪除第一個元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一個元素System.out.println("removeLast()刪除最后一個元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一個元素System.out.println("After remove:" + linkedList);System.out.println("contains()方法判斷列表是否包含1這個元素:" + linkedList.contains(1)); // 判斷此列表包含指定元素,如果是,則返回trueSystem.out.println("該linkedList的大小 : " + linkedList.size()); // 返回此列表的元素個數/************************** 位置訪問操作 ************************/System.out.println("-----------------------------------------");linkedList.set(1, 3); // 將此列表中指定位置的元素替換為指定的元素System.out.println("After set(1, 3):" + linkedList);System.out.println("get(1)獲得指定位置(這里為1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置處的元素/************************** Search操作 ************************/System.out.println("-----------------------------------------");linkedList.add(3);System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出現的指定元素的索引System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出現的指定元素的索引/************************** Queue操作 ************************/System.out.println("-----------------------------------------");System.out.println("peek(): " + linkedList.peek()); // 獲取但不移除此列表的頭System.out.println("element(): " + linkedList.element()); // 獲取但不移除此列表的頭linkedList.poll(); // 獲取并移除此列表的頭System.out.println("After poll():" + linkedList);linkedList.remove();System.out.println("After remove():" + linkedList); // 獲取并移除此列表的頭linkedList.offer(4);System.out.println("After offer(4):" + linkedList); // 將指定元素添加到此列表的末尾/************************** Deque操作 ************************/System.out.println("-----------------------------------------");linkedList.offerFirst(2); // 在此列表的開頭插入指定的元素System.out.println("After offerFirst(2):" + linkedList);linkedList.offerLast(5); // 在此列表末尾插入指定的元素System.out.println("After offerLast(5):" + linkedList);System.out.println("peekFirst(): " + linkedList.peekFirst()); // 獲取但不移除此列表的第一個元素System.out.println("peekLast(): " + linkedList.peekLast()); // 獲取但不移除此列表的第一個元素linkedList.pollFirst(); // 獲取并移除此列表的第一個元素System.out.println("After pollFirst():" + linkedList);linkedList.pollLast(); // 獲取并移除此列表的最后一個元素System.out.println("After pollLast():" + linkedList);linkedList.push(2); // 將元素推入此列表所表示的堆棧(插入到列表的頭)System.out.println("After push(2):" + linkedList);linkedList.pop(); // 從此列表所表示的堆棧處彈出一個元素(獲取并移除列表第一個元素)System.out.println("After pop():" + linkedList);linkedList.add(3);linkedList.removeFirstOccurrence(3); // 從此列表中移除第一次出現的指定元素(從頭部到尾部遍歷列表)System.out.println("After removeFirstOccurrence(3):" + linkedList);linkedList.removeLastOccurrence(3); // 從此列表中移除最后一次出現的指定元素(從尾部到頭部遍歷列表)System.out.println("After removeFirstOccurrence(3):" + linkedList);/************************** 遍歷操作 ************************/System.out.println("-----------------------------------------");linkedList.clear();for (int i = 0; i < 100000; i++) {linkedList.add(i);}// 迭代器遍歷long start = System.currentTimeMillis();Iterator<Integer> iterator = linkedList.iterator();while (iterator.hasNext()) {iterator.next();}long end = System.currentTimeMillis();System.out.println("Iterator:" + (end - start) + " ms");// 順序遍歷(隨機遍歷)start = System.currentTimeMillis();for (int i = 0; i < linkedList.size(); i++) {linkedList.get(i);}end = System.currentTimeMillis();System.out.println("for:" + (end - start) + " ms");// 另一種for循環遍歷start = System.currentTimeMillis();for (Integer i : linkedList);end = System.currentTimeMillis();System.out.println("for2:" + (end - start) + " ms");// 通過pollFirst()或pollLast()來遍歷LinkedListLinkedList<Integer> temp1 = new LinkedList<>();temp1.addAll(linkedList);start = System.currentTimeMillis();while (temp1.size() != 0) {temp1.pollFirst();}end = System.currentTimeMillis();System.out.println("pollFirst()或pollLast():" + (end - start) + " ms");// 通過removeFirst()或removeLast()來遍歷LinkedListLinkedList<Integer> temp2 = new LinkedList<>();temp2.addAll(linkedList);start = System.currentTimeMillis();while (temp2.size() != 0) {temp2.removeFirst();}end = System.currentTimeMillis();System.out.println("removeFirst()或removeLast():" + (end - start) + " ms");} } ``` > 原文地址: https://juejin.im/post/5c94a123f265da610916081f。## JVM 配置常用參數 1. 堆參數; 2. 回收器參數; 3. 項目中常用配置; 4. 常用組合;### 堆參數### 回收器參數如上表所示,目前**主要有串行、并行和并發三種**,對于大內存的應用而言,串行的性能太低,因此使用到的主要是并行和并發兩種。并行和并發 GC 的策略通過 `UseParallelGC `和` UseConcMarkSweepGC` 來指定,還有一些細節的配置參數用來配置策略的執行方式。例如:`XX:ParallelGCThreads`, `XX:CMSInitiatingOccupancyFraction` 等。 通常:Young 區對象回收只可選擇并行(耗時間),Old 區選擇并發(耗 CPU)。### 項目中常用配置> 備注:在Java8中永久代的參數`-XX:PermSize` 和`-XX:MaxPermSize`已經失效。### 常用組合## 常用 GC 調優策略1. GC 調優原則; 2. GC 調優目的; 3. GC 調優策略;### GC 調優原則在調優之前,我們需要記住下面的原則:> 多數的 Java 應用不需要在服務器上進行 GC 優化; 多數導致 GC 問題的 Java 應用,都不是因為我們參數設置錯誤,而是代碼問題; 在應用上線之前,先考慮將機器的 JVM 參數設置到最優(最適合); 減少創建對象的數量; 減少使用全局變量和大對象; GC 優化是到最后不得已才采用的手段; 在實際使用中,分析 GC 情況優化代碼比優化 GC 參數要多得多。### GC 調優目的將轉移到老年代的對象數量降低到最小; 減少 GC 的執行時間。### GC 調優策略**策略 1:**將新對象預留在新生代,由于 Full GC 的成本遠高于 Minor GC,因此盡可能將對象分配在新生代是明智的做法,實際項目中根據 GC 日志分析新生代空間大小分配是否合理,適當通過“-Xmn”命令調節新生代大小,最大限度降低新對象直接進入老年代的情況。**策略 2:**大對象進入老年代,雖然大部分情況下,將對象分配在新生代是合理的。但是對于大對象這種做法卻值得商榷,大對象如果首次在新生代分配可能會出現空間不足導致很多年齡不夠的小對象被分配的老年代,破壞新生代的對象結構,可能會出現頻繁的 full gc。因此,對于大對象,可以設置直接進入老年代(當然短命的大對象對于垃圾回收來說簡直就是噩夢)。`-XX:PretenureSizeThreshold` 可以設置直接進入老年代的對象大小。**策略 3:**合理設置進入老年代對象的年齡,`-XX:MaxTenuringThreshold` 設置對象進入老年代的年齡大小,減少老年代的內存占用,降低 full gc 發生的頻率。**策略 4:**設置穩定的堆大小,堆大小設置有兩個參數:`-Xms` 初始化堆大小,`-Xmx` 最大堆大小。**策略5:**注意: 如果滿足下面的指標,**則一般不需要進行 GC 優化:**> MinorGC 執行時間不到50ms; Minor GC 執行不頻繁,約10秒一次; Full GC 執行時間不到1s; Full GC 執行頻率不算頻繁,不低于10分鐘1次。 點擊關注[公眾號](#公眾號)及時獲取筆主最新更新文章,并可免費領取本文檔配套的《Java面試突擊》以及Java工程師必備學習資源。<!-- TOC -->- [Java 內存區域詳解](#java-內存區域詳解)- [寫在前面 (常見面試題)](#寫在前面-常見面試題)- [基本問題](#基本問題)- [拓展問題](#拓展問題)- [一 概述](#一-概述)- [二 運行時數據區域](#二-運行時數據區域)- [2.1 程序計數器](#21-程序計數器)- [2.2 Java 虛擬機棧](#22-java-虛擬機棧)- [2.3 本地方法棧](#23-本地方法棧)- [2.4 堆](#24-堆)- [2.5 方法區](#25-方法區)- [2.5.1 方法區和永久代的關系](#251-方法區和永久代的關系)- [2.5.2 常用參數](#252-常用參數)- [2.5.3 為什么要將永久代 (PermGen) 替換為元空間 (MetaSpace) 呢?](#253-為什么要將永久代-permgen-替換為元空間-metaspace-呢)- [2.6 運行時常量池](#26-運行時常量池)- [2.7 直接內存](#27-直接內存)- [三 HotSpot 虛擬機對象探秘](#三-hotspot-虛擬機對象探秘)- [3.1 對象的創建](#31-對象的創建)- [Step1:類加載檢查](#step1類加載檢查)- [Step2:分配內存](#step2分配內存)- [Step3:初始化零值](#step3初始化零值)- [Step4:設置對象頭](#step4設置對象頭)- [Step5:執行 init 方法](#step5執行-init-方法)- [3.2 對象的內存布局](#32-對象的內存布局)- [3.3 對象的訪問定位](#33-對象的訪問定位)- [四 重點補充內容](#四--重點補充內容)- [4.1 String 類和常量池](#41-string-類和常量池)- [4.2 String s1 = new String("abc");這句話創建了幾個字符串對象?](#42-string-s1--new-stringabc這句話創建了幾個字符串對象)- [4.3 8 種基本類型的包裝類和常量池](#43-8-種基本類型的包裝類和常量池)- [參考](#參考)- [公眾號](#公眾號)<!-- /TOC --># Java 內存區域詳解如果沒有特殊說明,都是針對的是 HotSpot 虛擬機。## 寫在前面 (常見面試題)### 基本問題- **介紹下 Java 內存區域(運行時數據區)** - **Java 對象的創建過程(五步,建議能默寫出來并且要知道每一步虛擬機做了什么)** - **對象的訪問定位的兩種方式(句柄和直接指針兩種方式)**### 拓展問題- **String 類和常量池** - **8 種基本類型的包裝類和常量池**## 一 概述對于 Java 程序員來說,在虛擬機自動內存管理機制下,不再需要像 C/C++程序開發程序員這樣為每一個 new 操作去寫對應的 delete/free 操作,不容易出現內存泄漏和內存溢出問題。正是因為 Java 程序員把內存控制權利交給 Java 虛擬機,一旦出現內存泄漏和溢出方面的問題,如果不了解虛擬機是怎樣使用內存的,那么排查錯誤將會是一個非常艱巨的任務。## 二 運行時數據區域 Java 虛擬機在執行 Java 程序的過程中會把它管理的內存劃分成若干個不同的數據區域。JDK. 1.8 和之前的版本略有不同,下面會介紹到。**JDK 1.8 之前:**<div align="center"> <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/JVM運行時數據區域.png" width="600px"/> </div>**JDK 1.8 :**<div align="center"> <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3Java運行時數據區域JDK1.8.png" width="600px"/> </div>**線程私有的:**- 程序計數器 - 虛擬機棧 - 本地方法棧**線程共享的:**- 堆 - 方法區 - 直接內存 (非運行時數據區的一部分)### 2.1 程序計數器 程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。**字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完成。**另外,**為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。****從上面的介紹中我們知道程序計數器主要有兩個作用:**1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。 2. 在多線程的情況下,程序計數器用于記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。**注意:程序計數器是唯一一個不會出現 OutOfMemoryError 的內存區域,它的生命周期隨著線程的創建而創建,隨著線程的結束而死亡。**### 2.2 Java 虛擬機棧**與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命周期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。****Java 內存可以粗糙的區分為堆內存(Heap)和棧內存 (Stack),其中棧就是現在說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。** (實際上,Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。)**局部變量表主要存放了編譯器可知的各種數據類型**(boolean、byte、char、short、int、float、long、double)、**對象引用**(reference 類型,它不同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)。**Java 虛擬機棧會出現兩種錯誤:StackOverFlowError 和 OutOfMemoryError。**- **StackOverFlowError:** 若 Java 虛擬機棧的內存大小不允許動態擴展,那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 錯誤。 - **OutOfMemoryError:** 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 錯誤。Java 虛擬機棧也是線程私有的,每個線程都有各自的 Java 虛擬機棧,而且隨著線程的創建而創建,隨著線程的死亡而死亡。**擴展:那么方法/函數如何調用?**Java 棧可用類比數據結構中棧,Java 棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每一個函數調用結束后,都會有一個棧幀被彈出。Java 方法有兩種返回方式:1. return 語句。 2. 拋出異常。不管哪種返回方式都會導致棧幀被彈出。### 2.3 本地方法棧和虛擬機棧所發揮的作用非常相似,區別是: **虛擬機棧為虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。** 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用于存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。方法執行完畢后相應的棧幀也會出棧并釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種錯誤。### 2.4 堆Java 虛擬機所管理的內存中最大的一塊,Java 堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。**此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。**Java 堆是垃圾收集器管理的主要區域,因此也被稱作**GC 堆(Garbage Collected Heap)**.從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間、From Survivor、To Survivor 空間等。**進一步劃分的目的是更好地回收內存,或者更快地分配內存。**在 JDK 7 版本及JDK 7 版本之前,堆內存被通常被分為下面三部分:1. 新生代內存(Young Generation) 2. 老生代(Old Generation) 3. 永生代(Permanent Generation)JDK 8 版本之后方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。**上圖所示的 Eden 區、兩個 Survivor 區都屬于新生代(為了區分,這兩個 Survivor 區域按照順序被命名為 from 和 to),中間一層屬于老年代。**大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s0 或者 s1,并且對象的年齡還會加 1(Eden 區->Survivor 區后對象的初始年齡變為 1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 `-XX:MaxTenuringThreshold` 來設置。> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍歷所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了survivor區的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作為新的晉升年齡閾值”。 > > **動態年齡計算的代碼如下** > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { > //survivor_capacity是survivor空間的大小 > size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); > size_t total = 0; > uint age = 1; > while (age < table_size) { > total += sizes[age];//sizes數組是每個年齡段對象大小 > if (total > desired_survivor_size) break; > age++; > } > uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; > ... > } > > ``` > > 堆這里最容易出現的就是 OutOfMemoryError 錯誤,并且出現這種錯誤之后的表現形式還會有幾種,比如:1. **`OutOfMemoryError: GC Overhead Limit Exceeded`** : 當JVM花太多時間執行垃圾回收并且只能回收很少的堆空間時,就會發生此錯誤。 2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在創建新的對象時, 堆內存中的空間不足以存放新創建的對象, 就會引發`java.lang.OutOfMemoryError: Java heap space` 錯誤。(和本機物理內存無關,和你配置的對內存大小有關!) 3. ......### 2.5 方法區方法區與 Java 堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然 **Java 虛擬機規范把方法區描述為堆的一個邏輯部分**,但是它卻有一個別名叫做 **Non-Heap(非堆)**,目的應該是與 Java 堆區分開來。方法區也被稱為永久代。很多人都會分不清方法區和永久代的關系,為此我也查閱了文獻。#### 2.5.1 方法區和永久代的關系> 《Java 虛擬機規范》只是規定了有方法區這么個概念和它的作用,并沒有規定如何去實現它。那么,在不同的 JVM 上方法區的實現肯定是不同的了。 **方法區和永久代的關系很像 Java 中接口和類的關系,類實現了接口,而永久代就是 HotSpot 虛擬機對虛擬機規范中方法區的一種實現方式。** 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規范中的定義,是一種規范,而永久代是一種實現,一個是標準一個是實現,其他的虛擬機實現并沒有永久代這一說法。#### 2.5.2 常用參數JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小```java -XX:PermSize=N //方法區 (永久代) 初始大小 -XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen ```相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據進入方法區后就“永久存在”了。JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。下面是一些常用參數:```java -XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小) -XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小 ```與永久代很大的不同就是,如果不指定大小的話,隨著更多類的創建,虛擬機會耗盡所有可用的系統內存。#### 2.5.3 為什么要將永久代 (PermGen) 替換為元空間 (MetaSpace) 呢?1. 整個永久代有一個 JVM 本身設置固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,雖然元空間仍舊可能溢出,但是比原來出現的幾率會更小。 >當你元空間溢出時會得到如下錯誤: `java.lang.OutOfMemoryError: MetaSpace`你可以使用 `-XX:MaxMetaspaceSize` 標志設置最大元空間大小,默認值為 unlimited,這意味著它只受系統內存的限制。`-XX:MetaspaceSize` 調整標志定義元空間的初始大小如果未指定此標志,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。 2. 元空間里面存放的是類的元數據,這樣加載多少類的元數據就不由 `MaxPermSize` 控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。 3. 在 JDK8,合并 HotSpot 和 JRockit 的代碼時, JRockit 從來沒有一個叫永久代的東西, 合并之后就沒有必要額外的設置這么一個永久代的地方了。### 2.6 運行時常量池運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用于存放編譯期生成的各種字面量和符號引用)既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 錯誤。**JDK1.7 及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。**  ——圖片來源:https://blog.csdn.net/wangbiao007/article/details/78545189### 2.7 直接內存**直接內存并不是虛擬機運行時數據區的一部分,也不是虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現。**JDK1.4 中新加入的 **NIO(New Input/Output) 類**,引入了一種基于**通道(Channel)** 與**緩存區(Buffer)** 的 I/O 方式,它可以直接使用 Native 函數庫直接分配堆外內存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因為**避免了在 Java 堆和 Native 堆之間來回復制數據**。本機直接內存的分配不會受到 Java 堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。## 三 HotSpot 虛擬機對象探秘 通過上面的介紹我們大概知道了虛擬機的內存情況,下面我們來詳細的了解一下 HotSpot 虛擬機在 Java 堆中對象分配、布局和訪問的全過程。### 3.1 對象的創建 下圖便是 Java 對象的創建過程,我建議最好是能默寫出來,并且要掌握每一步在做什么。 #### Step1:類加載檢查虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。#### Step2:分配內存在**類加載檢查**通過后,接下來虛擬機將為新生對象**分配內存**。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務等同于把一塊確定大小的內存從 Java 堆中劃分出來。**分配方式**有 **“指針碰撞”** 和 **“空閑列表”** 兩種,**選擇那種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定**。**內存分配的兩種方式:(補充內容,需要掌握)**選擇以上兩種方式中的哪一種,取決于 Java 堆內存是否規整。而 Java 堆內存是否規整,取決于 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,復制算法內存也是規整的**內存分配并發問題(補充內容,需要掌握)**在創建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發過程中,創建對象是很頻繁的事情,作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:- **CAS+失敗重試:** CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。**虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。** - **TLAB:** 為每一個線程預先在 Eden 區分配一塊兒內存,JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大于 TLAB 中的剩余內存或 TLAB 的內存已用盡時,再采用上述的 CAS 進行內存分配#### Step3:初始化零值內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。#### Step4:設置對象頭初始化零值完成之后,**虛擬機要對對象進行必要的設置**,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 **這些信息存放在對象頭中。** 另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。#### Step5:執行 init 方法在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛開始,`<init>` 方法還沒有執行,所有的字段都還為零。所以一般來說,執行 new 指令之后會接著執行 `<init>` 方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。### 3.2 對象的內存布局在 Hotspot 虛擬機中,對象在內存中的布局可以分為 3 塊區域:**對象頭**、**實例數據**和**對齊填充**。**Hotspot 虛擬機的對象頭包括兩部分信息**,**第一部分用于存儲對象自身的自身運行時數據**(哈希碼、GC 分代年齡、鎖狀態標志等等),**另一部分是類型指針**,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。**實例數據部分是對象真正存儲的有效信息**,也是在程序中所定義的各種類型的字段內容。**對齊填充部分不是必然存在的,也沒有什么特別的含義,僅僅起占位作用。** 因為 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。### 3.3 對象的訪問定位 建立對象就是為了使用對象,我們的 Java 程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有**①使用句柄**和**②直接指針**兩種:1. **句柄:** 如果使用句柄的話,那么 Java 堆中將會劃分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;2. **直接指針:** 如果使用直接指針訪問,那么 Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。**這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。**## 四 重點補充內容### 4.1 String 類和常量池**String 對象的兩種創建方式:**```java String str1 = "abcd";//先檢查字符串常量池中有沒有"abcd",如果字符串常量池中沒有,則創建一個,然后 str1 指向字符串常量池中的對象,如果有,則直接將 str1 指向"abcd""; String str2 = new String("abcd");//堆中創建一個新的對象 String str3 = new String("abcd");//堆中創建一個新的對象 System.out.println(str1==str2);//false System.out.println(str2==str3);//false ```這兩種不同的創建方法是有差別的。- 第一種方式是在常量池中拿對象; - 第二種方式是直接在堆內存空間創建一個新的對象。記住一點:**只要使用 new 方法,便需要創建新的對象。**再給大家一個圖應該更容易理解,圖片來源:<https://www.journaldev.com/797/what-is-java-string-pool>:**String 類型的常量池比較特殊。它的主要使用方法有兩種:**- 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。 - 如果不是用雙引號聲明的 String 對象,可以使用 String 提供的 intern 方法。String.intern() 是一個 Native 方法,它的作用是:如果運行時常量池中已經包含一個等于此 String 對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,JDK1.7之前(不包含1.7)的處理方式是在常量池中創建與此 String 內容相同的字符串,并返回常量池中創建的字符串的引用,JDK1.7以及之后的處理方式是在常量池中記錄此字符串的引用,并返回該引用。```javaString s1 = new String("計算機");String s2 = s1.intern();String s3 = "計算機";System.out.println(s2);//計算機System.out.println(s1 == s2);//false,因為一個是堆內存中的 String 對象一個是常量池中的 String 對象,System.out.println(s3 == s2);//true,因為兩個都是常量池中的 String 對象 ``` **字符串拼接:**```javaString str1 = "str";String str2 = "ing";String str3 = "str" + "ing";//常量池中的對象String str4 = str1 + str2; //在堆上創建的新的對象 String str5 = "string";//常量池中的對象System.out.println(str3 == str4);//falseSystem.out.println(str3 == str5);//trueSystem.out.println(str4 == str5);//false ``` 盡量避免多個字符串拼接,因為這樣會重新創建對象。如果需要改變字符串的話,可以使用 StringBuilder 或者 StringBuffer。 ### 4.2 String s1 = new String("abc");這句話創建了幾個字符串對象?**將創建 1 或 2 個字符串。如果池中已存在字符串常量“abc”,則只會在堆空間創建一個字符串常量“abc”。如果池中沒有字符串常量“abc”,那么它將首先在池中創建,然后在堆空間中創建,因此將創建總共 2 個字符串對象。****驗證:**```javaString s1 = new String("abc");// 堆內存的地址值String s2 = "abc";System.out.println(s1 == s2);// 輸出 false,因為一個是堆內存,一個是常量池的內存,故兩者是不同的。System.out.println(s1.equals(s2));// 輸出 true ```**結果:**``` false true ```### 4.3 8 種基本類型的包裝類和常量池- **Java 基本類型的包裝類的大部分都實現了常量池技術,即 Byte,Short,Integer,Long,Character,Boolean;這 5 種包裝類默認創建了數值[-128,127] 的相應類型的緩存數據,但是超出此范圍仍然會去創建新的對象。** 為啥把緩存設置為[-128,127]區間?([參見issue/461](https://github.com/Snailclimb/JavaGuide/issues/461))性能和資源之間的權衡。 - **兩種浮點數類型的包裝類 Float,Double 并沒有實現常量池技術。**```javaInteger i1 = 33;Integer i2 = 33;System.out.println(i1 == i2);// 輸出 trueInteger i11 = 333;Integer i22 = 333;System.out.println(i11 == i22);// 輸出 falseDouble i3 = 1.2;Double i4 = 1.2;System.out.println(i3 == i4);// 輸出 false ```**Integer 緩存源代碼:** ```java /** *此方法將始終緩存-128 到 127(包括端點)范圍內的值,并可以緩存此范圍之外的其他值。 */public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i);}```**應用場景:** 1. Integer i1=40;Java 在編譯的時候會直接將代碼封裝成 Integer i1=Integer.valueOf(40);,從而使用常量池中的對象。 2. Integer i1 = new Integer(40);這種情況下會創建新的對象。```javaInteger i1 = 40;Integer i2 = new Integer(40);System.out.println(i1==i2);//輸出 false ``` **Integer 比較更豐富的一個例子:**```javaInteger i1 = 40;Integer i2 = 40;Integer i3 = 0;Integer i4 = new Integer(40);Integer i5 = new Integer(40);Integer i6 = new Integer(0);System.out.println("i1=i2 " + (i1 == i2));System.out.println("i1=i2+i3 " + (i1 == i2 + i3));System.out.println("i1=i4 " + (i1 == i4));System.out.println("i4=i5 " + (i4 == i5));System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); System.out.println("40=i5+i6 " + (40 == i5 + i6)); ```結果:``` i1=i2 true i1=i2+i3 true i1=i4 false i4=i5 false i4=i5+i6 true 40=i5+i6 true ```解釋:語句 i4 == i5 + i6,因為+這個操作符不適用于 Integer 對象,首先 i5 和 i6 進行自動拆箱操作,進行數值相加,即 i4 == 40。然后 Integer 對象無法與數值進行直接比較,所以 i4 自動拆箱轉為 int 值 40,最終這條語句轉為 40 == 40 進行數值比較。## 參考- 《深入理解 Java 虛擬機:JVM 高級特性與最佳實踐(第二版》 - 《實戰 java 虛擬機》 - <https://docs.oracle.com/javase/specs/index.html> - <http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/> - <https://dzone.com/articles/jvm-permgen-%E2%80%93-where-art-thou> - <https://stackoverflow.com/questions/9095748/method-area-and-permgen> - 深入解析String#intern<https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html>## 公眾號如果大家想要實時關注我更新的文章以及分享的干貨的話,可以關注我的公眾號。**《Java面試突擊》:** 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本[公眾號](#公眾號)后臺回復 **"Java面試突擊"** 即可免費領取!**Java工程師必備學習資源:** 一些Java工程師常用學習資源[公眾號](#公眾號)后臺回復關鍵字 **“1”** 即可免費無套路獲取。  點擊關注[公眾號](#公眾號)及時獲取筆主最新更新文章,并可免費領取本文檔配套的《Java面試突擊》以及Java工程師必備學習資源。<!-- TOC -->- [JDK 監控和故障處理工具總結](#jdk-監控和故障處理工具總結)- [JDK 命令行工具](#jdk-命令行工具)- [`jps`:查看所有 Java 進程](#jps查看所有-java-進程)- [`jstat`: 監視虛擬機各種運行狀態信息](#jstat-監視虛擬機各種運行狀態信息)- [` jinfo`: 實時地查看和調整虛擬機各項參數](#-jinfo-實時地查看和調整虛擬機各項參數)- [`jmap`:生成堆轉儲快照](#jmap生成堆轉儲快照)- [**`jhat`**: 分析 heapdump 文件](#jhat-分析-heapdump-文件)- [**`jstack`** :生成虛擬機當前時刻的線程快照](#jstack-生成虛擬機當前時刻的線程快照)- [JDK 可視化分析工具](#jdk-可視化分析工具)- [JConsole:Java 監視與管理控制臺](#jconsolejava-監視與管理控制臺)- [連接 Jconsole](#連接-jconsole)- [查看 Java 程序概況](#查看-java-程序概況)- [內存監控](#內存監控)- [線程監控](#線程監控)- [Visual VM:多合一故障處理工具](#visual-vm多合一故障處理工具)<!-- /TOC --># JDK 監控和故障處理工具總結## JDK 命令行工具這些命令在 JDK 安裝目錄下的 bin 目錄下:- **`jps`** (JVM Process Status): 類似 UNIX 的 `ps` 命令。用戶查看所有 Java 進程的啟動類、傳入參數和 Java 虛擬機參數等信息; - **`jstat`**( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虛擬機各方面的運行數據; - **`jinfo`** (Configuration Info for Java) : Configuration Info forJava,顯示虛擬機配置信息; - **`jmap`** (Memory Map for Java) :生成堆轉儲快照; - **`jhat`** (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它會建立一個 HTTP/HTML 服務器,讓用戶可以在瀏覽器上查看分析結果; - **`jstack`** (Stack Trace for Java):生成虛擬機當前時刻的線程快照,線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合。### `jps`:查看所有 Java 進程`jps`(JVM Process Status) 命令類似 UNIX 的 `ps` 命令。`jps`:顯示虛擬機執行主類名稱以及這些進程的本地虛擬機唯一 ID(Local Virtual Machine Identifier,LVMID)。`jps -q` :只輸出進程的本地虛擬機唯一 ID。```powershell C:\Users\SnailClimb>jps 7360 NettyClient2 17396 7972 Launcher 16504 Jps 17340 NettyServer ````jps -l`:輸出主類的全名,如果進程執行的是 Jar 包,輸出 Jar 路徑。```powershell C:\Users\SnailClimb>jps -l 7360 firstNettyDemo.NettyClient2 17396 7972 org.jetbrains.jps.cmdline.Launcher 16492 sun.tools.jps.Jps 17340 firstNettyDemo.NettyServer ````jps -v`:輸出虛擬機進程啟動時 JVM 參數。`jps -m`:輸出傳遞給 Java 進程 main() 函數的參數。### `jstat`: 監視虛擬機各種運行狀態信息jstat(JVM Statistics Monitoring Tool) 使用于監視虛擬機各種運行狀態信息的命令行工具。 它可以顯示本地或者遠程(需要遠程主機提供 RMI 支持)虛擬機進程中的類信息、內存、垃圾收集、JIT 編譯等運行數據,在沒有 GUI,只提供了純文本控制臺環境的服務器上,它將是運行期間定位虛擬機性能問題的首選工具。**`jstat` 命令使用格式:**```powershell jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]] ```比如 `jstat -gc -h3 31736 1000 10`表示分析進程 id 為 31736 的 gc 情況,每隔 1000ms 打印一次記錄,打印 10 次停止,每 3 行后打印指標頭部。**常見的 option 如下:**- `jstat -class vmid` :顯示 ClassLoader 的相關信息; - `jstat -compiler vmid` :顯示 JIT 編譯的相關信息; - `jstat -gc vmid` :顯示與 GC 相關的堆信息; - `jstat -gccapacity vmid` :顯示各個代的容量及使用情況; - `jstat -gcnew vmid` :顯示新生代信息; - `jstat -gcnewcapcacity vmid` :顯示新生代大小與使用情況; - `jstat -gcold vmid` :顯示老年代和永久代的信息; - `jstat -gcoldcapacity vmid` :顯示老年代的大小; - `jstat -gcpermcapacity vmid` :顯示永久代大小; - `jstat -gcutil vmid` :顯示垃圾收集信息;另外,加上 `-t`參數可以在輸出信息上加一個 Timestamp 列,顯示程序的運行時間。### ` jinfo`: 實時地查看和調整虛擬機各項參數`jinfo vmid` :輸出當前 jvm 進程的全部參數和系統屬性 (第一部分是系統的屬性,第二部分是 JVM 的參數)。`jinfo -flag name vmid` :輸出對應名稱的參數的具體值。比如輸出 MaxHeapSize、查看當前 jvm 進程是否開啟打印 GC 日志 ( `-XX:PrintGCDetails` :詳細 GC 日志模式,這兩個都是默認關閉的)。```powershell C:\Users\SnailClimb>jinfo -flag MaxHeapSize 17340 -XX:MaxHeapSize=2124414976 C:\Users\SnailClimb>jinfo -flag PrintGC 17340 -XX:-PrintGC ```使用 jinfo 可以在不重啟虛擬機的情況下,可以動態的修改 jvm 的參數。尤其在線上的環境特別有用,請看下面的例子:`jinfo -flag [+|-]name vmid` 開啟或者關閉對應名稱的參數。```powershell C:\Users\SnailClimb>jinfo -flag PrintGC 17340 -XX:-PrintGCC:\Users\SnailClimb>jinfo -flag +PrintGC 17340C:\Users\SnailClimb>jinfo -flag PrintGC 17340 -XX:+PrintGC ```### `jmap`:生成堆轉儲快照`jmap`(Memory Map for Java)命令用于生成堆轉儲快照。 如果不使用 `jmap` 命令,要想獲取 Java 堆轉儲,可以使用 `“-XX:+HeapDumpOnOutOfMemoryError”` 參數,可以讓虛擬機在 OOM 異常出現之后自動生成 dump 文件,Linux 命令下可以通過 `kill -3` 發送進程退出信號也能拿到 dump 文件。`jmap` 的作用并不僅僅是為了獲取 dump 文件,它還可以查詢 finalizer 執行隊列、Java 堆和永久代的詳細信息,如空間使用率、當前使用的是哪種收集器等。和`jinfo`一樣,`jmap`有不少功能在 Windows 平臺下也是受限制的。示例:將指定應用程序的堆快照輸出到桌面。后面,可以通過 jhat、Visual VM 等工具分析該堆文件。```powershell C:\Users\SnailClimb>jmap -dump:format=b,file=C:\Users\SnailClimb\Desktop\heap.hprof 17340 Dumping heap to C:\Users\SnailClimb\Desktop\heap.hprof ... Heap dump file created ```### **`jhat`**: 分析 heapdump 文件**`jhat`** 用于分析 heapdump 文件,它會建立一個 HTTP/HTML 服務器,讓用戶可以在瀏覽器上查看分析結果。```powershell C:\Users\SnailClimb>jhat C:\Users\SnailClimb\Desktop\heap.hprof Reading from C:\Users\SnailClimb\Desktop\heap.hprof... Dump file created Sat May 04 12:30:31 CST 2019 Snapshot read, resolving... Resolving 131419 objects... Chasing references, expect 26 dots.......................... Eliminating duplicate references.......................... Snapshot resolved. Started HTTP server on port 7000 Server is ready. ```訪問 <http://localhost:7000/>### **`jstack`** :生成虛擬機當前時刻的線程快照`jstack`(Stack Trace for Java)命令用于生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合.生成線程快照的目的主要是定位線程長時間出現停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的原因。線程出現停頓的時候通過`jstack`來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在后臺做些什么事情,或者在等待些什么資源。**下面是一個線程死鎖的代碼。我們下面會通過 `jstack` 命令進行死鎖檢查,輸出死鎖信息,找到發生死鎖的線程。**```java public class DeadLockDemo {private static Object resource1 = new Object();//資源 1private static Object resource2 = new Object();//資源 2public static void main(String[] args) {new Thread(() -> {synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread() + "waiting get resource2");synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");}}}, "線程 1").start();new Thread(() -> {synchronized (resource2) {System.out.println(Thread.currentThread() + "get resource2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread() + "waiting get resource1");synchronized (resource1) {System.out.println(Thread.currentThread() + "get resource1");}}}, "線程 2").start();} } ```Output``` Thread[線程 1,5,main]get resource1 Thread[線程 2,5,main]get resource2 Thread[線程 1,5,main]waiting get resource2 Thread[線程 2,5,main]waiting get resource1 ```線程 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然后通過` Thread.sleep(1000);`讓線程 A 休眠 1s 為的是讓線程 B 得到執行然后獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然后這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。**通過 `jstack` 命令分析:**```powershell C:\Users\SnailClimb>jps 13792 KotlinCompileDaemon 7360 NettyClient2 17396 7972 Launcher 8932 Launcher 9256 DeadLockDemo 10764 Jps 17340 NettyServerC:\Users\SnailClimb>jstack 9256 ```輸出的部分內容如下:```powershell Found one Java-level deadlock: ============================= "線程 2":waiting to lock monitor 0x000000000333e668 (object 0x00000000d5efe1c0, a java.lang.Object),which is held by "線程 1" "線程 1":waiting to lock monitor 0x000000000333be88 (object 0x00000000d5efe1d0, a java.lang.Object),which is held by "線程 2"Java stack information for the threads listed above: =================================================== "線程 2":at DeadLockDemo.lambda$main$1(DeadLockDemo.java:31)- waiting to lock <0x00000000d5efe1c0> (a java.lang.Object)- locked <0x00000000d5efe1d0> (a java.lang.Object)at DeadLockDemo$$Lambda$2/1078694789.run(Unknown Source)at java.lang.Thread.run(Thread.java:748) "線程 1":at DeadLockDemo.lambda$main$0(DeadLockDemo.java:16)- waiting to lock <0x00000000d5efe1d0> (a java.lang.Object)- locked <0x00000000d5efe1c0> (a java.lang.Object)at DeadLockDemo$$Lambda$1/1324119927.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Found 1 deadlock. ```可以看到 `jstack` 命令已經幫我們找到發生死鎖的線程的具體信息。## JDK 可視化分析工具### JConsole:Java 監視與管理控制臺JConsole 是基于 JMX 的可視化監視、管理工具。可以很方便的監視本地及遠程服務器的 java 進程的內存使用情況。你可以在控制臺輸出`console`命令啟動或者在 JDK 目錄下的 bin 目錄找到`jconsole.exe`然后雙擊啟動。#### 連接 Jconsole如果需要使用 JConsole 連接遠程進程,可以在遠程 Java 程序啟動時加上下面這些參數:```properties -Djava.rmi.server.hostname=外網訪問 ip 地址 -Dcom.sun.management.jmxremote.port=60001 //監控的端口號 -Dcom.sun.management.jmxremote.authenticate=false //關閉認證 -Dcom.sun.management.jmxremote.ssl=false ```在使用 JConsole 連接時,遠程進程地址如下:``` 外網訪問 ip 地址:60001 ```#### 查看 Java 程序概況#### 內存監控JConsole 可以顯示當前內存的詳細信息。不僅包括堆內存/非堆內存的整體信息,還可以細化到 eden 區、survivor 區等的使用情況,如下圖所示。點擊右邊的“執行 GC(G)”按鈕可以強制應用程序執行一個 Full GC。> - **新生代 GC(Minor GC)**:指發生新生代的的垃圾收集動作,Minor GC 非常頻繁,回收速度一般也比較快。 > - **老年代 GC(Major GC/Full GC)**:指發生在老年代的 GC,出現了 Major GC 經常會伴隨至少一次的 Minor GC(并非絕對),Major GC 的速度一般會比 Minor GC 的慢 10 倍以上。#### 線程監控類似我們前面講的 `jstack` 命令,不過這個是可視化的。最下面有一個"檢測死鎖 (D)"按鈕,點擊這個按鈕可以自動為你找到發生死鎖的線程以及它們的詳細信息 。### Visual VM:多合一故障處理工具VisualVM 提供在 Java 虛擬機 (Java Virutal Machine, JVM) 上運行的 Java 應用程序的詳細信息。在 VisualVM 的圖形用戶界面中,您可以方便、快捷地查看多個 Java 應用程序的相關信息。Visual VM 官網:<https://visualvm.github.io/> 。Visual VM 中文文檔:<https://visualvm.github.io/documentation.html>。下面這段話摘自《深入理解 Java 虛擬機》。> VisualVM(All-in-One Java Troubleshooting Tool)是到目前為止隨 JDK 發布的功能最強大的運行監視和故障處理程序,官方在 VisualVM 的軟件說明中寫上了“All-in-One”的描述字樣,預示著他除了運行監視、故障處理外,還提供了很多其他方面的功能,如性能分析(Profiling)。VisualVM 的性能分析功能甚至比起 JProfiler、YourKit 等專業且收費的 Profiling 工具都不會遜色多少,而且 VisualVM 還有一個很大的優點:不需要被監視的程序基于特殊 Agent 運行,因此他對應用程序的實際性能的影響很小,使得他可以直接應用在生產環境中。這個優點是 JProfiler、YourKit 等工具無法與之媲美的。VisualVM 基于 NetBeans 平臺開發,因此他一開始就具備了插件擴展功能的特性,通過插件擴展支持,VisualVM 可以做到:- **顯示虛擬機進程以及進程的配置、環境信息(jps、jinfo)。** - **監視應用程序的 CPU、GC、堆、方法區以及線程的信息(jstat、jstack)。** - **dump 以及分析堆轉儲快照(jmap、jhat)。** - **方法級的程序運行性能分析,找到被調用最多、運行時間最長的方法。** - **離線程序快照:收集程序的運行時配置、線程 dump、內存 dump 等信息建立一個快照,可以將快照發送開發者處進行 Bug 反饋。** - **其他 plugins 的無限的可能性......**這里就不具體介紹 VisualVM 的使用,如果想了解的話可以看:- <https://visualvm.github.io/documentation.html> - <https://www.ibm.com/developerworks/cn/java/j-lo-visualvm/index.html>## 公眾號如果大家想要實時關注我更新的文章以及分享的干貨的話,可以關注我的公眾號。**《Java面試突擊》:** 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本公眾號后臺回復 **"Java面試突擊"** 即可免費領取!**Java工程師必備學習資源:** 一些Java工程師常用學習資源公眾號后臺回復關鍵字 **“1”** 即可免費無套路獲取。  無論什么級別的Java從業者,JVM都是進階時必須邁過的坎。不管是工作還是面試中,JVM都是必考題。如果不懂JVM的話,薪酬會非常吃虧(近70%的面試者掛在JVM上了)。掌握了JVM機制,就等于學會了深層次解決問題的方法。對于Java開發者而言,只有熟悉底層虛擬機的運行機制,才能通過JVM日志深入到字節碼的層次去分析排查問題,發現隱性的系統缺陷,進而提升系統性能。一些技術人員開發工具用得很熟練,觸及JVM問題時卻是模棱兩可,甚至連內存模型和內存區域,HotSpot和JVM規范,都混淆不清。工作很長時間,在生產時還在用缺省參數來直接啟動,以致系統運行時出現性能、穩定性等問題時束手無措,不知該如何追蹤排查。久而久之,這對自己的職業成長是極為不利的。掌握JVM,是深入Java技術棧的必經之路。 點擊關注[公眾號](#公眾號)及時獲取筆主最新更新文章,并可免費領取本文檔配套的《Java面試突擊》以及Java工程師必備學習資源。<!-- TOC -->- [JVM 垃圾回收](#jvm-垃圾回收)- [寫在前面](#寫在前面)- [本節常見面試題](#本節常見面試題)- [本文導火索](#本文導火索)- [1 揭開 JVM 內存分配與回收的神秘面紗](#1--揭開-jvm-內存分配與回收的神秘面紗)- [1.1 對象優先在 eden 區分配](#11-對象優先在-eden-區分配)- [1.2 大對象直接進入老年代](#12-大對象直接進入老年代)- [1.3 長期存活的對象將進入老年代](#13-長期存活的對象將進入老年代)- [1.4 動態對象年齡判定](#14-動態對象年齡判定)- [2 對象已經死亡?](#2-對象已經死亡)- [2.1 引用計數法](#21-引用計數法)- [2.2 可達性分析算法](#22-可達性分析算法)- [2.3 再談引用](#23-再談引用)- [2.4 不可達的對象并非“非死不可”](#24-不可達的對象并非非死不可)- [2.5 如何判斷一個常量是廢棄常量](#25-如何判斷一個常量是廢棄常量)- [2.6 如何判斷一個類是無用的類](#26-如何判斷一個類是無用的類)- [3 垃圾收集算法](#3-垃圾收集算法)- [3.1 標記-清除算法](#31-標記-清除算法)- [3.2 復制算法](#32-復制算法)- [3.3 標記-整理算法](#33-標記-整理算法)- [3.4 分代收集算法](#34-分代收集算法)- [4 垃圾收集器](#4-垃圾收集器)- [4.1 Serial 收集器](#41-serial-收集器)- [4.2 ParNew 收集器](#42-parnew-收集器)- [4.3 Parallel Scavenge 收集器](#43-parallel-scavenge-收集器)- [4.4.Serial Old 收集器](#44serial-old-收集器)- [4.5 Parallel Old 收集器](#45-parallel-old-收集器)- [4.6 CMS 收集器](#46-cms-收集器)- [4.7 G1 收集器](#47-g1-收集器)- [參考](#參考)<!-- /TOC --> # JVM 垃圾回收## 寫在前面### 本節常見面試題問題答案在文中都有提到- 如何判斷對象是否死亡(兩種方法)。 - 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。 - 如何判斷一個常量是廢棄常量 - 如何判斷一個類是無用的類 - 垃圾收集有哪些算法,各自的特點? - HotSpot 為什么要分為新生代和老年代? - 常見的垃圾回收器有哪些? - 介紹一下 CMS,G1 收集器。 - Minor Gc 和 Full GC 有什么不同呢?### 本文導火索當需要排查各種內存溢出問題、當垃圾收集成為系統達到更高并發的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。## 1 揭開 JVM 內存分配與回收的神秘面紗Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時,Java 自動內存管理最核心的功能是 **堆** 內存中對象的分配與回收。Java 堆是垃圾收集器管理的主要區域,因此也被稱作**GC 堆(Garbage Collected Heap)**.從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間、From Survivor、To Survivor 空間等。**進一步劃分的目的是更好地回收內存,或者更快地分配內存。****堆空間的基本結構:**<div align="center"> <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3堆結構.png" width="400px"/> </div>上圖所示的 eden 區、s0("From") 區、s1("To") 區都屬于新生代,tentired 區屬于老年代。大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s1("To"),并且對象的年齡還會加 1(Eden 區->Survivor 區后對象的初始年齡變為 1),當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 `-XX:MaxTenuringThreshold` 來設置。經過這次GC后,Eden區和"From"區已經被清空。這個時候,"From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重復這樣的過程,直到“To”區被填滿,"To"區被填滿之后,會將所有對象移動到老年代中。### 1.1 對象優先在 eden 區分配目前主流的垃圾收集器都會采用分代回收算法,因此需要將堆內存分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。大多數情況下,對象在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC.下面我們來進行實際測試以下。在測試之前我們先來看看 **Minor GC 和 Full GC 有什么不同呢?**- **新生代 GC(Minor GC)**:指發生新生代的的垃圾收集動作,Minor GC 非常頻繁,回收速度一般也比較快。 - **老年代 GC(Major GC/Full GC)**:指發生在老年代的 GC,出現了 Major GC 經常會伴隨至少一次的 Minor GC(并非絕對),Major GC 的速度一般會比 Minor GC 的慢 10 倍以上。**測試:**```java public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2;allocation1 = new byte[30900*1024];//allocation2 = new byte[900*1024];} } ``` 通過以下方式運行: 添加的參數:`-XX:+PrintGCDetails` 運行結果 (紅色字體描述有誤,應該是對應于 JDK1.7 的永久代):從上圖我們可以看出 eden 區內存幾乎已經被分配完全(即使程序什么也不做,新生代也會使用 2000 多 k 內存)。假如我們再為 allocation2 分配內存會出現什么情況呢?```java allocation2 = new byte[900*1024]; ``` **簡單解釋一下為什么會出現這種情況:** 因為給 allocation2 分配內存的時候 eden 區內存幾乎已經被分配完了,我們剛剛講了當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC.GC 期間虛擬機又發現 allocation1 無法存入 Survivor 空間,所以只好通過 **分配擔保機制** 把新生代的對象提前轉移到老年代中去,老年代上的空間足夠存放 allocation1,所以不會出現 Full GC。執行 Minor GC 后,后面分配的對象如果能夠存在 eden 區的話,還是會在 eden 區分配內存。可以執行如下代碼驗證:```java public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2,allocation3,allocation4,allocation5;allocation1 = new byte[32000*1024];allocation2 = new byte[1000*1024];allocation3 = new byte[1000*1024];allocation4 = new byte[1000*1024];allocation5 = new byte[1000*1024];} }```### 1.2 大對象直接進入老年代 大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。**為什么要這樣呢?**為了避免為大對象分配內存時由于分配擔保機制帶來的復制而降低效率。### 1.3 長期存活的對象將進入老年代 既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。如果對象在 Eden 出生并經過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并將對象年齡設為 1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 `-XX:MaxTenuringThreshold` 來設置。### 1.4 動態對象年齡判定為了更好的適應不同程序的內存情況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果 Survivor 空間中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無需達到要求的年齡。## 2 對象已經死亡?堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。### 2.1 引用計數法給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任何時候計數器為 0 的對象就是不可能再被使用的。**這個方法實現簡單,效率高,但是目前主流的虛擬機中并沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。** 所謂對象之間的相互引用問題,如下面代碼所示:除了對象 objA 和 objB 相互引用著對方之外,這兩個對象之間再無任何引用。但是他們因為互相引用對方,導致它們的引用計數器都不為 0,于是引用計數算法無法通知 GC 回收器回收他們。```java public class ReferenceCountingGc {Object instance = null;public static void main(String[] args) {ReferenceCountingGc objA = new ReferenceCountingGc();ReferenceCountingGc objB = new ReferenceCountingGc();objA.instance = objB;objB.instance = objA;objA = null;objB = null;} } ```### 2.2 可達性分析算法這個算法的基本思想就是通過一系列的稱為 **“GC Roots”** 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。### 2.3 再談引用無論是通過引用計數法判斷對象引用數量,還是通過可達性分析法判斷對象的引用鏈是否可達,判定對象的存活都與“引用”有關。JDK1.2 之前,Java 中引用的定義很傳統:如果 reference 類型的數據存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表一個引用。JDK1.2 以后,Java 對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)**1.強引用(StrongReference)**以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于**必不可少的生活用品**,垃圾回收器絕不會回收它。當內存空間不足,Java 虛擬機寧愿拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。**2.軟引用(SoftReference)**如果一個對象只具有軟引用,那就類似于**可有可無的生活用品**。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。**3.弱引用(WeakReference)**如果一個對象只具有弱引用,那就類似于**可有可無的生活用品**。弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。 弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。**4.虛引用(PhantomReference)**"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。**虛引用主要用來跟蹤對象被垃圾回收的活動**。**虛引用與軟引用和弱引用的一個區別在于:** 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。 特別注意,在程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為**軟引用可以加速 JVM 對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生**。### 2.4 不可達的對象并非“非死不可”即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過時,虛擬機將這兩種情況視為沒有必要執行。被判定為需要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。### 2.5 如何判斷一個常量是廢棄常量運行時常量池主要回收的是廢棄的常量。那么,我們如何判斷一個常量是廢棄常量呢?假如在常量池中存在字符串 "abc",如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 "abc" 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,"abc" 就會被系統清理出常量池。注意:我們在 [可能是把 Java 內存區域講的最清楚的一篇文章 ](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484303&idx=1&sn=af0fd436cef755463f59ee4dd0720cbd&chksm=fd9855eecaefdcf8d94ac581cfda4e16c8a730bda60c3b50bc55c124b92f23b6217f7f8e58d5&token=506869459&lang=zh_CN#rd) 也講了 JDK1.7 及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。### 2.6 如何判斷一個類是無用的類方法區主要回收的是無用的類,那么如何判斷一個類是無用的類的呢?判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面 3 個條件才能算是 **“無用的類”** :- 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。 - 加載該類的 ClassLoader 已經被回收。 - 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。虛擬機可以對滿足上述 3 個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。## 3 垃圾收集算法### 3.1 標記-清除算法該算法分為“標記”和“清除”階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。它是最基礎的收集算法,后續的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:1. **效率問題** 2. **空間問題(標記清除后會產生大量不連續的碎片)**<img src="http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/63707281.jpg" alt="公眾號" width="500px">### 3.2 復制算法為了解決效率問題,“復制”收集算法出現了。它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。<img src="http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-27/90984624.jpg" alt="公眾號" width="500px">### 3.3 標記-整理算法 根據老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。### 3.4 分代收集算法當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想,只是根據對象存活周期的不同將內存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。**比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。****延伸面試問題:** HotSpot 為什么要分為新生代和老年代?根據上面的對分代收集算法的介紹回答。## 4 垃圾收集器**如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。**雖然我們對各個收集器進行比較,但并非要挑選出一個最好的收集器。因為直到現在為止還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,**我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器**。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那么我們的 HotSpot 虛擬機就不會實現那么多不同的垃圾收集器了。### 4.1 Serial 收集器 Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 **“單線程”** 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( **"Stop The World"** ),直到它收集結束。**新生代采用復制算法,老年代采用標記-整理算法。** 虛擬機的設計者們當然知道 Stop The World 帶來的不良用戶體驗,所以在后續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。但是 Serial 收集器有沒有優于其他垃圾收集器的地方呢?當然有,它**簡單而高效(與其他收集器的單線程相比)**。Serial 收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對于運行在 Client 模式下的虛擬機來說是個不錯的選擇。### 4.2 ParNew 收集器 **ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為(控制參數、收集算法、回收策略等等)和 Serial 收集器完全一樣。****新生代采用復制算法,老年代采用標記-整理算法。** 它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器(真正意義上的并發收集器,后面會介紹到)配合工作。**并行和并發概念補充:**- **并行(Parallel)** :指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。- **并發(Concurrent)**:指用戶線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個 CPU 上。### 4.3 Parallel Scavenge 收集器Parallel Scavenge 收集器也是使用復制算法的多線程收集器,它看上去幾乎和ParNew都一樣。 **那么它有什么特別之處呢?**``` -XX:+UseParallelGC 使用 Parallel 收集器+ 老年代串行-XX:+UseParallelOldGC使用 Parallel 收集器+ 老年代并行```**Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是 CPU 中用于運行用戶代碼的時間與 CPU 總消耗時間的比值。** Parallel Scavenge 收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,手工優化存在困難的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。**新生代采用復制算法,老年代采用標記-整理算法。** ### 4.4.Serial Old 收集器 **Serial 收集器的老年代版本**,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備方案。### 4.5 Parallel Old 收集器**Parallel Scavenge 收集器的老年代版本**。使用多線程和“標記-整理”算法。在注重吞吐量以及 CPU 資源的場合,都可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。### 4.6 CMS 收集器**CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用。****CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。**從名字中的**Mark Sweep**這兩個詞可以看出,CMS 收集器是一種 **“標記-清除”算法**實現的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:- **初始標記:** 暫停所有的其他線程,并記錄下直接與 root 相連的對象,速度很快 ; - **并發標記:** 同時開啟 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發生引用更新的地方。 - **重新標記:** 重新標記階段就是為了修正并發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發標記階段時間短 - **并發清除:** 開啟用戶線程,同時 GC 線程開始對為標記的區域做清掃。從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:**并發收集、低停頓**。但是它有下面三個明顯的缺點:- **對 CPU 資源敏感;** - **無法處理浮動垃圾;** - **它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。**### 4.7 G1 收集器**G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.**被視為 JDK1.7 中 HotSpot 虛擬機的一個重要進化特征。它具備一下特點:- **并行與并發**:G1 能充分利用 CPU、多核環境下的硬件優勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過并發的方式讓 java 程序繼續執行。 - **分代收集**:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。 - **空間整合**:與 CMS 的“標記--清理”算法不同,G1 從整體來看是基于“標記整理”算法實現的收集器;從局部上來看是基于“復制”算法實現的。 - **可預測的停頓**:這是 G1 相對于 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內。G1 收集器的運作大致分為以下幾個步驟:- **初始標記** - **并發標記** - **最終標記** - **篩選回收****G1 收集器在后臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)**。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 GF 收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)。## 參考- 《深入理解 Java 虛擬機:JVM 高級特性與最佳實踐(第二版》 - https://my.oschina.net/hosee/blog/644618 - <https://docs.oracle.com/javase/specs/jvms/se8/html/index.html>## 公眾號如果大家想要實時關注我更新的文章以及分享的干貨的話,可以關注我的公眾號。**《Java面試突擊》:** 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本[公眾號](#公眾號)后臺回復 **"Java面試突擊"** 即可免費領取!**Java工程師必備學習資源:** 一些Java工程師常用學習資源[公眾號](#公眾號)后臺回復關鍵字 **“1”** 即可免費無套路獲取。  > 本文由 JavaGuide 翻譯自 https://www.baeldung.com/jvm-parameters,并對文章進行了大量的完善補充。翻譯不易,如需轉載請注明出處為: 作者: 。## 1.概述在本篇文章中,你將掌握最常用的 JVM 參數配置。如果對于下面提到了一些概念比如堆、## 2.堆內存相關>Java 虛擬機所管理的內存中最大的一塊,Java 堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。**此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。** >### 2.1.顯式指定堆內存`–Xms`和`-Xmx`與性能有關的最常見實踐之一是根據應用程序要求初始化堆內存。如果我們需要指定最小和最大堆大小(推薦顯示指定大小),以下參數可以幫助你實現:``` -Xms<heap size>[unit] -Xmx<heap size>[unit] ```- **heap size** 表示要初始化內存的具體大小。 - **unit** 表示要初始化內存的單位。單位為***“ g”*** (GB) 、***“ m”***(MB)、***“ k”***(KB)。舉個栗子🌰,如果我們要為JVM分配最小2 GB和最大5 GB的堆內存大小,我們的參數應該這樣來寫:``` -Xms2G -Xmx5G ```### 2.2.顯式新生代內存(Young Ceneration)根據[Oracle官方文檔](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html),在堆總可用內存配置完成之后,第二大影響因素是為 `Young Generation` 在堆內存所占的比例。默認情況下,YG 的最小大小為 1310 *MB*,最大大小為*無限制*。一共有兩種指定 新生代內存(Young Ceneration)大小的方法:**1.通過`-XX:NewSize`和`-XX:MaxNewSize`指定**``` -XX:NewSize=<young size>[unit] -XX:MaxNewSize=<young size>[unit] ```舉個栗子🌰,如果我們要為 新生代分配 最小256m 的內存,最大 1024m的內存我們的參數應該這樣來寫:``` -XX:NewSize=256m -XX:MaxNewSize=1024m ```**2.通過`-Xmn<young size>[unit] `指定**舉個栗子🌰,如果我們要為 新生代分配256m的內存(NewSize與MaxNewSize設為一致),我們的參數應該這樣來寫:``` -Xmn256m ```GC 調優策略中很重要的一條經驗總結是這樣說的:> 將新對象預留在新生代,由于 Full GC 的成本遠高于 Minor GC,因此盡可能將對象分配在新生代是明智的做法,實際項目中根據 GC 日志分析新生代空間大小分配是否合理,適當通過“-Xmn”命令調節新生代大小,最大限度降低新對象直接進入老年代的情況。另外,你還可以通過**`-XX:NewRatio=<int>`**來設置新生代和老年代內存的比值。比如下面的參數就是設置新生代(包括Eden和兩個Survivor區)與老年代的比值為1。也就是說:新生代與老年代所占比值為1:1,新生代占整個堆棧的 1/2。``` -XX:NewRatio=1 ```### 2.3.顯示指定永久代/元空間的大小**從Java 8開始,如果我們沒有指定 Metaspace 的大小,隨著更多類的創建,虛擬機會耗盡所有可用的系統內存(永久代并不會出現這種情況)。**JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小```java -XX:PermSize=N //方法區 (永久代) 初始大小 -XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen ```相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據進入方法區后就“永久存在”了。**JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。**下面是一些常用參數:```java -XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小) -XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小,如果不指定大小的話,隨著更多類的創建,虛擬機會耗盡所有可用的系統內存。 ```## 3.垃圾收集相關### 3.1.垃圾回收器為了提高應用程序的穩定性,選擇正確的[垃圾收集](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html)算法至關重要。JVM具有四種類型的*GC*實現:- 串行垃圾收集器 - 并行垃圾收集器 - CMS垃圾收集器 - G1垃圾收集器可以使用以下參數聲明這些實現:``` -XX:+UseSerialGC -XX:+UseParallelGC -XX:+USeParNewGC -XX:+UseG1GC ```有關*垃圾回收*實施的更多詳細信息,請參見[此處](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md)。### 3.2.GC記錄為了嚴格監控應用程序的運行狀況,我們應該始終檢查JVM的*垃圾回收*性能。最簡單的方法是以人類可讀的格式記錄*GC*活動。使用以下參數,我們可以記錄*GC*活動:``` -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=< number of log files > -XX:GCLogFileSize=< file size >[ unit ] -Xloggc:/path/to/gc.log ```## 推薦閱讀- [CMS GC 默認新生代是多大?](https://www.jianshu.com/p/832fc4d4cb53) - [CMS GC啟動參數優化配置](https://www.cnblogs.com/hongdada/p/10277782.html) - [從實際案例聊聊Java應用的GC優化-美團技術團隊](https://tech.meituan.com/2017/12/29/jvm-optimize.html) - [JVM性能調優詳解](https://www.choupangxia.com/2019/11/11/interview-jvm-gc-08/) (2019-11-11) - [JVM參數使用手冊](https://segmentfault.com/a/1190000010603813) 點擊關注[公眾號](#公眾號)及時獲取筆主最新更新文章,并可免費領取本文檔配套的《Java面試突擊》以及Java工程師必備學習資源。<!-- TOC -->- [回顧一下類加載過程](#回顧一下類加載過程) - [類加載器總結](#類加載器總結) - [雙親委派模型](#雙親委派模型)- [雙親委派模型介紹](#雙親委派模型介紹)- [雙親委派模型實現源碼分析](#雙親委派模型實現源碼分析)- [雙親委派模型的好處](#雙親委派模型的好處)- [如果我們不想要雙親委派模型怎么辦?](#如果我們不想要雙親委派模型怎么辦) - [自定義類加載器](#自定義類加載器) - [推薦](#推薦)<!-- /TOC -->> 公眾號JavaGuide 后臺回復關鍵字“1”,免費獲取JavaGuide配套的Java工程師必備學習資源(文末有公眾號二維碼)。## 回顧一下類加載過程類加載過程:**加載->連接->初始化**。連接過程又可分為三步:**驗證->準備->解析**。一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 `loadClass()` 方法)。數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。所有的類都由類加載器加載,加載的作用就是將 .class文件加載到內存。## 類加載器總結JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自`java.lang.ClassLoader`:1. **BootstrapClassLoader(啟動類加載器)** :最頂層的加載類,由C++實現,負責加載 `%JAVA_HOME%/lib`目錄下的jar包和類或者或被 `-Xbootclasspath`參數指定的路徑中的所有類。 2. **ExtensionClassLoader(擴展類加載器)** :主要負責加載目錄 `%JRE_HOME%/lib/ext` 目錄下的jar包和類,或被 `java.ext.dirs` 系統變量所指定的路徑下的jar包。 3. **AppClassLoader(應用程序類加載器)** :面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。## 雙親委派模型### 雙親委派模型介紹每一個類都有一個對應它的類加載器。系統中的 ClassLoder 在協同工作的時候會默認使用 **雙親委派模型** 。即在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載。加載的時候,首先會把該請求委派該父類加載器的 `loadClass()` 處理,因此所有的請求最終都應該傳送到頂層的啟動類加載器 `BootstrapClassLoader` 中。當父類加載器無法處理時,才由自己來處理。當父類加載器為null時,會使用啟動類加載器 `BootstrapClassLoader` 作為父類加載器。每個類加載都有一個父類加載器,我們通過下面的程序來驗證。```java public class ClassLoaderDemo {public static void main(String[] args) {System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());} } ```Output``` ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2 The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586 The GrandParent of ClassLodarDemo's ClassLoader is null ````AppClassLoader`的父類加載器為`ExtClassLoader` `ExtClassLoader`的父類加載器為null,**null并不代表`ExtClassLoader`沒有父類加載器,而是 `BootstrapClassLoader`** 。其實這個雙親翻譯的容易讓別人誤解,我們一般理解的雙親都是父母,這里的雙親更多地表達的是“父母這一輩”的人而已,并不是說真的有一個 Mother ClassLoader 和一個 Father ClassLoader 。另外,類加載器之間的“父子”關系也不是通過繼承來體現的,是由“優先級”來決定。官方API文檔對這部分的描述如下:>The Java platform uses a delegation model for loading classes. **The basic idea is that every class loader has a "parent" class loader.** When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself.### 雙親委派模型實現源碼分析雙親委派模型的實現代碼非常簡單,邏輯非常清晰,都集中在 `java.lang.ClassLoader` 的 `loadClass()` 中,相關代碼如下所示。```java private final ClassLoader parent; protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 首先,檢查請求的類是否已經被加載過Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//父加載器不為空,調用父加載器loadClass()方法處理c = parent.loadClass(name, false);} else {//父加載器為空,使用啟動類加載器 BootstrapClassLoader 加載c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//拋出異常說明父類加載器無法完成加載請求}if (c == null) {long t1 = System.nanoTime();//自己嘗試加載c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}} ```### 雙親委派模型的好處雙親委派模型保證了Java程序的穩定運行,可以避免類的重復加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱為 `java.lang.Object` 類的話,那么程序運行的時候,系統就會出現多個不同的 `Object` 類。### 如果我們不想用雙親委派模型怎么辦?為了避免雙親委托機制,我們可以自己定義一個類加載器,然后重寫 `loadClass()` 即可。## 自定義類加載器除了 `BootstrapClassLoader` 其他類加載器均由 Java 實現且全部繼承自`java.lang.ClassLoader`。如果我們要自定義自己的類加載器,很明顯需要繼承 `ClassLoader`。## 推薦閱讀- <https://blog.csdn.net/xyang81/article/details/7292380> - <https://juejin.im/post/5c04892351882516e70dcc9b> - <http://gityuan.com/2016/01/24/java-classloader/>### 公眾號如果大家想要實時關注我更新的文章以及分享的干貨的話,可以關注我的公眾號。**《Java面試突擊》:** 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本[公眾號](#公眾號)后臺回復 **"Java面試突擊"** 即可免費領取!**Java工程師必備學習資源:** 一些Java工程師常用學習資源[公眾號](#公眾號)后臺回復關鍵字 **“1”** 即可免費無套路獲取。  點擊關注[公眾號](#公眾號)及時獲取筆主最新更新文章,并可免費領取本文檔配套的《Java面試突擊》以及Java工程師必備學習資源。<!-- TOC -->- [類加載過程](#類加載過程)- [加載](#加載)- [驗證](#驗證)- [準備](#準備)- [解析](#解析)- [初始化](#初始化)<!-- /TOC -->> 公眾號JavaGuide 后臺回復關鍵字“1”,免費獲取JavaGuide配套的Java工程師必備學習資源(文末有公眾號二維碼)。# 類加載過程Class 文件需要加載到虛擬機中之后才能運行和使用,那么虛擬機是如何加載這些 Class 文件呢?系統加載 Class 類型的文件主要三步:**加載->連接->初始化**。連接過程又可分為三步:**驗證->準備->解析**。## 加載類加載過程的第一步,主要完成下面3件事情:1. 通過全類名獲取定義此類的二進制字節流 2. 將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構 3. 在內存中生成一個代表該類的 Class 對象,作為方法區這些數據的訪問入口虛擬機規范多上面這3點并不具體,因此是非常靈活的。比如:"通過全類名獲取定義此類的二進制字節流" 并沒有指明具體從哪里獲取、怎樣獲取。比如:比較常見的就是從 ZIP 包中讀取(日后出現的JAR、EAR、WAR格式的基礎)、其他文件生成(典型應用就是JSP)等等。**一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 `loadClass()` 方法)。數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。**類加載器、雙親委派模型也是非常重要的知識點,這部分內容會在后面的文章中單獨介紹到。加載階段和連接階段的部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。## 驗證## 準備**準備階段是正式為類變量分配內存并設置類變量初始值的階段**,這些內存都將在方法區中分配。對于該階段有以下幾點需要注意:1. 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。 2. 這里所設置的初始值"通常情況"下是數據類型默認的零值(如0、0L、null、false等),比如我們定義了`public static int value=111` ,那么 value 變量在準備階段的初始值就是 0 而不是111(初始化階段才會賦值)。特殊情況:比如給 value 變量加上了 fianl 關鍵字`public static final int value=111` ,那么準備階段 value 的值就被賦值為 111。**基本數據類型的零值:**## 解析解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。**直接引用**就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機為每個類都準備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接調用該方法了。通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被調用。綜上,解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量。## 初始化初始化是類加載的最后一步,也是真正執行類中定義的 Java 程序代碼(字節碼),初始化階段是執行類構造器 `<clinit> ()`方法的過程。對于`<clinit>()` 方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因為 `<clinit>()` 方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起死鎖,并且這種死鎖很難被發現。對于初始化階段,虛擬機嚴格規范了有且只有5種情況下,必須對類進行初始化:1. 當遇到 new 、 getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類,讀取一個靜態字段(未被 final 修飾)、或調用一個類的靜態方法時。 2. 使用 `java.lang.reflect` 包的方法對類進行反射調用時 ,如果類沒初始化,需要觸發其初始化。 3. 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。 4. 當虛擬機啟動時,用戶需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類。 5. 當使用 JDK1.7 的動態動態語言時,如果一個 MethodHandle 實例的最后解析結構為 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且這個句柄沒有初始化,則需要先觸發器初始化。**參考**- 《深入理解Java虛擬機》 - 《實戰Java虛擬機》 - <https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html>## 公眾號如果大家想要實時關注我更新的文章以及分享的干貨的話,可以關注我的公眾號。**《Java面試突擊》:** 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本[公眾號](#公眾號)后臺回復 **"Java面試突擊"** 即可免費領取!**Java工程師必備學習資源:** 一些Java工程師常用學習資源[公眾號](#公眾號)后臺回復關鍵字 **“1”** 即可免費無套路獲取。  點擊關注[公眾號](#公眾號)及時獲取筆主最新更新文章,并可免費領取本文檔配套的《Java面試突擊》以及Java工程師必備學習資源。<!-- TOC -->- [類文件結構](#類文件結構)- [一 概述](#一-概述)- [二 Class 文件結構總結](#二-class-文件結構總結)- [2.1 魔數](#21-魔數)- [2.2 Class 文件版本](#22-class-文件版本)- [2.3 常量池](#23-常量池)- [2.4 訪問標志](#24-訪問標志)- [2.5 當前類索引,父類索引與接口索引集合](#25-當前類索引父類索引與接口索引集合)- [2.6 字段表集合](#26-字段表集合)- [2.7 方法表集合](#27-方法表集合)- [2.8 屬性表集合](#28-屬性表集合)- [參考](#參考)<!-- /TOC --># 類文件結構## 一 概述在 Java 中,JVM 可以理解的代碼就叫做`字節碼`(即擴展名為 `.class` 的文件),它不面向任何特定的處理器,只面向虛擬機。Java 語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以 Java 程序運行時比較高效,而且,由于字節碼并不針對一種特定的機器,因此,Java 程序無須重新編譯便可在多種不同操作系統的計算機上運行。Clojure(Lisp 語言的一種方言)、Groovy、Scala 等語言都是運行在 Java 虛擬機之上。下圖展示了不同的語言被不同的編譯器編譯成`.class`文件最終運行在 Java 虛擬機之上。`.class`文件的二進制格式可以使用 [WinHex](https://www.x-ways.net/winhex/) 查看。**可以說`.class`文件是不同的語言在 Java 虛擬機之間的重要橋梁,同時也是支持 Java 跨平臺很重要的一個原因。**## 二 Class 文件結構總結根據 Java 虛擬機規范,類文件由單個 ClassFile 結構組成:```java ClassFile {u4 magic; //Class 文件的標志u2 minor_version;//Class 的小版本號u2 major_version;//Class 的大版本號u2 constant_pool_count;//常量池的數量cp_info constant_pool[constant_pool_count-1];//常量池u2 access_flags;//Class 的訪問標記u2 this_class;//當前類u2 super_class;//父類u2 interfaces_count;//接口u2 interfaces[interfaces_count];//一個類可以實現多個接口u2 fields_count;//Class 文件的字段屬性field_info fields[fields_count];//一個類會可以有個字段u2 methods_count;//Class 文件的方法數量method_info methods[methods_count];//一個類可以有個多個方法u2 attributes_count;//此類的屬性表中的屬性數attribute_info attributes[attributes_count];//屬性表集合 } ```下面詳細介紹一下 Class 文件結構涉及到的一些組件。**Class文件字節碼結構組織示意圖** (之前在網上保存的,非常不錯,原出處不明):### 2.1 魔數```javau4 magic; //Class 文件的標志 ```每個 Class 文件的頭四個字節稱為魔數(Magic Number),它的唯一作用是**確定這個文件是否為一個能被虛擬機接收的 Class 文件**。 程序設計者很多時候都喜歡用一些特殊的數字表示固定的文件類型或者其它特殊的含義。### 2.2 Class 文件版本```javau2 minor_version;//Class 的小版本號u2 major_version;//Class 的大版本號 ```緊接著魔數的四個字節存儲的是 Class 文件的版本號:第五和第六是**次版本號**,第七和第八是**主版本號**。高版本的 Java 虛擬機可以執行低版本編譯器生成的 Class 文件,但是低版本的 Java 虛擬機不能執行高版本編譯器生成的 Class 文件。所以,我們在實際開發的時候要確保開發的的 JDK 版本和生產環境的 JDK 版本保持一致。### 2.3 常量池```javau2 constant_pool_count;//常量池的數量cp_info constant_pool[constant_pool_count-1];//常量池 ```緊接著主次版本號之后的是常量池,常量池的數量是 constant_pool_count-1(**常量池計數器是從1開始計數的,將第0項常量空出來是有特殊考慮的,索引值為0代表“不引用任何一個常量池項”**)。常量池主要存放兩大常量:字面量和符號引用。字面量比較接近于 Java 語言層面的的常量概念,如文本字符串、聲明為 final 的常量值等。而符號引用則屬于編譯原理方面的概念。包括下面三類常量: - 類和接口的全限定名 - 字段的名稱和描述符 - 方法的名稱和描述符常量池中每一項常量都是一個表,這14種表有一個共同的特點:**開始的第一位是一個 u1 類型的標志位 -tag 來標識常量的類型,代表當前這個常量屬于哪種常量類型.**| 類型 | 標志(tag) | 描述 | | :------------------------------: | :---------: | :--------------------: | | CONSTANT_utf8_info | 1 | UTF-8編碼的字符串 | | CONSTANT_Integer_info | 3 | 整形字面量 | | CONSTANT_Float_info | 4 | 浮點型字面量 | | CONSTANT_Long_info | 5 | 長整型字面量 | | CONSTANT_Double_info | 6 | 雙精度浮點型字面量 | | CONSTANT_Class_info | 7 | 類或接口的符號引用 | | CONSTANT_String_info | 8 | 字符串類型字面量 | | CONSTANT_Fieldref_info | 9 | 字段的符號引用 | | CONSTANT_Methodref_info | 10 | 類中方法的符號引用 | | CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 | | CONSTANT_NameAndType_info | 12 | 字段或方法的符號引用 | | CONSTANT_MothodType_info | 16 | 標志方法類型 | | CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | | CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |`.class` 文件可以通過`javap -v class類名` 指令來看一下其常量池中的信息(`javap -v class類名-> temp.txt` :將結果輸出到 temp.txt 文件)。### 2.4 訪問標志在常量池結束之后,緊接著的兩個字節代表訪問標志,這個標志用于識別一些類或者接口層次的訪問信息,包括:這個 Class 是類還是接口,是否為 public 或者 abstract 類型,如果是類的話是否聲明為 final 等等。類訪問和屬性修飾符:我們定義了一個 Employee 類```java package top.snailclimb.bean; public class Employee {... } ```通過`javap -v class類名` 指令來看一下類的訪問標志。### 2.5 當前類索引,父類索引與接口索引集合```javau2 this_class;//當前類u2 super_class;//父類u2 interfaces_count;//接口u2 interfaces[interfaces_count];//一個雷可以實現多個接口 ```**類索引用于確定這個類的全限定名,父類索引用于確定這個類的父類的全限定名,由于 Java 語言的單繼承,所以父類索引只有一個,除了 `java.lang.Object` 之外,所有的 java 類都有父類,因此除了 `java.lang.Object` 外,所有 Java 類的父類索引都不為 0。****接口索引集合用來描述這個類實現了那些接口,這些被實現的接口將按`implents`(如果這個類本身是接口的話則是`extends`) 后的接口順序從左到右排列在接口索引集合中。**### 2.6 字段表集合```javau2 fields_count;//Class 文件的字段的個數field_info fields[fields_count];//一個類會可以有個字段 ```字段表(field info)用于描述接口或類中聲明的變量。字段包括類級變量以及實例變量,但不包括在方法內部聲明的局部變量。**field info(字段表) 的結構:**- **access_flags:** 字段的作用域(`public` ,`private`,`protected`修飾符),是實例變量還是類變量(`static`修飾符),可否被序列化(transient 修飾符),可變性(final),可見性(volatile 修飾符,是否強制從主內存讀寫)。 - **name_index:** 對常量池的引用,表示的字段的名稱; - **descriptor_index:** 對常量池的引用,表示字段和方法的描述符; - **attributes_count:** 一個字段還會擁有一些額外的屬性,attributes_count 存放屬性的個數; - **attributes[attributes_count]:** 存放具體屬性具體內容。上述這些信息中,各個修飾符都是布爾值,要么有某個修飾符,要么沒有,很適合使用標志位來表示。而字段叫什么名字、字段被定義為什么數據類型這些都是無法固定的,只能引用常量池中常量來描述。**字段的 access_flags 的取值:**### 2.7 方法表集合```javau2 methods_count;//Class 文件的方法的數量method_info methods[methods_count];//一個類可以有個多個方法 ```methods_count 表示方法的數量,而 method_info 表示的方法表。Class 文件存儲格式中對方法的描述與對字段的描述幾乎采用了完全一致的方式。方法表的結構如同字段表一樣,依次包括了訪問標志、名稱索引、描述符索引、屬性表集合幾項。 **method_info(方法表的) 結構:****方法表的 access_flag 取值:**注意:因為`volatile`修飾符和`transient`修飾符不可以修飾方法,所以方法表的訪問標志中沒有這兩個對應的標志,但是增加了`synchronized`、`native`、`abstract`等關鍵字修飾方法,所以也就多了這些關鍵字對應的標志。### 2.8 屬性表集合```javau2 attributes_count;//此類的屬性表中的屬性數attribute_info attributes[attributes_count];//屬性表集合 ```在 Class 文件,字段表,方法表中都可以攜帶自己的屬性表集合,以用于描述某些場景專有的信息。與 Class 文件中其它的數據項目要求的順序、長度和內容不同,屬性表集合的限制稍微寬松一些,不再要求各個屬性表具有嚴格的順序,并且只要不與已有的屬性名重復,任何人實現的編譯器都可以向屬性表中寫 入自己定義的屬性信息,Java 虛擬機運行時會忽略掉它不認識的屬性。## 參考- <https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html> - <https://coolshell.cn/articles/9229.html> - <https://blog.csdn.net/luanlouis/article/details/39960815> - 《實戰 Java 虛擬機》## 公眾號如果大家想要實時關注我更新的文章以及分享的干貨的話,可以關注我的公眾號。**《Java面試突擊》:** 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本[公眾號](#公眾號)后臺回復 **"Java面試突擊"** 即可免費領取!**Java工程師必備學習資源:** 一些Java工程師常用學習資源[公眾號](#公眾號)后臺回復關鍵字 **“1”** 即可免費無套路獲取。 ?
?
?
?
?
總結
以上是生活随笔為你收集整理的资料搜集-JAVA系统的梳理知识5-JAVA基础篇JVM的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 全球与中国搅拌站燃烧器市场现状及未来发展
- 下一篇: c# https请求忽略证书验证_c#