深入解析java虛擬機:垃圾回收,垃圾回收基礎概述

垃圾回收基礎概述

垃圾回收機制最早誕生於Lisp編程語言,但Lisp的作者McCathy在第一次現場演示Lisp時卻因中途耗盡全部32KB內存以及一些其他原因只能草草收場。60年後的今天,垃圾回收技術再也不是一個笑話,它儼然成爲諸如Java、C#、Python、Erlang、Golang編程語言的核心組件。

Java最吸引人的特性之一就是它的垃圾回收技術:程序員負責創建對象、使用對象,垃圾回收器負責回收資源,做好善後工作。它從GCRoot出發標記存活對象,清理未被標記的對象,這種方式又被稱爲追蹤式回收。Java的所有垃圾回收器都使用追蹤式回收,只是具體的算法細節不盡相同。本章將討論HotSpot VM中現存的所有垃圾回收器,在這之前,有必要先了解下垃圾回收的一些基礎知識。

GC Root

GC Root又叫根集,它是垃圾回收器掃描存活對象的起始地點。舉個簡單的例子,如代碼清單10-1所示:

代碼清單10-1 GC Root示例

public static void test(){Struct free = new Struct(“a”);Struct obj = new Struct(“b”);obj.field1 = new Object();obj.field2 = 12;System.gc(); // (1)free = null;System.gc(); // (2)}假設(1)處成功觸發垃圾回收,那麼垃圾回收器將不能回收任何對象,因爲線程棧上包括free和obj引用,它們分別指向對象a和對象b。

在(2)處調用成功後,垃圾回收器可以回收對象a但不能回收對象b,因爲棧上存在指向對象b的引用obj,而指向對象a的引用free被賦予null值,即再沒有指向對象a的引用,因此對象a被視作垃圾,可回收處理。

代碼清單10-1中的free和obj所在的線程棧即GC Root之一,垃圾回收器以它們爲起點找到存活對象:凡是從線程棧出發沒有觸及的對象就可以被認爲是死亡對象,繼而可以被回收。除了線程棧外,HotSpot VM還有一些地方也可以作爲GC Root:

1)所有已加載的類的對象引用(ClassLoaderDataGraph::roots_cld_do);

2)所有線程棧上的對象引用(Threads::possibly_parallel_oops_do);

3)虛擬機內部使用的Java對象引用(Universe::oops_do,SystemDictionary::oops_do);

4)JNI Handle(JNIHandles::oops_do);

5)被synchronized鎖住的對象引用(ObjectSynchronizer::oops_do);

6)Java工具用到的對象引用(Management::oops);

7)JVMTI導出對象引用(JvmtiExport::oops_do);

8)AOT堆對象引用(AOTLoader::oops);

9)CodeCache代碼引用(CodeCache::blobs_do);

10)String常量池對象引用(StringTable::oops_do)。

安全點

在垃圾回收器的眼中只有垃圾回收線程和修改對象的線程,後者被稱爲Mutator線程。由於垃圾回收線程也需要修改對象,尤其是在垃圾回收過程中可能有移動對象的情況,如果Mutator線程在移動對象的同時修改對象,勢必會造成錯誤,因此在垃圾回收時一般需要全過程,或者部分過程暫停Mutator線程,這種暫停Mutator線程的現象又叫作世界停頓(Stop The World,STW)。一般來說,Mutator線程可以主動或者被動達到STW,在HotSpot VM中,使用安全點(Safepoint)作爲主動STW機制。安全點本質上是一頁內存,如代碼清單10-2所示:

代碼清單10-2 安全點創建

