'阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?'

"


"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

看暈了吧,關注這幾個問題:

  • 為什麼要分新生代和老年代?
  • 新生代為什麼分一個Eden區和兩個Survivor區?
  • 一個Eden區和兩個Survivor區的比例為什麼是8:1:1?

現在還不能解釋為什麼,但這幾個問題都是垃圾回收機制所採用的算法決定的。 所以問題轉化為,是何種算法?為什麼要採用此種算法?

可回收對象的判定

講算法之前,我們先要搞清楚一個問題,什麼樣的對象是垃圾(無用對象),需要被回收? 目前市面上有兩種算法用來判定一個對象是否為垃圾。

1. 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

看暈了吧,關注這幾個問題:

  • 為什麼要分新生代和老年代?
  • 新生代為什麼分一個Eden區和兩個Survivor區?
  • 一個Eden區和兩個Survivor區的比例為什麼是8:1:1?

現在還不能解釋為什麼,但這幾個問題都是垃圾回收機制所採用的算法決定的。 所以問題轉化為,是何種算法?為什麼要採用此種算法?

可回收對象的判定

講算法之前,我們先要搞清楚一個問題,什麼樣的對象是垃圾(無用對象),需要被回收? 目前市面上有兩種算法用來判定一個對象是否為垃圾。

1. 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

優點是簡單,高效,現在的objective-c用的就是這種算法。 缺點是很難處理循環引用,比如圖中相互引用的兩個對象則無法釋放。 這個缺點很致命,有人可能會問,那objective-c不是用的好好的嗎? 我個人並沒有覺得objective-c好好的處理了這個循環引用問題,它其實是把這個問題拋給了開發者。

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

為了解決上面的循環引用問題,Java採用了一種新的算法:可達性分析算法。 從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。 [圖片上傳失敗...(image-d070ec-1563457467173)] OK,即使循環引用了,只要沒有被GC Roots引用了依然會被回收,完美! 但是,這個GC Roots的定義就要考究了,Java語言定義瞭如下GC Roots對象:

虛擬機棧(幀棧中的本地變量表)中引用的對象。 方法區中靜態屬性引用的對象。 方法區中常量引用的對象。 本地方法棧中JNI引用的對象。

Stop The World

有了上面的垃圾對象的判定,我們還要考慮一個問題,請大家做好心裡準備,那就是Stop The World。 因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍後回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處於暫停狀態,卡住了。 幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關於其他GC比如併發GC之類的,在此不討論)。 所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

幾種垃圾回收算法

有了上面兩個大基礎,我們的GC才能開始。 那麼問題來了,已經知道哪些是垃圾對象了,怎麼回收呢?目前主流有以下幾種算法。 PS:大家可以先猜猜Java虛擬機(這裡默認指Hotspot)採用的是那種算法,…,答對了,是分代回收算法,現在是不是明白了前面堆內存為什麼要分新生代和老年代了吧。但是即使猜對了,也要看其他幾種算法哦,不然不要說我沒提醒你,你會直接看不懂分代回收算法的。

1. 標記清除算法 (Mark-Sweep)

標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。 優點是簡單,容易實現。 缺點是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。 示意圖如下(不用我解說了吧):

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

看暈了吧,關注這幾個問題:

  • 為什麼要分新生代和老年代?
  • 新生代為什麼分一個Eden區和兩個Survivor區?
  • 一個Eden區和兩個Survivor區的比例為什麼是8:1:1?

現在還不能解釋為什麼,但這幾個問題都是垃圾回收機制所採用的算法決定的。 所以問題轉化為,是何種算法?為什麼要採用此種算法?

可回收對象的判定

講算法之前,我們先要搞清楚一個問題,什麼樣的對象是垃圾(無用對象),需要被回收? 目前市面上有兩種算法用來判定一個對象是否為垃圾。

1. 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

優點是簡單,高效,現在的objective-c用的就是這種算法。 缺點是很難處理循環引用,比如圖中相互引用的兩個對象則無法釋放。 這個缺點很致命,有人可能會問,那objective-c不是用的好好的嗎? 我個人並沒有覺得objective-c好好的處理了這個循環引用問題,它其實是把這個問題拋給了開發者。

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

