Java 內存分配
• 寄存器:程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼。
• 靜態域:static 定義的靜態成員。
• 常量池:編譯時被確定並保存在 .class 文件中的(final)
常量值和一些文本修飾的符號引用(類和接口的全限定名,字段的名稱和描述符,方法和名稱和描述符)。
• 非 RAM 存儲:硬盤等永久存儲空間。
• 堆內存:new 創建的對象和數組,由 Java 虛擬機自動垃圾回收器管理,存取速度慢。
• 棧內存:基本類型的變量和對象的引用變量(堆內存空間的訪問地址),速度快,可以共享,但是大小與生存期必須確定,缺乏靈活性。
串行(serial)收集器和吞吐量(throughput)收集器的區別是什麼?
吞吐量收集器使用並行版本的新生代垃圾收集器,它用於中等規模和大規模數據的應用程序。
而串行收集器對大多數的小應用(在現代處理器上需要大概 100M 左右的內存)就足夠了。
在 Java 中,對象什麼時候可以被垃圾回收?
當對象對當前使用這個對象的應用程序變得不可觸及的時候,這個對象就可以被回收了。
GC 是什麼? 爲什麼要有 GC?
GC 是垃圾收集的意思(GabageCollection),內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會導致程序或
系統的不穩定甚至崩潰,Java 提供的 GC 功能可以自動監測對象是否超過作用域從而達到自動回收內存的目的,Java 語言沒有提
供釋放已分配內存的顯示操作方法。
簡述 Java 垃圾回收機制。
在 Java 中,程序員是不需要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在 JVM 中,有一個垃圾回收線程,它是低優先級的,在正常情況下是不會執行的,只有在虛擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。
如何判斷一個對象是否存活?(或者 GC 對象的判定方法)
判斷一個對象是否存活有兩種方法:
引用計數法
所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就
減一。當一個對象的引用計數器爲零時,說明此對象沒有被引用,也就是“死對象”,將會被垃圾回收.引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象 A 引用對象 B,對象 B 又引用者對象 A,那麼此時 A、B 對象的引用計數器都不爲零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有采用這種算法。
可達性算法(引用鏈法)
該算法的思想是:從一個被稱爲 GC Roots 的對象開始向下搜索,如果一個對象到 GC Roots 沒有任何引用鏈相連時,則說明此對
象不可用。
在 Java 中可以作爲 GC Roots 的對象有以下幾種:
• 虛擬機棧中引用的對象
• 方法區類靜態屬性引用的對象
• 方法區常量池引用的對象
• 本地方法棧 JNI 引用的對象
雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象比不一定會被回收。當一個對象不可達 GC Root
時,這個對象並不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記.如果對象在可達性分析中沒有與 GC Root 的引用鏈,那麼此時就會被第一次標記並且進行一次篩選,篩選的條件是是否有必要執行finalize() 方法。當對象沒有覆蓋 finalize() 方法或者已被虛擬機調用過,那麼就認爲是沒必要的。 如果該對象有必要執行finalize() 方法,那麼這個對象將會放在一個稱爲 F-Queue 的對
隊列中,虛擬機會觸發一個 Finalize() 線程去執行,此線程是低優先級的,並且虛擬機不會承諾一直等待它運行完,這是因爲如果finalize() 執行緩慢或者發生了死鎖,那麼就會造成 F-Queue 隊列一直等待,造成了內存回收系統的崩潰。GC 對處於 F-Queue 中的對象進行第二次被標記,這時,該對象將被移除” 即將回收”集合,等待回收。
垃圾回收的優點和原理。並考慮 2 種回收機制。
Java 語言中一個顯著的特點就是引入了垃圾回收機制,使 C++ 程序員最頭疼的內存管理的問題迎刃而解,它使得 Java 程序員在編寫程序的時候不再需要考慮內存管理。由於有個垃圾回收機制,Java 中的對象不再有“作用域”的概念,只有對象的引用纔有"作用域"。垃圾回收可以有效的防止內存泄露,有效的使用可以使用的內存。垃圾回收器通常是作爲一個單獨的低級別的線程運行,不可預知的情況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清楚和回收,程序員不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。
回收機制有分代複製垃圾回收和標記垃圾回收,增量垃圾回收。
垃圾回收器的基本原理是什麼?垃圾回收器可以馬上回收內存嗎?有什麼辦法主動通知虛擬機進行垃圾回收?
對於 GC 來說,當程序員創建對象時,GC 就開始監控這個對象的地址、大小以及使用情況。通常,GC 採用有向圖的方式記錄和管理堆(heap)中的所有對象。通過這種方式確定哪些對象是”可達的”,哪些對象是”不可達的”。當 GC 確定一些對象爲“不可達”時,GC 就有責任回收這些內存空間。可以。程序員可以手動執行 System.gc(),通知 GC 運行,但是 Java 語言規範並不保證 GC 一定會執行。
System.gc() 和 Runtime.gc() 會做什麼事情?
這兩個方法用來提示 JVM 要進行垃圾回收。但是,立即開始還是延遲進行垃圾回收是取決於 JVM 的。
Java 堆的結構是什麼樣子的?什麼是堆中的永久代(Perm Gen space)?
JVM 的堆是運行時數據區,所有類的實例和數組都是在堆上分配內存。它在 JVM 啓動的時候被創建。對象所佔的堆內存是由自動
內存管理系統也就是垃圾收集器回收。堆內存是由存活和死亡的對象組成的。存活的對象是應用可以訪問的,不會被垃圾回收。死亡的對象是應用不可訪問尚且還沒有被垃圾收集器回收掉的對象。一直到垃圾收集器把這些 對象回收掉之前,他們會一直佔據堆內存空間。
Java 中會存在內存泄漏嗎,請簡單描述。
所謂內存泄露就是指一個不再被程序使用的對象或變量一直被佔據在內存中。Java 中有垃圾回收機制,它可以保證一對象不再被引用的時候,即對象變成了孤兒的時候,對象將自動被垃圾回收器從內存中清除掉。由於 Java 使用有向圖的方式進行垃圾回收管理,可以消除引用循環的問題,例如有兩個對象,相互引用,只要它們和根進程不可達的,那麼 GC 也是可以回收它們的,
下面的代碼可以看到這種情況的內存回收:
import java.io.IOException;public class GarbageTest {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
try {
gcTest();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("has exited gcTest!");
System.in.read();
System.in.read();
System.out.println("out begin gc!");
for(int i=0;i<100;i++){
System.gc();
System.in.read();
System.in.read();
}
}
private static void gcTest() throws IOException {
System.in.read();
System.in.read();
Person p1 = new Person();
System.in.read();
System.in.read();
Person p2 = new Person();
p1.setMate(p2);
p2.setMate(p1);
System.out.println("before exit gctest!");
System.in.read();
System.in.read();
System.gc();
System.out.println("exit gctest!");
}
private static class Person{
byte[] data = new byte[20000000];
Person mate = null;
public void setMate(Person other){
mate = other;
}
}
}
Java 中的內存泄露的情況:
長生命週期的對象持有短生命週期對象的引用就很可能發生內存泄露,儘管短生命週期對象已經不再需要,但是因爲長生命週期對象持有它的引用而導致不能被回收,這就是 Java 中內存泄露的發生場景,通俗地說,就是程序員可能創建了一個對象,以後一直不再使用這個對象,這個對象卻一直被引用,即這個對象無用但是卻無法被垃圾回收器回收的,這就是 java 中可能出現內存泄露的情況,例如,緩存系統,我們加載了一個對象放在緩存中 (例如放在一個全局 map 對象中),然後一直不再使用它,這個對象一直被緩存引用,但卻不再被使用。檢查 Java 中的內存泄露,一定要讓程序將各種分支情況都完整執行到程序結束,然後看某個對象是否被使用過,如果沒有,則才能
判定這個對象屬於內存泄露。如果一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由於內部類持久外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內存泄露。
public class Stack {
private Object[] elements=new Object[10];
private int size = 0;
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if( size == 0) throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity(){
if(elements.length == size){
Object[] oldElements = elements;
elements = new Object[2 * elements.length+1];
System.arraycopy(oldElements,0, elements, 0, size);
}
} }
上面的原理應該很簡單,假如堆棧加了 10 個元素,然後全部彈出來,雖然堆棧是空的,沒有我們要的東西,但是這是個對象是無法回收的,這個才符合了內存泄露的兩個條件:無用,無法回收。但是就是存在這樣的東西也不一定會導致什麼樣的後果,如果這個
堆棧用的比較少,也就浪費了幾個 K 內存而已,反正我們的內存都上 G 了,哪裏會有什麼影響,再說這個東西很快就會被回收的,
有什麼關係。下面看兩個例子。
public class Bad{
public static Stack s=Stack();
static{
s.push(new Object());
s.pop(); //這裏有一個對象發生內存泄露
s.push(new Object()); //上面的對象可以被回收了,等於是自
愈了
} }
因爲是 static,就一直存在到程序退出,但是我們也可以看到它有自愈功能,就是說如果你的 Stack 最多有 100 個對象,那麼最
多也就只有 100 個對象無法被回收其實這個應該很容易理解,Stack 內部持有 100 個引用,最壞的情況就是他們都是無用的,
因爲我們一旦放新的進取,以前的引用自然消失!內存泄露的另外一種情況:當一個對象被存儲進 HashSet 集合中以後,就不能修改這對象中的那些參與計算哈希值的字段了,否則,對象修改後的哈希值與最初存儲進 HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當前引用作爲的參數去 HashSet 集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從HashSet 集合中單獨刪除當前對象,造成內存泄露。
深拷貝和淺拷貝。
簡單來講就是複製、克隆。
Person p=new Person(“張三”);
淺拷貝就是對對象中的數據成員進行簡單賦值,如果存在動態成員或者指針就會報錯。深拷貝就是對對象中存在的動態成員或指針重新開闢內存空間。
finalize() 方法什麼時候被調用?析構函數 (finalization) 的目的是什麼?
垃圾回收器(garbage colector)決定回收某對象時,就會運行該對象的 finalize() 方法 但是在 Java 中很不幸,如果內存總是充
足的,那麼垃圾回收可能永遠不會進行,也就是說 filalize() 可能永遠不被執行,顯然指望它做收尾工作是靠不住的。
那麼finalize() 究竟是做什麼的呢?
它最主要的用途是回收特殊渠道申請的內存。Java 程序有垃圾回收器,所以一般情況下內存問題不用程序員操心。但有一種 JNI(Java Native Interface)調用non-Java 程序(C 或 C++), finalize() 的工作就是回收這部分的內存。
如果對象的引用被置爲 null,垃圾收集器是否會立即釋放對象佔用的內存?
不會,在下一個垃圾回收週期中,這個對象將是可被回收的。
什麼是分佈式垃圾回收(DGC)?它是如何工作的?
DGC 叫做分佈式垃圾回收。RMI 使用 DGC 來做自動垃圾回收。因爲 RMI 包含了跨虛擬機的遠程對象的引用,垃圾回收是很困難的。DGC 使用引用計數算法來給遠程對象提供自動內存管理。
簡述 Java 內存分配與回收策率以及 Minor GC 和 Major GC。
• 對象優先在堆的 Eden 區分配
• 大對象直接進入老年代
• 長期存活的對象將直接進入老年代
當 Eden 區沒有足夠的空間進行分配時,虛擬機會執行一次Minor GC。Minor GC 通常發生在新生代的 Eden 區,在這個區的對象生存期短,往往發生 Gc 的頻率較高,回收速度比較快;
Full GC/Major GC 發生在老年代,一般情況下,觸發老年代 GC 的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之
前進行一次 Minor GC 這樣可以加快老年代的回收速度。
JVM 的永久代中會發生垃圾回收麼?
垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,
會觸發完全垃圾回收(Full GC)。
注:Java 8 中已經移除了永久代,新加了一個叫做元數據區的
native 內存區。
Java 中垃圾收集的方法有哪些?
標記 - 清除:這是垃圾收集算法中最基礎的,根據名字就可以知
道,它的思想就是標記哪些要被回收的對象,然後統一回收。這種
方法很簡單,但是會有兩個主要問題:
1.效率不高,標記和清除的效率都很低;
2.會產生大量不連續的內存碎片,導致以後程序在分配較大的
對象時,由於沒有充足的連續內存而提前觸發一次 GC 動作。
複製算法:
爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,然後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,然後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一般的內存。於是將該算法進行了改進,內存區域不再是按照 1:1 去劃分,而是將內存劃分爲 8:1:1 三部分,較大那份內存交 Eden 區,其餘是兩塊較小的內存區叫 Survior 區。每次都會優先使用 Eden 區,若 Eden 區滿,就將對象複製到第二塊內存區上,然後清除 Eden
區,如果此時存活的對象太多,以至於 Survivor 不夠時,會將這些對象通過分配擔保機制複製到老年代中。(java 堆又分爲新生代和老年代)
標記 - 整理:
該算法主要是爲了解決標記 - 清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生內存碎片了。
分代收集:
現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保。
什麼是類加載器,類加載器有哪些?
實現通過類的權限定名獲取該類的二進制字節流的代碼塊叫做類加載器。
主要有一下四種類加載器:
• 啓動類加載器(Bootstrap ClassLoader)用來加載 Java 核心類庫,無法被 Java 程序直接引用。
• 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
• 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過ClassLoader.getSystemClassLoader() 來獲取它。
• 用戶自定義類加載器,通過繼承 java.lang.ClassLoader 類的方式實現。
類加載器雙親委派模型機制?
當一個類收到了類加載請求時,不會自己先去加載這個類,而是將其委派給父類,由父類去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載。
- 內存模型以及分區,需要詳細到每個區放什麼。
JVM 分爲堆區和棧區,還有方法區,初始化的對象放在堆裏面,引用放在棧裏面,class 類信息常量池(static 常量和 static 變量)等放在方法區
方法區:主要是存儲類信息,常量池(static 常量和 static 變量),編譯後的代碼(字節碼)等數據
堆:初始化的對象,成員變量 (那種非 static 的變量),所有的對象實例和數組都要在堆上分配
棧:棧的結構是棧幀組成的,調用一個方法就壓入一幀,幀上面存儲局部變量表,操作數棧,方法出
本地方法棧:主要爲 Native 方法服務
程序計數器:記錄當前線程執行的行號 - 堆裏面的分區:Eden,survival (from+ to),老年代,各自的特點。
堆裏面分爲新生代和老生代(java8 取消了永久代,採用了 Metaspace),新生代包含 Eden+Survivor 區,survivor 區裏面分爲 from 和 to 區,內存回收時,如果用的是複製算法,從 from 複製到 to,當經過一次或者多次 GC 之後,存活下來的對象會被移動到老年區,當 JVM 內存不夠用的時候,會觸發 Full GC,清理 JVM 老年區當新生區滿了之後會觸發 YGC,先把存活的對象放到其中一個 Survice區,然後進行垃圾清理。因爲如果僅僅清理需要刪除的對象,這樣會導致內存碎片,因此一般會把 Eden 進行完全的清理,然後整理內存。那麼下次 GC 的時候,就會使用下一個 Survive,這樣循環使用。如果有特別大的對象,新生代放不下,就會使用老年代的擔保,直接放到老年代裏面。因爲 JVM 認爲,一般大對象的存活時間一般比較久遠。 - 對象創建方法,對象的內存分配,對象的訪問定位。new 一個對象
- GC 的兩種判定方法:
引用計數法:指的是如果某個地方引用了這個對象就+1,如果失效了就-1,當爲 0 就會回收但是 JVM 沒有用這種方式,因爲無法判定相互循環引用(A 引用 B,B 引用 A)的情況
引用鏈法: 通過一種 GC ROOT 的對象(方法區中靜態變量引用的對象等-static 變量)來判斷,如果有一條鏈能夠到達 GC ROOT 就說明,不能到達 GC ROOT 就說明可以回收
SafePoint 是什麼
比如 GC 的時候必須要等到 Java 線程都進入到 safepoint 的時候 VMThread 才能開始執行 GC,
1.循環的末尾 (防止大循環的時候一直不進入 safepoint,而其他線程在等待它進入safepoint)
2.方法返回前
3.調用方法的 call 之後
4.拋出異常的位置 - GC 的三種收集方法:標記清除、標記整理、複製算法的原理與特點,分別用在什麼地方,如果讓你優化收集方法,有什麼思路?
先標記,標記完畢之後再清除,效率不高,會產生碎片複製算法:分爲 8:1 的 Eden 區和 survivor 區,就是上面談到的 YGC標記整理:標記完畢之後,讓所有存活的對象向一端移動
GC 收集器有哪些?CMS 收集器與 G1 收集器的特點。
並行收集器:串行收集器使用一個單獨的線程進行收集,GC 時服務有停頓時間
串行收集器:次要回收中使用多線程來執行CMS 收集器是基於“標記—清除”算法實現的,經過多次標記纔會被清除
G1 從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的
Minor GC 與 Full GC 分別在什麼時候發生
新生代內存不夠用時候發生 MGC 也叫 YGC,JVM 內存不夠的時候發生 FGC
幾種常用的內存調試工具:jmap、jstack、jconsole、jhat
jstack 可以看當前棧的情況,jmap 查看內存,jhat 進行 dump 堆的信息
簡述 java 內存分配與回收策率以及 Minor GC 和
Major GC
1.對象優先在堆的 Eden 區分配。
2.大對象直接進入老年代.
3.長期存活的對象將直接進入老年代. 當 Eden 區沒有足夠的空間進行分配時,虛擬機會執行一次 Minor GC.Minor Gc 通
常發生在新生代的 Eden 區,在這個區的對象生存期短,往往發生 Gc 的頻率較高,回收速度比較快;Full Gc/Major GC 發生在老年代,一般情況下,觸發老年代 GC的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之前進行一次 Minor GC 這樣可以加快老年代的回收速度。
Java 類加載過程?
Java 類加載需要經歷一下 7 個過程: - 加載
加載是類加載的第一個過程,在這個階段,將完成一下三件事情:
• 通過一個類的全限定名獲取該類的二進制流。
• 將該二進制流中的靜態存儲結構轉化爲方法去運行時數據結
構。
• 在內存中生成該類的 Class 對象,作爲該類的數據訪問入口。 - 驗證
驗證的目的是爲了確保 Class 文件的字節流中的信息不回危害到虛擬機.在該階段主要完成以下四鍾驗證: • 文件格式驗證:驗證字節流是否符合 Class 文件的規範,如主次版本號是否在當前虛擬機範圍內,常量池中的常量是否有不被支持的類型. • 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。
• 字節碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證數據流和控制流的分析,確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。
• 符號引用驗證:這個動作在後面的解析過程中發生,主要是爲了確保解析動作能正確執行。 - 準備
準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一起分配在 Java 堆中。
public static int value=123;//在準備階段 value 初始值爲 0 。在初始化階段纔會變爲 123 。 - 解析
該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之後。 - 初始化
初始化時類加載的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java 程序代碼。 - 使用
- 卸載
描述一下 JVM 加載 Class 文件的原理機制?
Java 語言是一種具有動態性的解釋型語言,類(Class)只有被加載到 JVM 後才能運行。當運行指定程序時,JVM 會將編譯生成
的 .class 文件按照需求和一定的規則加載到內存中,並組織成爲一個完整的 Java 應用程序。這個加載過程是由類加載器完成,具
體來說,就是由 ClassLoader 和它的子類來實現的。類加載器本身也是一個類,其實質是把類文件從硬盤讀取到內存中。
類的加載方式分爲隱式加載和顯示加載。
隱式加載指的是程序在使用 new 等方式創建對象時,會隱式地調用類的加載器把對應的類加載到 JVM 中。顯示加載指的是通過直接調用 class.forName() 方法來把所需的類加載到 JVM 中。任何一個工程項目都是由許多類組成的,當程序啓動時,只把需要的類加載到 JVM 中,其他類只有被使用到的時候纔會被加載,採用這種方法一方面可以加快加載速度,另一方面可以節約程序運行時對內存的開銷。此外,在 Java 語言中,每個類或接口都對應一個 .class 文件,這些文件可以被看成是一個個可以被動態加載的單元,因此當只有部分類被修改時,只需要重新編譯變化的類即可,而不需要重新編譯所有文件,因此加快了編譯速度。在 Java 語言中,類的加載是動態的,它並不會一次性將所有類全部加載後再運行,而是保證程序運行的基礎類(例如基類)完全加載到 JVM 中,至於其他類,則在需要的時候才加載。
類加載的主要步驟:
• 裝載。根據查找路徑找到相應的 class 文件,然後導入。
• 鏈接。鏈接又可分爲 3 個小步:
• 檢查,檢查待加載的 class 文件的正確性。
• 準備,給類中的靜態變量分配存儲空間。
• 解析,將符號引用轉換爲直接引用(這一步可選)
• 初始化。對靜態變量和靜態代碼塊執行初始化工作。