void SafepointMechanism::default_initialize() {if (ThreadLocalHandshakes) {...// 分配兩頁內存,一頁用於bad_page,一頁用於good_pagechar* polling_page = os::reserve_memory(...);char* bad_page = polling_page;char* good_page = polling_page + page_size;// bad_page表示這片內存不可讀不可寫,good_page表示可讀os::protect_memory(bad_page, page_size, os::MEM_PROT_NONE);os::protect_memory(good_page, page_size, os::MEM_PROT_READ);os::set_polling_page((address)(bad_page));...} else {// 分配一頁內存char* polling_page = os::reserve_memory(...);os::commit_memory_or_exit(...);// 將它設置爲可讀os::protect_memory(polling_page, page_size, os::MEM_PROT_READ);os::set_polling_page((address)(polling_page));}}虛擬機將“讀取安全點內存頁”的操作安插在一些合適的地方。當程序沒有請求垃圾回收時(實際上除了垃圾回收外,還有其他操作可能會請求安全點),安全點內存頁可讀,Mutator線程對安全點的訪問不會引發任何問題。當需要垃圾回收時,VMThread將安全點設置爲不可讀不可寫,然後等待所有Mutator線程走到安全點。由於Mutator線程訪問不可讀不可寫的內存時會引發異常信號,虛擬機可通過內部的信號處理器捕獲並停止Mutator線程的執行,這樣一來相當於讓所有Mutator線程主動停止。

在具體實現中,SafepointSynchronize::begin()和SafepointSynchronize::end()分別表示安全點的開啓和關閉,兩者之間構成一個安全區域,它們只能被VMThread調用。代碼清單10-3展示了安全點開啓的代碼實現:

代碼清單10-3 SafepointSynchronize::begin

void SafepointSynchronize::begin() {...// 設置狀態爲安全點開啓中_state = _synchronizing;// 如果使用全局安全點,修改安全點內存頁,將其設置爲不可讀不可寫// (對應還有如果使用線程握手的處理,這裏已省略)if (SafepointMechanism::uses_global_page_poll()) {Interpreter::notice_safepoints();PageArmed = 1 ;os::make_polling_page_unreadable();}...while(still_running > 0) {jtiwh.rewind();// 對於當前所有運行的線程for (; JavaThread *cur = jtiwh.next(); ) {// 獲取線程狀態ThreadSafepointState *cur_state = cur->safepoint_state();// 如果還在運行if (cur_state->is_running()) {// 檢查線程是不是suspend或者其他情況,並處理它cur_state->examine_state_of_thread();// 再次檢查線程是否還在運行if (!cur_state->is_running()) {// 如果沒有運行,計數減一(still_running表示當前還在運行的線程)still_running--;}}}... // 如果循環太多次,可能會使當前線程暫停}// VMThread等待所有線程停下來while (_waiting_to_block > 0) {... Safepoint_lock->wait(true, remaining_time / MICROUNITS);}// 安全點開啓成功,設置狀態,計數增加_safepoint_counter ++;_state = _synchronized;OrderAccess::fence();... // 日誌記錄等}VMThread會等待所有線程,直到都達到安全點,此時安全點開啓成功。開啓安全點的核心是線程狀態的轉換,不同線程進入安全點的方式也不盡相同。

1)解釋器線程:第5章提到過,VMThread調用TemplateInterpreter::notice_safepoints通知模板解釋器將模板表切換爲安全點表(這意味着執行完一條字節碼後遇到一個安全點時,可以進入安全點),安全點表除了執行字節碼代碼外還負責安全點處理,其中就包括進入安全點。

2)執行native代碼的線程:VMThread不會暫停執行native代碼的線程,但是當線程從native代碼返回到Java代碼時,需要檢查_state,如果發現是_synchronizing則線程停止。

3)執行編譯後的代碼的線程:開啓安全點後,執行編譯後的代碼的線程使用test指令訪問安全點,此時安全點不可讀,所以引發異常信號,異常信號會被虛擬機的信號處理器(在Linux平臺上是handle_linux_signal)捕獲,然後阻塞線程。

4)已經阻塞的線程:對於已經阻塞的線程,繼續保持阻塞狀態即可,在安全點操作沒有結束前不允許醒來。

5)執行虛擬機內部代碼或者正在狀態轉換的線程:Java線程大部分時間在執行字節碼,有時也會執行虛擬機自身的一些代碼,這些線程會在狀態轉換時阻塞自身。

線程局部握手

上節節的代碼清單10-2展示的代碼中有一個線程局部握手(ThreadLocal Handshakes)標誌,它是JEP 312引入的特性。根據上面的描述,安全點是一個全局的內存頁,一旦VMThread開啓安全點(將內存設置爲不可讀不可寫)後,所有Mutator線程都會繼續運行直到遇到附近的安全點讀取,再通過異常處理機制主動停止。但是有時並不需要停止所有Mutator線程,如偏向鎖撤銷,或者打印某個線程的線程棧,在這些情況下,VMThread只需要停止某個指定的線程並打印線程棧即可。基於這些考慮,HotSpot VM引入了線程局部握手機制,使VMThread可以有選擇性地針對某個線程開啓或者關閉線程局部的安全點。

GC屏障

GC屏障即後綴爲BarrierSet的一系列類,它們的作用是在字段讀操作或者寫操作前後插入一段代碼,執行某些垃圾回收必要的邏輯,如代碼清單10-4所示:

代碼清單10-4 GC屏障

public void barrier(Struct obj){// Write_barreir_pre();obj.field = new Object();// Write_barreir_post();}虛擬機在字段寫操作前後可以分別插入前置寫屏障、後置寫屏障(讀屏障同理),這些寫屏障會執行一些GC必要的邏輯,如檢測到對象引用關係的修改並記錄到記憶集中。GC屏障對性能有較大影響,因爲字段讀寫操作是程序最常見的行爲,所以不應該在GC屏障中放置“重量級”代碼。

本文給大家講解的內容是深入解析java虛擬機:垃圾回收,垃圾回收基礎概述

下篇文章給大家講解的是深入解析java虛擬機:垃圾回收,Epsilon GC;覺得文章不錯的朋友可以轉發此文關注小編;感謝大家的支持!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章