為了解決上面的循環引用問題,Java採用了一種新的算法:可達性分析算法。 從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。 [圖片上傳失敗...(image-d070ec-1563457467173)] OK,即使循環引用了,只要沒有被GC Roots引用了依然會被回收,完美! 但是,這個GC Roots的定義就要考究了,Java語言定義瞭如下GC Roots對象:

虛擬機棧(幀棧中的本地變量表)中引用的對象。 方法區中靜態屬性引用的對象。 方法區中常量引用的對象。 本地方法棧中JNI引用的對象。

Stop The World

有了上面的垃圾對象的判定,我們還要考慮一個問題,請大家做好心裡準備,那就是Stop The World。 因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍後回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處於暫停狀態,卡住了。 幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關於其他GC比如併發GC之類的,在此不討論)。 所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

幾種垃圾回收算法

有了上面兩個大基礎,我們的GC才能開始。 那麼問題來了,已經知道哪些是垃圾對象了,怎麼回收呢?目前主流有以下幾種算法。 PS:大家可以先猜猜Java虛擬機(這裡默認指Hotspot)採用的是那種算法,…,答對了,是分代回收算法,現在是不是明白了前面堆內存為什麼要分新生代和老年代了吧。但是即使猜對了,也要看其他幾種算法哦,不然不要說我沒提醒你,你會直接看不懂分代回收算法的。

1. 標記清除算法 (Mark-Sweep)

標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。 優點是簡單,容易實現。 缺點是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

2. 複製算法 (Copying)

複製算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。 優缺點就是,實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。 從算法原理我們可以看出,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。 示意圖如下(不用我解說了吧):

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

看暈了吧,關注這幾個問題:

  • 為什麼要分新生代和老年代?
  • 新生代為什麼分一個Eden區和兩個Survivor區?
  • 一個Eden區和兩個Survivor區的比例為什麼是8:1:1?

現在還不能解釋為什麼,但這幾個問題都是垃圾回收機制所採用的算法決定的。 所以問題轉化為,是何種算法?為什麼要採用此種算法?

可回收對象的判定

講算法之前,我們先要搞清楚一個問題,什麼樣的對象是垃圾(無用對象),需要被回收? 目前市面上有兩種算法用來判定一個對象是否為垃圾。

1. 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

優點是簡單,高效,現在的objective-c用的就是這種算法。 缺點是很難處理循環引用,比如圖中相互引用的兩個對象則無法釋放。 這個缺點很致命,有人可能會問,那objective-c不是用的好好的嗎? 我個人並沒有覺得objective-c好好的處理了這個循環引用問題,它其實是把這個問題拋給了開發者。

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

為了解決上面的循環引用問題,Java採用了一種新的算法:可達性分析算法。 從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。 [圖片上傳失敗...(image-d070ec-1563457467173)] OK,即使循環引用了,只要沒有被GC Roots引用了依然會被回收,完美! 但是,這個GC Roots的定義就要考究了,Java語言定義瞭如下GC Roots對象:

虛擬機棧(幀棧中的本地變量表)中引用的對象。 方法區中靜態屬性引用的對象。 方法區中常量引用的對象。 本地方法棧中JNI引用的對象。

Stop The World

有了上面的垃圾對象的判定,我們還要考慮一個問題,請大家做好心裡準備,那就是Stop The World。 因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍後回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處於暫停狀態,卡住了。 幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關於其他GC比如併發GC之類的,在此不討論)。 所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

幾種垃圾回收算法

有了上面兩個大基礎,我們的GC才能開始。 那麼問題來了,已經知道哪些是垃圾對象了,怎麼回收呢?目前主流有以下幾種算法。 PS:大家可以先猜猜Java虛擬機(這裡默認指Hotspot)採用的是那種算法,…,答對了,是分代回收算法,現在是不是明白了前面堆內存為什麼要分新生代和老年代了吧。但是即使猜對了,也要看其他幾種算法哦,不然不要說我沒提醒你,你會直接看不懂分代回收算法的。

1. 標記清除算法 (Mark-Sweep)

標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。 優點是簡單,容易實現。 缺點是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

