Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

前言

這周我投遞出了簡歷,崗位是java後端開發工程師。這周阿里面試官給我進行了面試。面試過程中他問了java垃圾回收機制以及算法,今天結合面試官的三個問題詳細講一講

java的垃圾回收機制

java對象

面試官大佬:如何判斷java對象已經被回收

我:(這可難不倒我)

引用計數

為每個對象存儲一個計數RC,當有其他引用指向它時,計數RC++;當其他引用與其斷開時,RC--;如果有RC=0,則回收它(及其所以指向的object)。

可達性分析法(根搜索算法)

把內存中的每一個對象都看作一個節點,並且定義了一些對象作為根節點“GC Roots”。**以“GC Root”的對象作為起始點,開始向下搜索,搜索所走過的路徑稱為引用鏈。**如果一個對象與起始點沒有任何引用鏈,則說明不可用,需要被回收。


Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

圖示object6、7、8與起始點沒有任何引用鏈,則說明不可用,需要被回收。

面試官大佬:談一談JVM垃圾回收算法的進化

我:(這可難不倒我)

JVM(java虛擬機)的內存結構

Native Stacks本地方法棧PC代碼行號指示器,用於跳轉下一條需要執行的命令Method area用於存儲被VM加載的 類信息、常量、靜態變量等

垃圾回收基本算法

標記-清除算法

定義:為每個object設定狀態位(live/dead)並記錄,即mark階段;將 標記為dead的對象進行清理,即sweep階段。

簡單來說,首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

標記過程:

Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

清除過程:

Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

存在問題:

  • 效率不高:標記和清除過程的效率都不高
  • 標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致大對象無法分配到足夠的連續內存,從而不得不提前觸發GC,甚至Stop The World

標記-整理


首先標記出所有需要回收的對象

在標記完成後讓所有存活的對象都向一端移動

最後直接清理掉端邊界以外的內存


Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

優點:避免碎片化

缺點:時間消耗太長,影響程序本身

複製算法


該GC策略與標記-整理的區別在於:不是在同一個區域內進行整理,而是將live對象全部複製到另一個區域。

將可用內存按照容量劃分為大小相等的兩塊,每次只使用其中的一塊。

當這一塊的內存用完了。首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象,每次只使用其中的一塊。

當一塊的內存用完了,將還存活著的對象複製到另外一塊上面,然後清理已使用過的內存空間。

Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

<code>flip(){
\tFromspace, Tospace = Tospace, Fromspace
\ttop_of_space = Tospace + space_size
\tscan = free = Tospace
\tfor R in Roots {R = copy(R)}
\twhile scan < free {
\tfor P in Children(scan) {*P = copy(*P)}
\tscan = scan + size (scan)
\t}
}
copy(P) {
\tif forwarded(P){return forwarding_address(P)}
\telse {
\taddr = free
\tmove(P,free)
\tfree = free + size(P)
\tforwarding_address(P) = addr
\treturn addr
\t}
}
複製代碼/<code>

優點:

  • 免費壓縮空間
  • 所有對象大小的分配都非常便宜:只需增加空閒指針即可分配
  • 僅處理實時數據(通常是堆的一小部分)
  • 固定的空間開銷:釋放和掃描指針
  • 全面:自然收集的循環垃圾
  • 易於實施並且合理有效

存在問題:

  • 效率問題:在效率存活率較高時,複製次數多,效率低
  • 空間問題:內存縮小了一半;需要額外空間做分配擔保(老年代)

分代回收算法

Java堆分為新生代、老年代和永久區(Java 8之後改名為Metaspace)。

Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

針對不同的區域,使用不同的GC策略

  • 在新生代中,只有一小部分對象可較長時間 存活,選用複製算法
  • 針對年老代:這裡的對象有很高的倖存度,使用“標記-清理”或“標記-整理”算法

JVM調優

面試官大佬:詳細說說一次你JVM調優的經歷

我:(這可難不到我)

問題


Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

Young GC較為頻繁。查看服務器的JVM參數如下

<code>-Xms1000M 
-Xmx1800M
-Xmn350M
-Xss300K
-XX:+DisableExplicitGC
-XX:SurvivorRatio=4
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+CMSParallelRemarkEnabled
複製代碼/<code>

-Xms表示初始化堆內存

-Xmx表示最大堆內存

-Xmn表示新生代內存

-XX:SurvivorRatio=4表示新生代的Eden是4/10,S1和S2各佔3/10

因此Eden的內存大小為:0.435010241024字節, 為14010241024**字節

程序

<code>/**
* @date : 2020-03-22 09:48
**/
public class JavaHeapTest {

public final static int OUTOFMEMORY = 500 * 1024 * 1024;

private String oom;

StringBuffer tempOOM = new StringBuffer();

public JavaHeapTest(int leng) {
int i = 0;
while (i < leng) {

i++;
try {
tempOOM.append("a");
} catch (OutOfMemoryError e) {
e.printStackTrace();
break;
}
}
this.oom = tempOOM.toString();
}

public String getOom() {
return oom;
}

public static void main(String[] args) {
for(int i=0;i<50;i++) {
JavaHeapTest javaHeapTest = new JavaHeapTest(OUTOFMEMORY);
System.out.println(javaHeapTest.getOom().length());
}
}
}

複製代碼/<code>

原因

年輕代分為1個Eden和2個Survivor區(一個是from,另一個是to)。新創建的對象一般都會被分配到Eden區,如果經過第一次GC後仍然存活,就會被移到Survivor區。Survivor區中的對象每經過一次Minor GC,年齡+1,當年齡增加到一定程度時,會被移動到年老代。

OUTOFMEMORY = 500 * 1024 * 1024,大於Eden內存的大小。新生代分配內存小,導致YoungGC的頻繁觸發。

初始化堆內存沒有和最大堆內存一致,在每次GC後進行內存可能重新分配。

解決方法

提升新生代大小

將初始化堆內存設置為最大內存

將SurvivorRatio由4修改為8,讓垃圾在新生代時儘可能的多被回收掉

<code>-Xmn350M -> -Xmn800M
-XX:SurvivorRatio=4 -> -XX:SurvivorRatio=8
-Xms1000m ->-Xms1800m
複製代碼/<code>

效果

YoungGC次數明顯減少

Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!

總結

關於對象從出生到回收的全過程,看到一段很棒的話分享一下。

Java 垃圾回收,我的回答讓阿里面試官豎起了大拇指!


“我是一個普通的java對象,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。”

“有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。

“直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。於是我就去了年老代那邊,年老代裡,人很多,並且年齡都挺大的,我在這裡也認識了很多人。在年老代裡,我生活了20年(每次GC加一歲),然後被回收。”


分享到:


相關文章: