1 記憶體結構 1、簡述一下JVM的記憶體結構?(高頻) JVM在執行Java程式時,會把它管理的記憶體劃分為若幹個的區域,每個區域都有自己的用途和創建銷毀時間。如下圖所示,可以分為兩大部分,線程私有區和共用區。 線程私有區: ① 程式計數器 作用:是一塊較小的記憶體空間,可以理解為是當前線程所執行程式的字 ...
1 記憶體結構
1、簡述一下JVM的記憶體結構?(高頻)
JVM在執行Java程式時,會把它管理的記憶體劃分為若幹個的區域,每個區域都有自己的用途和創建銷毀時間。如下圖所示,可以分為兩大部分,線程私有區和共用區。
線程私有區:
① 程式計數器
- 作用:是一塊較小的記憶體空間,可以理解為是當前線程所執行程式的位元組碼文件的行號指示器,存儲的是當前線程所執行的行號
- 特點:線程私有 ,唯一一個不會出現記憶體溢出的記憶體空間
② 虛擬機棧
- 作用:管理JAVA方法執行的記憶體模型。每個方法執行時都會創建一個棧楨來存儲方法中變數的變數表、操作數棧、動態鏈接方法、返回值、返回地址等信息。棧的大小決定了方法調用的可達深度(遞歸多少層次,或嵌套調用多少層其他方法,-Xss參數可以設置虛擬機棧大小)
-
特點:
1、線程私有
2、局部變數表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)以及對象引用(reference 類型)
3、棧太小或者方法調用過深,都將拋出StackOverflowError異常
-
測試代碼
public class StackDemo02 {
// 記錄調用了多少次出現了棧記憶體溢出
private static int count = 0 ;
// 入口方法
public static void main(String[] args) {
try {
show() ;
}catch (Throwable e) {
e.printStackTrace();
}
System.out.println("show方法被調用了:" + count + "次");
}
// 測試方法
public static void show() {
count++ ;
System.out.println("show方法執行了.....");
show();
}
}
配置虛擬機參數-Xss可以指定棧記憶體大小;例如:-Xss180k
棧記憶體的預設值問題:
The default value depends on the platform:
* Linux/x64 (64-bit): 1024 KB
* macOS (64-bit): 1024 KB
* Oracle Solaris/x64 (64-bit): 1024 KB
* Windows: The default value depends on virtual memory
③ 本地方法棧:與虛擬機棧作用相似。但它不是為Java方法服務的,而是本地方法(C語言)。由於規範對這塊沒有強制要求,不同虛擬機實現方法不同。
線程共用區:
① 堆記憶體
- 作用:是Java記憶體區域中一塊用來存放對象實例的區域,新創建的對象,數組都使用堆記憶體;【從Java7開始,常量池也會使用堆記憶體】
-------------------------------------------------------- |
Java 堆從GC的角度還可以細分為: 新生代( Eden區 、From Survivor區和 To Survivor區 )和老年代。
-
特點:
1、被線程共用,因此需要考慮線程安全問題
2、會產生記憶體溢出問題
-
測試代碼:
public class HeapDemo01 {
public static void main(String[] args) {
// 定義一個變數
int count = 0 ;
// 創建一個ArrayList對象
ArrayList arrayList = new ArrayList() ;
try {
while(true) {
arrayList.add(new Object()) ;
count++ ;
}
}catch (Throwable a) {
a.printStackTrace();
// 輸出程式執行的次數
System.out.println("總共執行了:" + count + "次");
}
}
}
- 虛擬機參數:
-Xms 設置最小堆記憶體大小(不能小於1024K); -Xms 堆記憶體初始大小,可以通過jmap工具進行查看
-Xmx 設置最大堆記憶體大小(不能小於1024K); -Xmx 堆記憶體最大值,可以通過jmap工具進行查看
例如:-Xms1024K -Xmx2048K
註意:
② 方法區
-
作用:它用於存儲已被虛擬機載入的類信息、常量、靜態變數、即時編譯器編譯後的代碼等數據
-
特點:
1、方法區是一塊線程共用的記憶體區域
2、方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出記憶體溢出的錯誤
3、jdk1.6和jdk1.7方法區也常常被稱之為永久區(永久代),大小一般都是幾百兆;
4、jdk1.8已經將方法區取消,替代的是元數據區(元空間),如果不指定大小,預設情況下,虛擬機會耗盡可用系統記憶體
5、jdk7以後就將方法區中的常量池移動至堆記憶體
變化的原因:
1、提高記憶體的回收效率(方法區記憶體的回收效率遠遠低於堆記憶體,因為方法去中存儲的都是類信息,靜態變數...這些信息不能被輕易回收)
2、字元串常量池在方法區,那麼很容易產生記憶體溢出(因為方法區的垃圾回收效率比較低);
- 測試代碼
/**
jdk1.8的元數據區可以使用參數-XX:MaxMetaspaceSzie設定大小
* 演示元空間記憶體溢出
* -XX:-UseCompressedClassPointers -XX:MaxMetaspaceSize=10m
UseCompressedClassPointers使用指針壓縮,如果不使用這個參數可能會出現: Compressed class space記憶體溢出
*/
public class MaxMetaspaceDemo extends ClassLoader { // 當前這個類就是一個類載入器
public static void main(String[] args) {
// 定義變數,記錄程式產生類的個數
int j = 0;
try {
MaxMetaspaceDemo test = new MaxMetaspaceDemo();
for (int i = 0; i < 10000; i++, j++) {
// 位元組碼寫入器
ClassWriter cw = new ClassWriter(0);
// 定義一個類版本為Opcodes.V1_1,它的訪問域為public,名稱為Class{i},父類為java.lang.Object,不實現任何介面
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
// 載入該類
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
2、堆和棧的區別?(高頻)
① 功能不同:棧記憶體用來存儲局部變數和方法調用,而堆記憶體用來存儲Java中的對象。無論是成員變數,局部變數,還是類變數,它們指向的對象都存儲在堆記憶體中。
② 共用性不同:棧記憶體是線程私有的。堆記憶體是所有線程共有的。
③ 異常錯誤不同:如果棧記憶體或者堆記憶體不足都會拋出異常。棧空間不足:java.lang.StackOverFlowError。堆空間不足:
java.lang.OutOfMemoryError。
④ 空間大小:棧的空間大小遠遠小於堆的。
3、怎麼獲取Java程式使用的記憶體?堆使用的百分比?
可以通過java.lang.Runtime類中與記憶體相關方法來獲取剩餘的記憶體,總記憶體及最大堆記憶體。通過這些方法你也可以獲取到堆使用的百分比及堆記憶體的剩餘空間。
1、Runtime.freeMemory() 方法返回剩餘空間的位元組數
2、Runtime.totalMemory()方法總記憶體的位元組數
4、棧幀都有哪些數據?
棧幀包含:局部變數表、操作數棧、動態連接、返回值、返回地址等。
5、如何啟動系統的時候設置jvm的啟動參數?
其實都很簡單,比如說採用"java -jar"的方式啟動一個jar包裡面的系統,那麼就可以才用類似下麵的格式:
2 垃圾回收
6、如何判斷一個對象是否為垃圾?(高頻)
兩種演算法:
① 引用計數法:堆中每個對象實例都有一個引用計數。當一個對象被創建時,且將該對象實例分配給一個變數,該變數計數設置為1。當任何其它變數被賦值為這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命周期或者被設置為一個新值時,對象實例的引用計數器減1。任何引用計數器為0的對象實例可以被當作垃圾收集。
特點:簡單、無法解決迴圈引用問題
定義學生類:
public class Student {
// 定義成員變數
public Object instance ;
}
編寫測試類:
/*
jvm參數:-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-verbose:gc -XX:+PrintGCDetails:列印gc日誌信息
-XX:+PrintGCTimeStamps: 列印gc日誌的時間戳
*/
public class ReferenceCountGcDemo {
public static void main(String[] args) {
// 創建Student對象
Student a = new Student() ;
Student b = new Student() ;
// 進行迴圈引用
a.instance = b ;
b.instance = a ;
// 將a對象和b對象設置為null
a = null ;
b = null ;
// 調用System.gc進行垃圾回收
System.gc(); // 如果沒有觸發垃圾回收說明Hotspot的jvm使用的就是引用計數法來判斷對象是否為垃圾
}
}
控制台輸出gc日誌:
0.076: [GC (System.gc()) [PSYoungGen: 7802K->856K(151552K)] 7802K->864K(498688K), 0.0008493 secs] [Times: user=0.17 sys=0.02, real=0.00 secs]
0.077: [Full GC (System.gc()) [PSYoungGen: 856K->0K(151552K)] [ParOldGen: 8K->620K(347136K)] 864K->620K(498688K), [Metaspace: 3356K->3356K(1056768K)], 0.0044768 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 151552K, used 3901K [0x0000000716c00000, 0x0000000721500000, 0x00000007c0000000)
eden space 130048K, 3% used [0x0000000716c00000,0x0000000716fcf748,0x000000071eb00000)
from space 21504K, 0% used [0x000000071eb00000,0x000000071eb00000,0x0000000720000000)
to space 21504K, 0% used [0x0000000720000000,0x0000000720000000,0x0000000721500000)
ParOldGen total 347136K, used 620K [0x00000005c4400000, 0x00000005d9700000, 0x0000000716c00000)
object space 347136K, 0% used [0x00000005c4400000,0x00000005c449b318,0x00000005d9700000)
Metaspace used 3365K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 370K, capacity 388K, committed 512K, reserved 1048576K
① 0.076: 代表gc發生的時間,從jvm啟動以來經過的秒數
② [GC和[Full Gc: 說明這次垃圾收集器的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有"Full",說明此次GC發生了stop-the-world。System.gc()是說明顯示的調用了 System.gc方法進行垃圾回收
③ [PSYoungGen:表示GC發生的區域, 不同的垃圾收集器展示的區功能變數名稱稱不一樣,PSYoungGen表示的是新生代,這裡預設使用的是Parallel Scavenge收集器 (-XX:+UseSerialGC)
④ 7802K->856K(151552K):GC前該區域已使用容量 -> GC後該區域已使用容量(該區域的總容量)
⑤ 7802K->864K(498688K):GC前Java堆已使用容量 -> GC後Java堆已使用容量(Java堆總容量)
⑥ 0.0008493 secs:該區域GC所占用的時間
⑦ [Times: user=0.17 sys=0.02, real=0.00 secs]: 分別表示用戶態消耗的CPU時間、內核態消耗的CPU時間和操作從開始到結束所經過的牆鐘時間(牆鐘時間包括非運算的等待耗時)。多線程操作會疊加這些CPU時間,所以user、sys時間超過real時間是完全正常的。
② 可達性分析演算法 : 可達性分析演算法又叫做跟搜索法,就是通過一系列的稱之為"GC Roots"的對象作為起始點,從這些節點開始向下搜索,搜索走過的
路徑被稱為(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。
(似於葡萄串);
7、可達性演算法中,哪些對象可作為GC Roots對象?(高頻)
可以作為GC ROOTS對象的情況:
1、虛擬機棧中引用的對象
2、方法區靜態成員引用的對象
3、方法區常量引用對象
4、本地方法棧引用的對象
8、Java中都有哪些引用類型?(高頻)
① 強引用
Java中預設聲明的就是強引用,比如:
Object obj = new Object(); //只要obj還指向Object對象,Object對象就不會被回收
obj = null; //手動置null
只要強引用存在,垃圾回收器將永遠不會回收被引用的對象,哪怕記憶體不足時,JVM也會直接拋出OutOfMemoryError,不會去回收。如果想中斷強引用與對象之間的聯繫,可以顯示的將強引用賦值為null,這樣一來,JVM就可以適時的回收對象了
示例:
/**
* JVM參數:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
*/
public class StrongReferenceDemo01 {
private static List<Object> list = new ArrayList<Object>() ;
public static void main(String[] args) {
// 創建對象
for(int x = 0 ; x < 10 ; x++) {
byte[] buff = new byte[1024 * 1024 * 1];
list.add(buff);
}
}
}
② 軟引用
軟引用是用來描述一些非必需但仍有用的對象。在記憶體足夠的時候,軟引用對象不會被回收,只有在記憶體不足時,系統則會回收軟引用對象,如果回收了軟引用對象之後仍然沒有足夠的記憶體,才會拋出記憶體溢出異常。這種特性常常被用來實現緩存技術,比如網頁緩存,圖片緩存等。
在 JDK1.2 之後,用java.lang.ref.SoftReference類來表示軟引用。
示例代碼:
/**
* JVM參數:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
*/
public class SoftReferenceDemo01 {
private static List<Object> list = new ArrayList<>();
public static void main(String[] args) {
// 創建數組對象
for(int x = 0 ; x < 10 ; x++) {
SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024 * 1024 * 1]) ;
list.add(softReference) ;
}
System.gc(); // 主動通知垃圾回收器進行垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((SoftReference) list.get(i)).get();
System.out.println(obj);
}
}
}
我們發現無論迴圈創建多少個軟引用對象,列印結果總是有一些為null,這裡就說明瞭在記憶體不足的情況下,軟引用將會被自動回收。
③ 弱引用
弱引用的引用強度比軟引用要更弱一些,無論記憶體是否足夠,只要 JVM 開始進行垃圾回收,那些被弱引用關聯的對象都會被回收。在 JDK1.2之後,用
java.lang.ref.WeakReference來表示弱引用。
示例代碼:
/**
* JVM參數:-verbose:gc -XX:+PrintGCDetails -Xms10M -Xmx10M -Xmn5M
*/
public class WeakReferenceDemo01 {
private static List<Object> list = new ArrayList<>();
public static void main(String[] args) {
// 創建數組對象
for(int x = 0 ; x < 10 ; x++) {
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024 * 1024 * 1]) ;
list.add(weakReference) ;
}
System.gc(); // 主動通知垃圾回收器進行垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((WeakReference) list.get(i)).get();
System.out.println(obj);
}
}
}
④ 虛引用
虛引用是最弱的一種引用關係,如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,它隨時可能會被回收,在 JDK1.2 之後,用PhantomReference 類來表示,通過查看這個類的源碼,發現它只有一個構造函數和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是說將永遠無法通過虛引用來獲取對象,虛引用必須要和 ReferenceQueue 引用隊列一起使用。
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
/**
* Creates a new phantom reference that refers to the given object and
* is registered with the given queue.
*
* <p> It is possible to create a phantom reference with a <tt>null</tt>
* queue, but such a reference is completely useless: Its <tt>get</tt>
* method will always return null and, since it does not have a queue, it
* will never be enqueued.
*
* @param referent the object the new phantom reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
特點:
1、每次垃圾回收時都會被回收,主要用於監測對象是否已經從記憶體中刪除
2、虛引用必須和引用隊列關聯使用, 當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用隊列中
3、程式可以通過判斷引用隊列中是否已經加入了虛引用,來瞭解被引用的對象是否將要被垃圾回收。如果程式發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的記憶體被回收之前採取必要的行動
示例代碼:
public class PhantomReferenceDemo {
public static void main(String[] args) throws InterruptedException {
// 創建一個引用隊列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
// 創建一個虛引用,指向一個Object對象
PhantomReference<Object> phantomReference = new PhantomReference<Object>(new Object(), referenceQueue);
// 主動通知垃圾回收器進行垃圾回收
System.gc();
// 從引用隊列中獲取元素, 該方法是阻塞方法
System.out.println(referenceQueue.remove());
}
}
9、常見的垃圾回收演算法都有哪些?(高頻)
① 標記清除
執行過程:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。
優點:速度比較快
缺點:會產生記憶體碎片,碎片過多,仍會使得連續空間少
② 標記整理
執行過程:首先標記出所有需要回收的對象,在標記完成後統一進行整理,整理是指存活對象向一端移動來減少記憶體碎片,相對效率較低
優點:無記憶體碎片
缺點:效率較低
③ 複製演算法
執行過程:開闢兩份大小相等空間,一份空間始終空著,垃圾回收時,將存活對象拷貝進入空閑空間;
優點:無記憶體碎片
缺點:占用空間多
註意:如果有很多對象的存活率較高,這時我們採用複製演算法,那麼效率就比較低;
④ 分代回收
概述:根據對象存活周期的不同,將對象劃分為幾塊,比如Java的堆記憶體,分為新生代和老年代,然後根據各個年代的特點採用最合適的演算法;
新生代對象的存活的時間都比較短,因此使用的是【複製演算法】;而老年代對象存活的時間比較長那麼採用的就是【標記清除】或者【標記整理】;
10、簡述Java垃圾回收機制?有什麼辦法主動通知虛擬機進行垃圾回收?
在Java中,程式員是不需要顯示的去釋放一個對象的記憶體的,而是由虛擬機自行執行。在JVM中,有一個垃圾回收線程,它是低優先順序的,在正常情況下是不會執行的,只有在虛擬機空閑或者當前堆記憶體不足時,才會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。程式員可以手動執行System.gc(),通知GC運行,但是Java語言規範並不保證GC一定會執行。
3 對象分配
11、對象在記憶體中是如何進行分配的?(高頻)
① 對象優先在Eden分配:對象優先在『伊甸園』分配,當『伊甸園』沒有足夠的空間時,觸發 'Minor GC'(小範圍的GC)
情況一:伊甸園的記憶體空間足夠,不會發生'Minor GC'
情況二:伊甸園的空間不夠了
垃圾回收線程啟動,進行垃圾回收,此時會觸發"stop the world"(停止所有用戶線程),Eden區中所有存活的對象都會被覆制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡最多到一定值(最大值是15,對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲)(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被覆制到“To”區域。
"From"和"To"會交換他們的角色,下一次垃圾回收的時候也是從Eden將存活的對象複製到TO區
Minor GC會一直重覆這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有對象移動到年老代中。
案例演示:
jvm參數設置:
-XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gc.log -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:+UseSerialGC 是指使用 Serial + SerialOld 回收器組合
-XX:+PrintGCDetails -verbose:gc 是指列印 GC 詳細信息
-XX:+PrintGCTimeStamps 列印gc日誌的時間戳
-Xloggc:./gc.log 將gc日誌輸出到一個日誌文件中
-Xms20M -Xmx20M -Xmn10M 是指分配給JVM的最小,最大以及新生代記憶體
-XX:SurvivorRatio=8 是指『伊甸園』與『幸存區 From』和『幸存區 To』比例為 8:1:1
定義記憶體大小變數
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _4MB = 4 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
案例1:沒有創建數組對象,看參數運行情況
案例2:創建一個4M的數組,查看記憶體分配情況
// 創建一個4M大小的數組
byte[] bytes = new byte[_4MB] ;
Heap
def new generation total 9216K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 100% used [0x00000000fec00000, 0x00000000ff400000, 0x00000000ff400000) // 在伊甸園中創建對象
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3444K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
沒有觸發GC操作,對象直接在Eden分配;
案例3:創建一個7M的數組,查看記憶體分配情況
// 創建一個7M大小的數組
byte[] bytes1 = new byte[_7MB] ;
-- 觸發垃圾回收
[GC (Allocation Failure) [DefNew: 2004K->647K(9216K), 0.0023439 secs] 2004K->647K(19456K), 0.0024142 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 7897K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 88% used [0x00000000fec00000, 0x00000000ff314930, 0x00000000ff400000)
from space 1024K, 63% used [0x00000000ff500000, 0x00000000ff5a1e58, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3446K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
由於程式在啟動的時候jdk內部還會存在一些對象的創建,因此當我們分配了一個7M的記憶體空間,eden記憶體不足,因此發生了一次Minor GC!並且將存活下的對象最終存儲到from區中。
案例4: 在案例3的基礎上,在分配一個512KB的數組記憶體空間
byte[] bytes1 = new byte[_7MB] ;
byte[] bytes2 = new byte[_512KB] ;
[GC (Allocation Failure) [DefNew: 2005K->623K(9216K), 0.0015235 secs] 2005K->623K(19456K), 0.0015799 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 8713K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 98% used [0x00000000fec00000, 0x00000000ff3e6820, 0x00000000ff400000)
from space 1024K, 60% used [0x00000000ff500000, 0x00000000ff59bdb8, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3444K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
觸發一次GC操作!並且將存活下的對象最終存儲到from區中,第二次分配_512KB大小的記憶體空間的時候,直接在伊甸園分配即可。
案例5: 在4的基礎上在分配一個512KB的數組記憶體空間
byte[] bytes1 = new byte[_7MB] ;
byte[] bytes2 = new byte[_512KB] ;
byte[] bytes3 = new byte[_512KB] ;
[GC (Allocation Failure) [DefNew: 2004K->620K(9216K), 0.0018706 secs] 2004K->620K(19456K), 0.0019275 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 8628K->539K(9216K), 0.0063389 secs] 8628K->8323K(19456K), 0.0063773 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
Heap
def new generation total 9216K, used 1133K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 7% used [0x00000000fec00000, 0x00000000fec94930, 0x00000000ff400000)
from space 1024K, 52% used [0x00000000ff400000, 0x00000000ff486de0, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7784K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 76% used [0x00000000ff600000, 0x00000000ffd9a040, 0x00000000ffd9a200, 0x0000000100000000)
Metaspace used 3443K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
觸發了2次垃圾回收!並且將from區中存活的對象存儲到老年代!
② 大對象直接晉升至老年代
當對象太大,伊甸園包括幸存區都存放不下時,這時候老年代的連續空間足夠,此對象會直接晉升至老年代,不會發生 GC
案例演示:
案例1:直接分配一個8M的記憶體空間
byte[] bytes1 = new byte[_8MB] ;
伊甸園總大小隻有 8 MB,但新分配的對象大小已經是 8MB,而幸存區都僅有 1MB,也無法容納這個對象
Heap
def new generation total 9216K, used 2169K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee1e560, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3443K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
可以看到結果並沒有發生 GC,大對象直接被放入了老年代「tenured generation total 10240K, used 8192K」
案例演示2:老年代連續空間不足,觸發 Full GC
byte[] bytes1 = new byte[_8MB] ;
byte[] bytes2 = new byte[_8MB] ;
第一個 8MB 直接進入老年代,第二個 8MB 對象在分配時發現老年代空間不足,只好嘗試先進行一次 Minor GC,結果發現新生代沒有連續空間,只好觸發一次 Full GC,最後發現老年代也沒有連續空間,這時出現 OutOfMemoryError
[GC (Allocation Failure) [DefNew: 2004K->647K(9216K), 0.0022693 secs][Tenured: 8192K->8838K(10240K), 0.0452151 secs] 10197K->8838K(19456K), [Metaspace: 3438K->3438K(1056768K)], 0.0504669 secs] [Times: user=0.00 sys=0.00, real=0.05 secs]
[Full GC (Allocation Failure) [TenuredException in thread "main" : 8838K->8820K(10240K), 0.0027463 secs] 8838K->8820K(19456K), [Metaspace: 3438K->3438K(1056768K)], 0.0027877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
java.lang.OutOfMemoryError: Java heap space
at com.itheima.jvm.gc.ObjectMemoryDemo.main(ObjectMemoryDemo.java:14)
Heap
def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8820K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffe9d220, 0x00000000ffe9d400, 0x0000000100000000)
Metaspace used 3470K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 379K, capacity 388K, committed 512K, reserved 1048576K
12、對象是怎麼從年輕代進入老年代的?
存在3種情況:
1、如果對象夠老,會通過提升(Promotion)進入老年代,這一般是根據對象的年齡進行判斷的。
2、動態對象年齡判定。有的垃圾回收演算法,比如G1,並不要求age必須達到15才能晉升到老年代,它會使用一些動態的計算方法。
3、超出某個大小的對象將直接在老年代分配。不過這個值預設為0,意思是全部首選Eden區進行分配。
13、簡單描述一下(分代)垃圾回收的過程?(高頻)
分代回收器有兩個分區:老生代和新生代,新生代預設的空間占比總空間的 1/3,老生代的預設占比是2/3。
新生代使用的是複製演算法,新生代里有3個分區:Eden、To Survivor、From Survivor,它們的預設占比是 8:1:1,它的執行流程如下:
當年輕代中的Eden區分配滿的時候,就會觸發年輕代的GC(Minor GC)。具體過程如下:
1、在Eden區執行了第一次GC之後,存活的對象會被移動到其中一個Survivor分區(以下簡稱to)
2、From區中的對象根據對象的年齡值決定去向,達到閾值15移動到老年代,沒有達到複製到to區域(複製演算法)
3、在把Eden和to區中的對象清空掉
14、JVM的永久代中會發生垃圾回收麽?
永久代會觸發垃圾回收的,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。
註:Java 8 中已經移除了永久代,新加了一個叫做元數據區(Metaspace)的記憶體區。
4 垃圾收集器
15、常見的垃圾收集器都有哪些?(高頻)
常見的垃圾收集器如下所示:
不同的垃圾收集器,作用的堆記憶體空間是不一樣的;上面的 serial , parnew , Paraller Scavenge 是新生代的垃圾回收器;CMS , Serial Old , Paralle Old是老年代的垃圾收集器 , G1垃圾收集器可以作用於新生代和老年代; 連線表示垃圾收集器可以搭配使用;
① Serial
特點:
-
Serial是一個單線程的垃圾收集器
-
"Stop The World",它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。在用戶不可見的情況下把用戶正常工作的線程全部停掉。
應用場景: -
使用場景:多用於桌面應用,Client端的垃圾回收器
-
桌面應用記憶體小,進行垃圾回收的時間比較短,只要不頻繁發生停頓就可以接受
Serial Old收集器是Serial的老年代版本和Serial一樣是單線程,使用的演算法是"標記-整理"
② ParNew
概述: ParNew 收集器其實就是 Serial 收集器的多線程版本
特點:
1、會觸發stop the world
2、多線程方式進行垃圾回收
應用場景:它卻是許多運行在 Server 模式下的虛擬機中首選的新生代收集器
註意:如果是單核cpu即使使用該垃圾回收器也無法提高執行效率
③ Parallel Scavenge
概述:Parallel Scavenge 收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多線程收集器
特點:由於與吞吐量關係密切,Parallel Scavenge 收集器也經常稱為“吞吐量優先”收集器
所謂吞吐量就是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99%應用場景: 高吞吐量則可以高效率地利用 CPU 時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多交互的任務。
Parallel old收集器Parallel Scavenge收集器的老年代版本,使用多線程+標記整理演算法
④ CMS(重點)
概述:CMS (Concurrent Mark Sweep)收集器是-種以獲取最短回收停頓時間為目標的收集器。
特點:
-
CMS 收集器是基於“標記-清除”演算法實現的
-
目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。
步驟流程:
- 初始標記(CMS initial mark) -------- 標記一下 GC Roots 能直接關聯到的對象,速度很快(stop the world)
- 併發標記(CMS concurrent mark) -------- 對初始標記標記過的對象,進行trace(進行追蹤,得到所有關聯的對象,進行標記)
- 重新標記(CMS remark) -------- 為了修正併發標記期間因用戶程式導致標記產生變動的標記記錄(stop the world)
- 併發清除(CMS concurrent sweep)
缺點:會產生垃圾碎片
⑤ G1
概述: G1是一個分代的,並行與併發的"標記-整理"垃圾回收器。 它的設計目標是為了適應現在不斷擴大的記憶體和不斷增加的處理器數量,進一步降低暫停時間(pause time),同時兼顧良好的吞吐量。
相比於CMS:
-
G1垃圾回收器使用的是"標記-整理",因此其回收得到的空間是連續的。
-
G1回收器的記憶體與CMS回收器要求的記憶體模型有極大的不同。G1將記憶體劃分一個個固定大小的region,每個region可以是年輕代、老年代的一個。記憶體的回收是以region作為基本單位的;
16、你都用過G1垃圾回收器的哪幾個重要參數?
① -XX:MaxGCPauseMillis
暫停時間,預設值200ms。這是一個軟性目標,G1會儘量達成,如果達不成,會逐漸做自我調整。
② -XX:G1HeapRegionSize
Region大小,若未指定則預設最多生成2048塊,每塊的大小需要為2的冪次方,如1,2,4,8,16,32,最大值為32M。
③ -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent
新生代比例有兩個數值指定,下限:-XX:G1NewSizePercent,預設值5%,上限:-XX:G1MaxNewSizePercent,預設值60%。
17、串列(serial)收集器和吞吐量(throughput)收集器的應用場景?
吞吐量收集器使用並行版本的新生代垃圾收集器,它用於中等規模和大規模數據的應用程式。 而串列收集器對大多數的小應用(在現代處理器上需要大概100M 左右的記憶體)就足夠了。
18、生產上如何配置垃圾收集器的?
1、首先是記憶體大小問題,基本上每一個記憶體區域我都會設置一個上限,來避免溢出問題,比如元空間。通常,堆空間我會設置成操作系統的2/3(這是想給其他進程和操作系統預留一些時間),超過8GB的堆優先選用G1。
2、接下來,我會對JVM進行初步優化。比如根據老年代的對象提升速度,來調整年輕代和老年代之間的比例。
3、再接下來,就是專項優化,主要判斷的依據就是系統容量、訪問延遲、吞吐量等。我們的服務是高併發的,所以對STW的時間非常敏感。我會通過記錄詳細的GC日誌,來找到這個瓶頸點,借用gceasy(重點)https://gceasy.io/這樣的日誌分析工具,很容易定位到問題。之所以選擇採用工具,是因為gc日誌看起來實在是太麻煩了,gceasy號稱是AI學習分析問題,可視化做的較好。
5 類載入器
19、什麼是類載入器,類載入器有哪些?(高頻)
類載入器的作用:負載將的class文件載入到java虛擬機中,併為之創建一個Class對象
從Java虛擬機的角度來講,只存在如下兩種不同的類載入器:
- 啟動類載入器(Bootstrap ClassLoader), 這個類載入器使用C++語言實現,是虛擬機自身的一部分
- 其他類載入器,這些類載入器都由Java語言實現,獨立於虛擬機外部,並且全部都繼承自抽象類(java.lang.ClassLoader)
從Java開發人員的角度來講,類載入器還可以劃分的更細緻一下,絕大部分Java程式都會使用到以下3種系統提供的類載入器:
-
啟動類載入器(Bootstrap class loader):它是虛擬機的內置類載入器,通過表示為null
-
平臺類載入器(Platform class loader) :它是平臺類載入器; 負責載入JDK中一些特殊的模塊;
-
系統類載入器(System class loader) :它也被稱為應用程式類載入器, 它負責載入用戶類路徑上所指定的類庫,一般情況下這個就是程式中默
認的類載入器
20、Java的雙親委托機制是什麼?(高頻)
概述
我們的應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,還可以加入自定義的類載入器。這些類載入器之間的層次關係一般會如下圖所示:
上圖所展示的類載入器之間的這種層次關係,就稱之為類載入器的雙親委派模型。雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應該有自己的父類載入器。這裡的類載入器的父子關係不是真正物理意義上的繼承,而是邏輯上的繼承。
工作過程
雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己嘗試載入這個類,而是把這請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳說到頂層的啟動類載入器中,只有當父類載入器返回自己無法完成這個載入請求(它的搜索返回中沒有找到所需的類)時,子類載入器才會嘗試自己去載入。
6 性能調優
21、調優命令有哪些?
1、jps,JVM Process Status Tool顯示指定系統內所有的HotSpot虛擬機進程。
2、jstat,JVM statistics Monitoring是用於監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、記憶體、垃圾收集、JIT編譯等運
行數據。
查詢幫助文檔:jstat -options
3、jmap,JVM Memory Map命令用於查看堆記憶體的分配情況以及生成heap dump文件
查詢幫助文檔:jmap -h
示例1:jmap -heap 33193 查詢堆記憶體的分配情況
示例2:jmap -dump:format=b,file=thread-cup.log 33193
4、jhat,JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML伺服器,生成dump的
分析結果後,可以在瀏覽器中查看
查詢幫助文檔:
jhat -h
示例:jhat -J-Xmx512M thread-cup.log
5、jstack,用於生成java虛擬機當前時刻的線程快照。
查看幫助文檔:jstack -h
示例:jstack -l 33193
6、jinfo,JVM Configuration info 這個命令作用是實時查看和調整虛擬機運行參數。
查看幫助文檔:jinfo -h
示例:jinfo -flags 33193
22、你知道哪些JVM性能調優參數?(高頻)
1、設定堆記憶體大小:
-Xms 設置最小堆記憶體大小(不能小於1024K); -Xms 堆記憶體初始大小,可以通過jmap工具進行查看
-Xmx 設置最大堆記憶體大小(不能小於1024K); -Xmx 堆記憶體最大值,可以通過jmap工具進行查看
2、設定新生代大小:
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
3、-XX:SurvivorRatio:伊甸園空間和幸存者空間的占比
4、設定垃圾回收器
年輕代用 -XX:+UseParNewGC
年老代用-XX:+UseConcMarkSweepGC
23、你用過哪些性能調優工具?(高頻)
常用調優工具分為兩類
1、jdk自帶監控工具
-
jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制台,用於對JVM中記憶體,線程和類等的監控
-
jvisualvm,jdk自帶全能工具,可以分析記憶體快照、線程快照;監控記憶體變化、GC變化等。
2、第三方
-
MAT,Memory Analyzer Tool,一個基於Eclipse的記憶體分析工具,是一個快速、功能豐富的Java heap分析工具,它可以幫助我們查找記憶體泄漏和減少記憶體消耗
-
GChisto,一款專業分析gc日誌的工具
24、你都有哪些手段用來排查記憶體溢出?(高頻)
記憶體溢出包含很多種情況,我在平常工作中遇到最多的就是堆溢出。有一次線上遇到故障,重新啟動後,使用jstat命令,發現Old區在一直增長。我使用jmap命令,導出了一份線上堆棧,然後使用MAT進行分析。通過對GC Roots的分析,我發現了一個非常大的HashMap對象,這個原本是有位同學做緩存用的,但是一個無界緩存,造成了堆記憶體占用一直上升。後來,將這個緩存改成 Guava Cache,並設置了弱引用,故障就消失了。
本文來自博客園,作者:{Orator-xy},轉載請註明原文鏈接:{https://www.cnblogs.com/xy1857/}