2. 複製算法 (Copying)

複製算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。 優缺點就是,實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。 從算法原理我們可以看出,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

3. 標記整理算法 (Mark-Compact)

該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。 所以,特別適用於存活對象多,回收對象少的情況下。 示意圖如下(不用我解說了吧):

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

看暈了吧,關注這幾個問題:

  • 為什麼要分新生代和老年代?
  • 新生代為什麼分一個Eden區和兩個Survivor區?
  • 一個Eden區和兩個Survivor區的比例為什麼是8:1:1?

現在還不能解釋為什麼,但這幾個問題都是垃圾回收機制所採用的算法決定的。 所以問題轉化為,是何種算法?為什麼要採用此種算法?

可回收對象的判定

講算法之前,我們先要搞清楚一個問題,什麼樣的對象是垃圾(無用對象),需要被回收? 目前市面上有兩種算法用來判定一個對象是否為垃圾。

1. 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

優點是簡單,高效,現在的objective-c用的就是這種算法。 缺點是很難處理循環引用,比如圖中相互引用的兩個對象則無法釋放。 這個缺點很致命,有人可能會問,那objective-c不是用的好好的嗎? 我個人並沒有覺得objective-c好好的處理了這個循環引用問題,它其實是把這個問題拋給了開發者。

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

為了解決上面的循環引用問題,Java採用了一種新的算法:可達性分析算法。 從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。 [圖片上傳失敗...(image-d070ec-1563457467173)] OK,即使循環引用了,只要沒有被GC Roots引用了依然會被回收,完美! 但是,這個GC Roots的定義就要考究了,Java語言定義瞭如下GC Roots對象:

虛擬機棧(幀棧中的本地變量表)中引用的對象。 方法區中靜態屬性引用的對象。 方法區中常量引用的對象。 本地方法棧中JNI引用的對象。

Stop The World

有了上面的垃圾對象的判定,我們還要考慮一個問題,請大家做好心裡準備,那就是Stop The World。 因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍後回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處於暫停狀態,卡住了。 幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關於其他GC比如併發GC之類的,在此不討論)。 所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

幾種垃圾回收算法

有了上面兩個大基礎,我們的GC才能開始。 那麼問題來了,已經知道哪些是垃圾對象了,怎麼回收呢?目前主流有以下幾種算法。 PS:大家可以先猜猜Java虛擬機(這裡默認指Hotspot)採用的是那種算法,…,答對了,是分代回收算法,現在是不是明白了前面堆內存為什麼要分新生代和老年代了吧。但是即使猜對了,也要看其他幾種算法哦,不然不要說我沒提醒你,你會直接看不懂分代回收算法的。

1. 標記清除算法 (Mark-Sweep)

標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。 優點是簡單,容易實現。 缺點是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

2. 複製算法 (Copying)

複製算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。 優缺點就是,實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。 從算法原理我們可以看出,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

3. 標記整理算法 (Mark-Compact)

該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。 所以,特別適用於存活對象多,回收對象少的情況下。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

4. 分代回收算法

分代回收算法其實不算一種新的算法,而是根據複製算法和標記整理算法的的特點綜合而成。這種綜合是考慮到java的語言特性的。 這裡重複一下兩種老算法的適用場景:

複製算法:適用於存活對象很少。回收對象多 標記整理算法: 適用用於存活對象多,回收對象少

剛好互補!不同類型的對象生命週期決定了更適合採用哪種算法。 於是,我們根據對象存活的生命週期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Old Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。 這就是分代回收算法。 現在回頭去看堆內存為什麼要劃分新生代和老年代,是不是覺得如此的清晰和自然了?

我們再說的細一點:

  • 對於新生代採取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要複製的操作次數較少,採用Copying算法效率最高。但是,但是,但是,實際中並不是按照上面算法中說的1:1的比例來劃分新生代的空間的,而是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,比例為8:1:1.。為什麼?下一節深入分析。
  • 由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

深入理解分代回收算法

對於這個算法,我相信很多人還是有疑問的,我們來各個擊破,說清楚了就很簡單。

為什麼不是一塊Survivor空間而是兩塊?

