[TOC]
## 寫在前面(常見面試題)
### 基本問題
* **介紹下 Java 內存區域(運行時數據區)**
* **Java 對象的創建過程(五步,建議能默寫出來并且要知道每一步虛擬機做了什么)**
* **對象的訪問定位的兩種方式(句柄和直接指針兩種方式)**
### 拓展問題
* **String類和常量池**
* **8種基本類型的包裝類和常量池**
## 一 概述
對于 Java 程序員來說,在虛擬機自動內存管理機制下,不再需要像C/C++程序開發程序員這樣為內一個 new 操作去寫對應的 delete/free 操作,不容易出現內存泄漏和內存溢出問題。正是因為 Java 程序員把內存控制權利交給 Java 虛擬機,一旦出現內存泄漏和溢出方面的問題,如果不了解虛擬機是怎樣使用內存的,那么排查錯誤將會是一個非常艱巨的任務。
## 二 運行時數據區域
Java 虛擬機在執行 Java 程序的過程中會把它管理的內存劃分成若干個不同的數據區域。JDK. 1.8 和之前的版本略有不同,下面會介紹到。
**JDK 1.8之前:**
[](https://camo.githubusercontent.com/a66819fd82c6adfa69b368edf3c52b1fa9cdc89d/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d332f4a564de8bf90e8a18ce697b6e695b0e68daee58cbae59f9f2e706e67)
**JDK 1.8 :**
[](https://camo.githubusercontent.com/0bcc6c01a919b175827f0d5540aeec115df6c001/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d334a617661e8bf90e8a18ce697b6e695b0e68daee58cbae59f9f4a444b312e382e706e67)
**線程私有的:**
* 程序計數器
* 虛擬機棧
* 本地方法棧
**線程共享的:**
* 堆
* 方法區
* 直接內存(非運行時數據區的一部分)
### 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空間等。**進一步劃分的目的是更好地回收內存,或者更快地分配內存。**
[](https://camo.githubusercontent.com/4012482f49926b35d8557b63952ee605fd259f62/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d33e5a086e7bb93e69e842e706e67)
上圖所示的 eden區、s0區、s1區都屬于新生代,tentired 區屬于老年代。大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收后,如果對象還存活,則會進入 s0 或者 s1,并且對象的年齡還會加 1(Eden區->Survivor 區后對象的初始年齡變為1),當它的年齡增加到一定程度(默認為15歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數`-XX:MaxTenuringThreshold`來設置。
### 2.5 方法區
方法區與 Java 堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做**Non-Heap(非堆)**,目的應該是與 Java 堆區分開來。
方法區也被稱為永久代。很多人都會分不清方法區和永久代的關系,為此我也查閱了文獻。
#### 方法區和永久代的關系
> 《Java虛擬機規范》只是規定了有方法區這么個概念和它的作用,并沒有規定如何去實現它。那么,在不同的 JVM 上方法區的實現肯定是不同的了。**方法區和永久代的關系很像Java中接口和類的關系,類實現了接口,而永久代就是HotSpot虛擬機對虛擬機規范中方法區的一種實現方式。**也就是說,永久代是HotSpot的概念,方法區是Java虛擬機規范中的定義,是一種規范,而永久代是一種實現,一個是標準一個是實現,其他的虛擬機實現并沒有永久帶這一說法。
#### 常用參數
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的最大大小
~~~
與永久代很大的不同就是,如果不指定大小的話,隨著更多類的創建,虛擬機會耗盡所有可用的系統內存。
#### 為什么要將永久代(PermGen)替換為元空間(MetaSpace)呢?
整個永久代有一個 JVM 本身設置固定大小上線,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,并且永遠不會得到java.lang.OutOfMemoryError。你可以使用`-XX:MaxMetaspaceSize`標志設置最大元空間大小,默認值為 unlimited,這意味著它只受系統內存的限制。`-XX:MetaspaceSize`調整標志定義元空間的初始大小如果未指定此標志,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。
當然這只是其中一個原因,還有很多底層的原因,這里就不提了。
### 2.6 運行時常量池
運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用于存放編譯期生成的各種字面量和符號引用)
既然運行時常量池時方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。
**JDK1.7及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。**
[](https://camo.githubusercontent.com/17620721a9f326a235aeec8956949cec03f3f125/687474703a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f31382d392d31342f32363033383433332e6a7067)——圖片來源:[https://blog.csdn.net/wangbiao007/article/details/78545189](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 對象的創建過程,我建議最好是能默寫出來,并且要掌握每一步在做什么。[](https://camo.githubusercontent.com/e99480df412dd718430d78094143a5485c908fa7/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f382f32322f313635363165353961343133353836393f773d39353026683d32373926663d706e6726733d3238353239)
**①類加載檢查:**虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
**②分配內存:**在**類加載檢查**通過后,接下來虛擬機將為新生對象**分配內存**。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務等同于把一塊確定大小的內存從 Java 堆中劃分出來。**分配方式**有**“指針碰撞”**和**“空閑列表”**兩種,**選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定**。
1. 指針碰撞,假設一種“完美情況”:堆內存都是規整的。堆內存被一個指針一分為二。指針的左邊都被塞滿了對象,指針的右變是未使用的區域。每一次有新的對象創建,指針就會向右移動一個對象size的距離。這就被稱為指針碰撞。
2. 空閑列表,JVM維護一個“列表”用于記錄未分配內容的內存空間。
**內存分配的兩種方式:(補充內容,需要掌握)**
選擇以上兩種方式中的哪一種,取決于 Java 堆內存是否規整。而 Java 堆內存是否規整,取決于 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,復制算法內存也是規整的
[](https://camo.githubusercontent.com/65fc0c035f1f70081f4dcdd113c3b2c7aa931a2a/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f382f32322f313635363165353961343061326333643f773d3134323626683d33333326663d706e6726733d3236333436)
**內存分配并發問題(補充內容,需要掌握)**
在創建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發過程中,創建對象是很頻繁的事情,作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:
* **CAS+失敗重試:**CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。**虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。**
* **TLAB:**為每一個線程預先在Eden區分配一塊兒內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大于TLAB中的剩余內存或TLAB的內存已用盡時,再采用上述的CAS進行內存分配
**③初始化零值:**內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
**④設置對象頭:**初始化零值完成之后,**虛擬機要對對象進行必要的設置**,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的 GC 分代年齡等信息。**這些信息存放在對象頭中。**另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。

**⑤執行 init 方法:**在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛開始,`<init>`方法還沒有執行,所有的字段都還為零。所以一般來說,執行 new 指令之后會接著執行`<init>`方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。
### 3.2 對象的內存布局
在 Hotspot 虛擬機中,對象在內存中的布局可以分為3塊區域:**對象頭**、**實例數據**和**對齊填充**。
**Hotspot虛擬機的對象頭包括兩部分信息**,**第一部分用于存儲對象自身的自身運行時數據**(哈希碼、GC分代年齡、鎖狀態標志等等),**另一部分是類型指針**,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。
**實例數據部分是對象真正存儲的有效信息**,也是在程序中所定義的各種類型的字段內容。
**對齊填充部分不是必然存在的,也沒有什么特別的含義,僅僅起占位作用。**因為Hotspot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
### 3.3 對象的訪問定位
建立對象就是為了使用對象,我們的Java程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式有虛擬機實現而定,目前主流的訪問方式有**①使用句柄**和**②直接指針**兩種:
1. **句柄:**如果使用句柄的話,那么Java堆中將會劃分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;[](https://camo.githubusercontent.com/5923998f8408ea936a0416291faf1b2a1d215108/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f342f32372f313633303662393537333936383934363f773d37383626683d33363226663d706e6726733d313039323031)
2. **直接指針:**如果使用直接指針訪問,那么 Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而reference 中存儲的直接就是對象的地址。
[](https://camo.githubusercontent.com/af3d3845b1d5d9c1927e77f79d2cb96ea84090fe/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f342f32372f313633303662613361343162366236353f773d37363626683d33353326663d706e6726733d3939313732)
**這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。**
## 四 重點補充內容
### String 類和常量池
**1 String 對象的兩種創建方式:**
~~~java
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false
~~~
這兩種不同的創建方法是有差別的,第一種方式是在常量池中拿對象,第二種方式是直接在堆內存空間創建一個新的對象。[](https://camo.githubusercontent.com/83e5520e2e0fe8c2b4b1575da9b06d42bbac581f/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f382f32322f313635363165353961353963303837333f773d36393826683d33353526663d706e6726733d3130343439)記住:只要使用new方法,便需要創建新的對象。
**2 String 類型的常量池比較特殊。它的主要使用方法有兩種:**
* 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
* 如果不是用雙引號聲明的 String 對象,可以使用 String 提供的 intern 方法。String.intern() 是一個 Native 方法,它的作用是:如果運行時常量池中已經包含一個等于此 String 對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,則在常量池中創建與此 String 內容相同的字符串,并返回常量池中創建的字符串的引用。
~~~java
String 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對象
~~~
**3 String 字符串拼接**
~~~java
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的對象
String str4 = str1 + str2; //在堆上創建的新的對象
String str5 = "string";//常量池中的對象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
~~~
[](https://camo.githubusercontent.com/34b5b7e57a2e001dd120546ba61720367b0951e9/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f382f32322f313635363165353961346431336639323f773d35393326683d36303326663d706e6726733d3232323635)
盡量避免多個字符串拼接,因為這樣會重新創建對象。如果需要改變字符串的話,可以使用 StringBuilder 或者 StringBuffer。
### String s1 = new String("abc");這句話創建了幾個對象?
**創建了兩個對象。**
**驗證:**
~~~java
String s1 = new String("abc");// 堆內存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 輸出false,因為一個是堆內存,一個是常量池的內存,故兩者是不同的。
System.out.println(s1.equals(s2));// 輸出true
~~~
**結果:**
~~~
false
true
~~~
**解釋:**
先有字符串"abc"放入常量池,然后 new 了一份字符串"abc"放入Java堆(字符串常量"abc"在編譯期就已經確定放入常量池,而 Java 堆上的"abc"是在運行期初始化階段才確定),然后 Java 棧的 str1 指向Java堆上的"abc"。
### 8種基本類型的包裝類和常量池
* **Java 基本類型的包裝類的大部分都實現了常量池技術,即Byte,Short,Integer,Long,Character,Boolean;這5種包裝類默認創建了數值\[-128,127\]的相應類型的緩存數據,但是超出此范圍仍然會去創建新的對象。**
* **兩種浮點數類型的包裝類 Float,Double 并沒有實現常量池技術。**
~~~java
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 輸出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 輸出false
Double 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);這種情況下會創建新的對象。
~~~java
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//輸出false
~~~
**Integer比較更豐富的一個例子:**
~~~java
Integer 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](https://docs.oracle.com/javase/specs/index.html)
* [http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/](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://dzone.com/articles/jvm-permgen-%E2%80%93-where-art-thou)
* [https://stackoverflow.com/questions/9095748/method-area-and-permgen](https://stackoverflow.com/questions/9095748/method-area-and-permgen)
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