這裡涉及到一個新生代和老年代的存活週期的問題,比如一個對象在新生代經歷15次(僅供參考)GC,就可以移到老年代了。問題來了,當我們第一次GC的時候,我們可以把Eden區的存活對象放到Survivor A空間,但是第二次GC的時候,Survivor A空間的存活對象也需要再次用Copying算法,放到Survivor B空間上,而把剛剛的Survivor A空間和Eden空間清除。第三次GC時,又把Survivor B空間的存活對象複製到Survivor A空間,如此反覆。 所以,這裡就需要兩塊Survivor空間來回倒騰。

為什麼Eden空間這麼大而Survivor空間要分的少一點?

新創建的對象都是放在Eden空間,這是很頻繁的,尤其是大量的局部變量產生的臨時對象,這些對象絕大部分都應該馬上被回收,能存活下來被轉移到survivor空間的往往不多。所以,設置較大的Eden空間和較小的Survivor空間是合理的,大大提高了內存的使用率,緩解了Copying算法的缺點。 我看8:1:1就挺好的,當然這個比例是可以調整的,包括上面的新生代和老年代的1:2的比例也是可以調整的。 新的問題又來了,從Eden空間往Survivor空間轉移的時候Survivor空間不夠了怎麼辦?直接放到老年代去。

Eden空間和兩塊Survivor空間的工作流程

這裡本來簡單的Copying算法被劃分為三部分後很多朋友一時理解不了,也確實不好描述,下面我來演示一下Eden空間和兩塊Survivor空間的工作流程。

現在假定有新生代Eden,Survivor A, Survivor B三塊空間和老生代Old一塊空間。

// 分配了一個又一個對象
放到Eden區
// 不好,Eden區滿了,只能GC(新生代GC:Minor GC)了
把Eden區的存活對象copy到Survivor A區,然後清空Eden區(本來Survivor B區也需要清空的,不過本來就是空的)
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor A區的存活對象copy到Survivor B區,然後清空Eden區和Survivor A區
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor B區的存活對象copy到Survivor A區,然後清空Eden區和Survivor B區
// ...
// 有的對象來回在Survivor A區或者B區呆了比如15次,就被分配到老年代Old區
// 有的對象太大,超過了Eden區,直接被分配在Old區
// 有的存活對象,放不下Survivor區,也被分配到Old區
// ...
// 在某次Minor GC的過程中突然發現:
// 不好,老年代Old區也滿了,這是一次大GC(老年代GC:Major GC)
Old區慢慢的整理一番,空間又夠了
// 繼續Minor GC
// ...
// ...

從這段流程中,我相信大家應該有了一個清晰的認識了,當然為了說明原理,這只是最簡化版本。

觸發GC的類型

瞭解這些是為了解決實際問題,Java虛擬機會把每次觸發GC的信息打印出來來幫助我們分析問題,所以掌握觸發GC的類型是分析日誌的基礎。

GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。 GC_CONCURRENT: 當我們應用程序的堆內存達到一定量,或者可以理解為快要滿的時候,系統會自動觸發GC操作來釋放內存。 GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。 GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最後努力而觸發的GC

總結

好了,今天的分享就到這裡,如果你對在面試中遇到的問題,或者剛畢業及工作幾年迷茫不知道該如何準備面試並突破現狀提升自己,對於自己的未來還不夠了解不知道給如何規劃,可以私信我“技能提升”來看看同行們都是如何突破現狀,怎麼學習的,來吸收他們的面試以及工作經驗完善自己的之後的面試計劃及職業規劃。

面試相關資料的也可以後臺私信回覆“面試資料”免費獲取~

這裡放上一部分我工作以來以及參與過的大大小小的面試收集總結出來的一套進階學習的視頻及面試專題資料包,在這裡免費分享給大家,主要還是希望大家在如今大環境不好的情況下面試能夠順利一點,希望可以幫助到大家~

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

看暈了吧,關注這幾個問題:

  • 為什麼要分新生代和老年代?
  • 新生代為什麼分一個Eden區和兩個Survivor區?
  • 一個Eden區和兩個Survivor區的比例為什麼是8:1:1?

現在還不能解釋為什麼,但這幾個問題都是垃圾回收機制所採用的算法決定的。 所以問題轉化為,是何種算法?為什麼要採用此種算法?

可回收對象的判定

講算法之前,我們先要搞清楚一個問題,什麼樣的對象是垃圾(無用對象),需要被回收? 目前市面上有兩種算法用來判定一個對象是否為垃圾。

1. 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

優點是簡單,高效,現在的objective-c用的就是這種算法。 缺點是很難處理循環引用,比如圖中相互引用的兩個對象則無法釋放。 這個缺點很致命,有人可能會問,那objective-c不是用的好好的嗎? 我個人並沒有覺得objective-c好好的處理了這個循環引用問題,它其實是把這個問題拋給了開發者。

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

為了解決上面的循環引用問題,Java採用了一種新的算法:可達性分析算法。 從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。 [圖片上傳失敗...(image-d070ec-1563457467173)] OK,即使循環引用了,只要沒有被GC Roots引用了依然會被回收,完美! 但是,這個GC Roots的定義就要考究了,Java語言定義瞭如下GC Roots對象:

虛擬機棧(幀棧中的本地變量表)中引用的對象。 方法區中靜態屬性引用的對象。 方法區中常量引用的對象。 本地方法棧中JNI引用的對象。

Stop The World

有了上面的垃圾對象的判定,我們還要考慮一個問題,請大家做好心裡準備,那就是Stop The World。 因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍後回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處於暫停狀態,卡住了。 幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關於其他GC比如併發GC之類的,在此不討論)。 所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

幾種垃圾回收算法

有了上面兩個大基礎,我們的GC才能開始。 那麼問題來了,已經知道哪些是垃圾對象了,怎麼回收呢?目前主流有以下幾種算法。 PS:大家可以先猜猜Java虛擬機(這裡默認指Hotspot)採用的是那種算法,…,答對了,是分代回收算法,現在是不是明白了前面堆內存為什麼要分新生代和老年代了吧。但是即使猜對了,也要看其他幾種算法哦,不然不要說我沒提醒你,你會直接看不懂分代回收算法的。

1. 標記清除算法 (Mark-Sweep)

標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。 優點是簡單,容易實現。 缺點是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

2. 複製算法 (Copying)

複製算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。 優缺點就是,實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。 從算法原理我們可以看出,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

3. 標記整理算法 (Mark-Compact)

該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。 所以,特別適用於存活對象多,回收對象少的情況下。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

4. 分代回收算法

分代回收算法其實不算一種新的算法,而是根據複製算法和標記整理算法的的特點綜合而成。這種綜合是考慮到java的語言特性的。 這裡重複一下兩種老算法的適用場景:

複製算法:適用於存活對象很少。回收對象多 標記整理算法: 適用用於存活對象多,回收對象少

剛好互補!不同類型的對象生命週期決定了更適合採用哪種算法。 於是,我們根據對象存活的生命週期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Old Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。 這就是分代回收算法。 現在回頭去看堆內存為什麼要劃分新生代和老年代,是不是覺得如此的清晰和自然了?

我們再說的細一點:

  • 對於新生代採取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要複製的操作次數較少,採用Copying算法效率最高。但是,但是,但是,實際中並不是按照上面算法中說的1:1的比例來劃分新生代的空間的,而是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,比例為8:1:1.。為什麼?下一節深入分析。
  • 由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

深入理解分代回收算法

對於這個算法,我相信很多人還是有疑問的,我們來各個擊破,說清楚了就很簡單。

為什麼不是一塊Survivor空間而是兩塊?

這裡涉及到一個新生代和老年代的存活週期的問題,比如一個對象在新生代經歷15次(僅供參考)GC,就可以移到老年代了。問題來了,當我們第一次GC的時候,我們可以把Eden區的存活對象放到Survivor A空間,但是第二次GC的時候,Survivor A空間的存活對象也需要再次用Copying算法,放到Survivor B空間上,而把剛剛的Survivor A空間和Eden空間清除。第三次GC時,又把Survivor B空間的存活對象複製到Survivor A空間,如此反覆。 所以,這裡就需要兩塊Survivor空間來回倒騰。

為什麼Eden空間這麼大而Survivor空間要分的少一點?

新創建的對象都是放在Eden空間,這是很頻繁的,尤其是大量的局部變量產生的臨時對象,這些對象絕大部分都應該馬上被回收,能存活下來被轉移到survivor空間的往往不多。所以,設置較大的Eden空間和較小的Survivor空間是合理的,大大提高了內存的使用率,緩解了Copying算法的缺點。 我看8:1:1就挺好的,當然這個比例是可以調整的,包括上面的新生代和老年代的1:2的比例也是可以調整的。 新的問題又來了,從Eden空間往Survivor空間轉移的時候Survivor空間不夠了怎麼辦?直接放到老年代去。

Eden空間和兩塊Survivor空間的工作流程

這裡本來簡單的Copying算法被劃分為三部分後很多朋友一時理解不了,也確實不好描述,下面我來演示一下Eden空間和兩塊Survivor空間的工作流程。

現在假定有新生代Eden,Survivor A, Survivor B三塊空間和老生代Old一塊空間。

// 分配了一個又一個對象
放到Eden區
// 不好,Eden區滿了,只能GC(新生代GC:Minor GC)了
把Eden區的存活對象copy到Survivor A區,然後清空Eden區(本來Survivor B區也需要清空的,不過本來就是空的)
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor A區的存活對象copy到Survivor B區,然後清空Eden區和Survivor A區
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor B區的存活對象copy到Survivor A區,然後清空Eden區和Survivor B區
// ...
// 有的對象來回在Survivor A區或者B區呆了比如15次,就被分配到老年代Old區
// 有的對象太大,超過了Eden區,直接被分配在Old區
// 有的存活對象,放不下Survivor區,也被分配到Old區
// ...
// 在某次Minor GC的過程中突然發現:
// 不好,老年代Old區也滿了,這是一次大GC(老年代GC:Major GC)
Old區慢慢的整理一番,空間又夠了
// 繼續Minor GC
// ...
// ...

從這段流程中,我相信大家應該有了一個清晰的認識了,當然為了說明原理,這只是最簡化版本。

觸發GC的類型

瞭解這些是為了解決實際問題,Java虛擬機會把每次觸發GC的信息打印出來來幫助我們分析問題,所以掌握觸發GC的類型是分析日誌的基礎。

GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。 GC_CONCURRENT: 當我們應用程序的堆內存達到一定量,或者可以理解為快要滿的時候,系統會自動觸發GC操作來釋放內存。 GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。 GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最後努力而觸發的GC

總結

好了,今天的分享就到這裡,如果你對在面試中遇到的問題,或者剛畢業及工作幾年迷茫不知道該如何準備面試並突破現狀提升自己,對於自己的未來還不夠了解不知道給如何規劃,可以私信我“技能提升”來看看同行們都是如何突破現狀,怎麼學習的,來吸收他們的面試以及工作經驗完善自己的之後的面試計劃及職業規劃。

面試相關資料的也可以後臺私信回覆“面試資料”免費獲取~

這裡放上一部分我工作以來以及參與過的大大小小的面試收集總結出來的一套進階學習的視頻及面試專題資料包,在這裡免費分享給大家,主要還是希望大家在如今大環境不好的情況下面試能夠順利一點,希望可以幫助到大家~

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

專注分享大型Bat面試知識,後續會持續更新,希望通過這些高級面試題能夠降低面試Android崗位的門檻,讓更多的Android工程師理解Android系統,掌握Android系統。喜歡的話麻煩點擊一個喜歡在關注一下~

"


阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

垃圾回收機制是如何實現的呢?其實從Jvm內存模型中入手對於理解GC會有很大的幫助,不過這裡只需要瞭解一個大概,說多了反而混淆視線。

Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。

簡言之,Java程序內存主要(這裡強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。

配一張示意圖總結一下:

堆內存(Heap Memory): 存放Java對象 非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data 其它(Other): 存放JVM 自身代碼等

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

堆內存模型

既然重點是堆內存,我們就再看看堆的內存模型。

堆內存由垃圾回收器的自動內存管理系統回收。 堆內存分為兩大部分:新生代和老年代。比例為1:2。 老年代主要存放應用程序中生命週期長的存活對象。 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。 Eden區存放新生的對象。 Survivor存放每次垃圾回收後存活的對象。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

看暈了吧,關注這幾個問題:

  • 為什麼要分新生代和老年代?
  • 新生代為什麼分一個Eden區和兩個Survivor區?
  • 一個Eden區和兩個Survivor區的比例為什麼是8:1:1?

現在還不能解釋為什麼,但這幾個問題都是垃圾回收機制所採用的算法決定的。 所以問題轉化為,是何種算法?為什麼要採用此種算法?

可回收對象的判定

講算法之前,我們先要搞清楚一個問題,什麼樣的對象是垃圾(無用對象),需要被回收? 目前市面上有兩種算法用來判定一個對象是否為垃圾。

1. 引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

優點是簡單,高效,現在的objective-c用的就是這種算法。 缺點是很難處理循環引用,比如圖中相互引用的兩個對象則無法釋放。 這個缺點很致命,有人可能會問,那objective-c不是用的好好的嗎? 我個人並沒有覺得objective-c好好的處理了這個循環引用問題,它其實是把這個問題拋給了開發者。

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

為了解決上面的循環引用問題,Java採用了一種新的算法:可達性分析算法。 從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。 [圖片上傳失敗...(image-d070ec-1563457467173)] OK,即使循環引用了,只要沒有被GC Roots引用了依然會被回收,完美! 但是,這個GC Roots的定義就要考究了,Java語言定義瞭如下GC Roots對象:

虛擬機棧(幀棧中的本地變量表)中引用的對象。 方法區中靜態屬性引用的對象。 方法區中常量引用的對象。 本地方法棧中JNI引用的對象。

Stop The World

有了上面的垃圾對象的判定,我們還要考慮一個問題,請大家做好心裡準備,那就是Stop The World。 因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍後回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處於暫停狀態,卡住了。 幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關於其他GC比如併發GC之類的,在此不討論)。 所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。

幾種垃圾回收算法

有了上面兩個大基礎,我們的GC才能開始。 那麼問題來了,已經知道哪些是垃圾對象了,怎麼回收呢?目前主流有以下幾種算法。 PS:大家可以先猜猜Java虛擬機(這裡默認指Hotspot)採用的是那種算法,…,答對了,是分代回收算法,現在是不是明白了前面堆內存為什麼要分新生代和老年代了吧。但是即使猜對了,也要看其他幾種算法哦,不然不要說我沒提醒你,你會直接看不懂分代回收算法的。

1. 標記清除算法 (Mark-Sweep)

標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。 優點是簡單,容易實現。 缺點是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

2. 複製算法 (Copying)

複製算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。 優缺點就是,實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。 從算法原理我們可以看出,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

3. 標記整理算法 (Mark-Compact)

該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。 所以,特別適用於存活對象多,回收對象少的情況下。 示意圖如下(不用我解說了吧):

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

4. 分代回收算法

分代回收算法其實不算一種新的算法,而是根據複製算法和標記整理算法的的特點綜合而成。這種綜合是考慮到java的語言特性的。 這裡重複一下兩種老算法的適用場景:

複製算法:適用於存活對象很少。回收對象多 標記整理算法: 適用用於存活對象多,回收對象少

剛好互補!不同類型的對象生命週期決定了更適合採用哪種算法。 於是,我們根據對象存活的生命週期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Old Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。 這就是分代回收算法。 現在回頭去看堆內存為什麼要劃分新生代和老年代,是不是覺得如此的清晰和自然了?

我們再說的細一點:

  • 對於新生代採取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要複製的操作次數較少,採用Copying算法效率最高。但是,但是,但是,實際中並不是按照上面算法中說的1:1的比例來劃分新生代的空間的,而是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,比例為8:1:1.。為什麼?下一節深入分析。
  • 由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

深入理解分代回收算法

對於這個算法,我相信很多人還是有疑問的,我們來各個擊破,說清楚了就很簡單。

為什麼不是一塊Survivor空間而是兩塊?

這裡涉及到一個新生代和老年代的存活週期的問題,比如一個對象在新生代經歷15次(僅供參考)GC,就可以移到老年代了。問題來了,當我們第一次GC的時候,我們可以把Eden區的存活對象放到Survivor A空間,但是第二次GC的時候,Survivor A空間的存活對象也需要再次用Copying算法,放到Survivor B空間上,而把剛剛的Survivor A空間和Eden空間清除。第三次GC時,又把Survivor B空間的存活對象複製到Survivor A空間,如此反覆。 所以,這裡就需要兩塊Survivor空間來回倒騰。

為什麼Eden空間這麼大而Survivor空間要分的少一點?

新創建的對象都是放在Eden空間,這是很頻繁的,尤其是大量的局部變量產生的臨時對象,這些對象絕大部分都應該馬上被回收,能存活下來被轉移到survivor空間的往往不多。所以,設置較大的Eden空間和較小的Survivor空間是合理的,大大提高了內存的使用率,緩解了Copying算法的缺點。 我看8:1:1就挺好的,當然這個比例是可以調整的,包括上面的新生代和老年代的1:2的比例也是可以調整的。 新的問題又來了,從Eden空間往Survivor空間轉移的時候Survivor空間不夠了怎麼辦?直接放到老年代去。

Eden空間和兩塊Survivor空間的工作流程

這裡本來簡單的Copying算法被劃分為三部分後很多朋友一時理解不了,也確實不好描述,下面我來演示一下Eden空間和兩塊Survivor空間的工作流程。

現在假定有新生代Eden,Survivor A, Survivor B三塊空間和老生代Old一塊空間。

// 分配了一個又一個對象
放到Eden區
// 不好,Eden區滿了,只能GC(新生代GC:Minor GC)了
把Eden區的存活對象copy到Survivor A區,然後清空Eden區(本來Survivor B區也需要清空的,不過本來就是空的)
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor A區的存活對象copy到Survivor B區,然後清空Eden區和Survivor A區
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor B區的存活對象copy到Survivor A區,然後清空Eden區和Survivor B區
// ...
// 有的對象來回在Survivor A區或者B區呆了比如15次,就被分配到老年代Old區
// 有的對象太大,超過了Eden區,直接被分配在Old區
// 有的存活對象,放不下Survivor區,也被分配到Old區
// ...
// 在某次Minor GC的過程中突然發現:
// 不好,老年代Old區也滿了,這是一次大GC(老年代GC:Major GC)
Old區慢慢的整理一番,空間又夠了
// 繼續Minor GC
// ...
// ...

從這段流程中,我相信大家應該有了一個清晰的認識了,當然為了說明原理,這只是最簡化版本。

觸發GC的類型

瞭解這些是為了解決實際問題,Java虛擬機會把每次觸發GC的信息打印出來來幫助我們分析問題,所以掌握觸發GC的類型是分析日誌的基礎。

GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。 GC_CONCURRENT: 當我們應用程序的堆內存達到一定量,或者可以理解為快要滿的時候,系統會自動觸發GC操作來釋放內存。 GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。 GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最後努力而觸發的GC

總結

好了,今天的分享就到這裡,如果你對在面試中遇到的問題,或者剛畢業及工作幾年迷茫不知道該如何準備面試並突破現狀提升自己,對於自己的未來還不夠了解不知道給如何規劃,可以私信我“技能提升”來看看同行們都是如何突破現狀,怎麼學習的,來吸收他們的面試以及工作經驗完善自己的之後的面試計劃及職業規劃。

面試相關資料的也可以後臺私信回覆“面試資料”免費獲取~

這裡放上一部分我工作以來以及參與過的大大小小的面試收集總結出來的一套進階學習的視頻及面試專題資料包,在這裡免費分享給大家,主要還是希望大家在如今大環境不好的情況下面試能夠順利一點,希望可以幫助到大家~

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

專注分享大型Bat面試知識,後續會持續更新,希望通過這些高級面試題能夠降低面試Android崗位的門檻,讓更多的Android工程師理解Android系統,掌握Android系統。喜歡的話麻煩點擊一個喜歡在關注一下~

阿里高級Android面試題:安卓內存模型 垃圾回收機制是如何實現?

"

相關推薦

推薦中...