91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何理解JVM和垃圾回收

發布時間:2021-10-21 16:32:21 來源:億速云 閱讀:132 作者:iii 欄目:編程語言

這篇文章主要講解了“如何理解JVM和垃圾回收”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何理解JVM和垃圾回收”吧!

JVM的發展

就跟我們用三方庫一樣,同樣的功能有不同的實現。JVM也是一樣的,第一款JVM是Sun公司的Classic VM,JDK1.2之前JVM都是采用的Classic VM,而之后,逐漸被我們都知道的HotSpot給替代,直到JDK1.4,Classic VM才完全被棄用。

HotSpot應該是目前使用最廣泛的虛擬機(自信點,把應該去掉),也是OpenJDK中所帶的虛擬機。但是你可能不知道,HotSpot最開始并不是由Sun公司開發,而是由一家小公司設計并實現的,而且最初也不是為Java語言設計的。Sun公司看到了這個虛擬機在JIT上的優勢,于是就收購了這家公司,從而獲得了HotSpot VM。

運行時內存區域

可能你經歷過被靈魂拷問是什么滋味,如果線上發生了OOM(Out Of Memory),該怎么排查?如果要你來對一個JVM的運行參數進行調優,你該怎么做?

不像C++可以自己來主宰內存,同時扮演上帝和最底層勞工的角色,Java里我們把內存管理交給了JVM,如果我們不能了解其中具體的運行時內存分布以及垃圾回收的原理,那等到問題真正出現了,很可能就無從查起。這也是要深入的了解JVM的必要性。

Java在運行時會將內存分成如下幾個區域進行管理,方法區虛擬機棧本地方法棧程序計數器

堆(Java Heap)是JVM所管理的內存中最大的一塊了。我們平常開發中使用new關鍵字來進行實例化的對象幾乎都會在堆中分配內存,所有線程都可以共享被分配在堆上的對象。

堆也是JVM垃圾回收的主要區域,正因為垃圾回收的分代機制,其實堆中還可以分為更細的新生代、老年代。GC這塊后面會細講。

那為什么是幾乎呢?在JVM本身的規范中是規定了所有的對象都會在堆上分配內存的,但是隨著JIT(Just In Time)編譯器和逃逸分析技術的成熟,所有對象都在堆上分配內存就變得沒有那么絕對了。

JIT編譯器

不知道你有沒有聽說過,二八定律在我們的程序中也同樣適用,那就是20%的代碼占用了系統運行中80%的資源。在我們寫的代碼中,就可能會存在一些熱點代碼,頻繁的被調用。除了被頻繁的調用的代碼,還有被執行多次的循環體也算熱點代碼。

那此時JIT編譯器就會對這部分的代碼進行優化,將它們編譯成Machine Code,并做一些對應的優化。不熟悉的同學可能會說,我們的代碼不都已經被編譯成了字節碼了嗎?怎么又被編譯成了Machine Code?

因為字節碼只是一個中間狀態,真正的運行是JVM在運行的時候,就跟解釋型語言一樣將字節碼逐條的翻譯成了Machine Code,這個Machine Code才是操作系統能夠識別直接運行的指令。而JIT就會把編譯好的熱點代碼所對應的Machine Code保存下來, 下載再調用時就省去了從字節碼編譯到Machine Code的過程,效率自然也就提高了。

逃逸分析

我們剛剛提到過,Java中幾乎所有的對象都在堆上分配空間,堆中的內存空間是所有線程共享的,所以在多線程下才需要去考慮同步的相關問題。那如果這個變量是個局部變量,只會在某個函數中被訪問到呢?

這種局部變量就是未逃逸的變量,而這個變量如果在其他的地方也能被訪問到呢?這說明這個變量逃逸出了當前的作用域。通過逃逸分析我們可以知道哪些變量沒有逃逸出當前作用域,那這個對象內存就可以在棧中分配,隨著調用的結束,隨著線程的繼續執行完成,棧空間被回收,這個局部變量分配的內存也會一起被回收。

方法區

方法區存放了被加載的Class信息、常量、靜態變量和JIT編譯之后的結果等數據,與堆一樣,方法區也是被所有線程共享的內存區域。但與堆不同,相對于堆的GC力度,這塊的垃圾回收力度可以說是小了非常多,但是仍然有針對常量的GC。

虛擬機棧

虛擬機棧是線程私有的,所以在多線程下不需要做同步的操作,是線程安全的。當每個方法執行時,就會在當前線程中虛擬機棧中創建一個棧幀,每個方法從調用到結束的過程,就對應了棧幀在虛擬機棧中的入棧、出棧的過程。那自然而然,棧幀中應該存放的就是方法的局部變量操作數棧動態鏈接和對應的返回信息

不知道你遇到過在方法內寫遞歸時,由于退出條件一直沒有達到,導致程序陷入了無限循環,然后就會看到程序拋出了一個StackOverflow的錯誤。其所對應的棧就是上面提到的操作數棧。

當然這是在內存足夠的情況下,如果內存不夠,則會直接拋出OutOfMemory,也就是常說的OOM。

本地方法棧

本地方法棧的功能與虛擬機棧類似,區別在于虛擬機棧是服務于JVM中的Java方法,而本地方法棧則服務于Native的方法。

GC

其實堆中的區域還可以劃分為新生代和老年代,再分割的細一點,可以到Eden、From Survivor、To Survivor。首先分配的對象實例會到Eden區,在新生代這塊區域一般是最大的,與From Survivor的比例是8:1,當然這個比例可以通過JVM參數來改變。而且當分配的對象實體很大的時候將會直接進入到老年代。

為什么要對堆進行更加細致的內存區域劃分,其實是為了讓垃圾回收更加的高效。

垃圾識別機制

那JVM是如何判斷哪些對象是“垃圾”需要被回收呢?我們就需要來了解一下JVM是如何來判斷哪些內存需要進行回收的。

引用計數

實現的思路是,給每個對象添加一個引用計數器,每當有其他的對象引用了這個對象,就把引用計數器的值+1,如果一個對象的引用計數為0則說明沒有對象引用它。

乍一看是沒有問題的,那為什么Java并沒有采取這種呢?

想象一下這個場景,一個函數中定義了兩個對象O1和O2,然后O1引用了O2,O1又引用了O1,這樣一來,兩個對象的引用計數器都不為0,但是實際上這兩個對象再也不會被訪問到了。

所以我們需要另外一種方案來解決這個問題。

可達性分析

可達性分析可以理解為一棵樹的遍歷,根節點是一個對象,而其子節點是引用了當前對象的對象。從根節點開始做遍歷,如果發現從所有根節點出發的遍歷都已經完成了,但是仍然有對象沒有被訪問到,那么說明這些對象是不可用的,需要將內存回收掉。

這些根節點有個名字叫做GC Roots,哪些資源可以被當作GC Roots呢?

  • 棧幀中的局部變量所引用的對象

  • 方法區中類靜態屬性所引用的對象

  • 方法區中常量所引用的對象

  • 本地方法棧所引用的對象

我們剛剛聊過,在引用計數中,如果其引用計數器的值為0,則占用的內存會被回收掉。而在可達性分析中,如果沒有某個對象沒有任何引用,它也不一定會被回收掉。

垃圾回收算法

聊完了JVM如何判斷一個對象是否需要回收,接下來我們再聊一下JVM是如何進行回收的。

標記-清除

顧名思義,其過程分為兩個階段,分別是標記清除。首先標記出所有需要回收的對象,然后統一對標記的對象進行回收。這個算法的十分的局限,首先標記和清除的兩個過程效率都不高,而且這樣的清理方式會產生大量的內存碎片,什么意思呢?

就是雖然總體看起來還有足夠的剩余內存空間,但是他們都是以一塊很小的內存分散在各個地方。如果此時需要為一個大對象申請空間,即使總體上的內存空間足夠,但是JVM無法找到一塊這么大的連續內存空間,就會導致觸發一次GC。

如何理解JVM和垃圾回收

復制

其大致的思路是,將現有的內存空間分為兩半A和B,所有的新對象的內存都在A中分配,然后當A用完了之后,就開始對象存活判斷,將A中還存活的對象復制到B去,然后一次性將A中的內存空間回收掉。

如何理解JVM和垃圾回收

這樣一來就不會出現使用標記-清除所造成的內存碎片的問題了。但是,它仍然有自己的不足。那就是以內存空間縮小了一半為代價,而在某些情況下,這種代價其實是很高的。

堆中新生代就是采用的復制算法。剛剛提到過,新生代被分為了Eden、From Survivor、To Survivor,由于幾乎所有的新對象都會在這里分配內存,所以Eden區比Survivor區要大很多。因此Eden區和Survivor區就不需要按照復制算法默認的1:1的來分配內存。

在HotSpot中Eden和Survivor的比例默認是8:1,也就意味著只有10%的空間會被浪費掉。

看到這你可能會發現一個問題。

既然你的Eden區要比Survivor區大這么多,要是一次GC之后的存活對象的大小大于Survivor區的總大小該怎么處理?

的確,在新生代GC時,最壞的情況就是Eden區的所有對象都是存活的,那這個JVM會怎么處理呢?這里需要引入一個概念叫做內存分配擔保

當發生了上面這種情況,新生代需要老年代的內存空間來做擔保,把Survivor存放不下的對象直接存進老年代中。

標記-整理

標記-整理其GC的過程與標記-清楚是一樣的,只不過會讓所有的存活對象往同一邊移動,這樣一來就不會像標記-整理那樣留下大量的內存碎片。

如何理解JVM和垃圾回收

分代收集

這也是當前主流虛擬機所采用的算法,其實就是針對不同的內存區域的特性,使用上面提到過的不同的算法。

例如新生代的特性是大部分的對象都是需要被回收掉的,只有少量對象會存活下來。所以新生代一般都是采用復制算法

而老年代屬于對象存活率都很高的內存空間,則采用標記-清除標記-整理算法來進行垃圾回收。

垃圾收集器

新生代收集器

聊完了垃圾回收的算法,我們需要再了解一下GC具體是通過什么落地的, 也就是上面的算法的實際應用。

Serial

Serial采用的是復制算法的垃圾收集器,而且是單線程運作的。也就是說,當Serial進行垃圾收集時,必須要暫停其他所有線程的工作,直到垃圾收集完成,這個動作叫STW(Stop The World) 。Golang中的GC也會存在STW,在其標記階段的準備過程中會暫停掉所有正在運行的Goroutine。

而且這個暫停動作對用戶來說是不可見的,用戶可能只會知道某個請求執行了很久,沒有經驗的話是很難跟GC掛上鉤的。

但是從某些方面來看,如果你的系統就只有單核,那么Serial就不會存在線程之間的交互的開銷,可以提高GC的效率。這也是為什么Serial仍然是Client模式下的默認新生代收集器。

如何理解JVM和垃圾回收

ParNew

ParNew與Serial只有一個區別,那就是ParNew是多線程的,而Serial是單線程的。除此之外,其使用的垃圾收集算法和收集行為完全一樣。

該收集器如果在單核的環境下,其性能可能會比Serial更差一些,因為單核無法發揮多線程的優勢。在多核環境下,其默認的線程與CPU數量相同。

如何理解JVM和垃圾回收

Parallel Scavenge

Parallel Scavenge是一個多線程的收集器,也是在server模式下的默認垃圾收集器。上面的兩種收集器關注的重點是如何減少STW的時間,而Parallel Scavenge則更加關注于系統的吞吐量

例如JVM已經運行了100分鐘,而GC了1分鐘,那么此時系統的吞吐量(100 - 1)/100 = 99%

吞吐量短停頓時間其側重的點不一樣,需要根據自己的實際情況來判斷。

高吞吐量

GC的總時間越短,系統的吞吐量則越高。換句話說,高吞吐量則意味著,STW的時間可能會比正常的時間多一點,也就更加適合那種不存在太多交互的后臺的系統,因為對實時性的要求不是很高,就可以高效率的完成任務。

短停頓時間

STW的時間短,則說明對系統的響應速度要求很高,因為要跟用戶頻繁的交互。因為低響應時間會帶來較高的用戶體驗。

老年代收集器

Serial Old

Serial Old是Serial的老年代版本,使用的標記-整理算法, 其實從這看出來,新生代和老年代收集器的一個差別。

新生代:大部分的資源都是需要被回收

老年代:大部分的資源都不需要被回收

所以,新生代收集器基本都是用的復制算法,老年代收集器基本都是用的標記-整理算法。

Serial Old也是給Client模式下JVM使用的。

Parallel Old

Parallel Old是Parallel Scavenge的老年代版本,也是一個多線程的、采用標記-整理算法的收集器,剛剛討論過了系統吞吐量,那么在對CPU的資源十分敏感的情況下, 可以考慮Parallel Scavenge和Parallel Old這個新生代-老年代的垃圾收集器組合。

如何理解JVM和垃圾回收

CMS

CMS全稱(Concurrent Mark Sweep),使用的是標記-清除的收集算法。重點關注于最低的STW時間的收集器,如果你的應用非常注重與響應時間,那么就可以考慮使用CMS。

如何理解JVM和垃圾回收

從圖中可以看出其核心的步驟:

  • 首先會進行初始標記,標記從GCRoots出發能夠關聯到的所有對象,此時需要STW,但是不需要很多時間

  • 然后會進行并發標記,多線程對所有對象通過GC Roots Tracing進行可達性分析,這個過程較為耗時

  • 完成之后會重新標記,由于在并發標記的過程中,程序還在正常運行,此時有些對象的狀態可能已經發生了變化,所以需要STW,來進行重新標記,所用的時間大小關系為初始標記 < 重新標記 < 并發標記

  • 標記階段完成之后,開始執行并發清楚

CMS是一個優點很明顯的的垃圾收集器,例如可以多線程的進行GC,且擁有較低的STW的時間。但是同樣的,CMS也有很多缺點。

缺點

我們開篇也提到過,使用標記-清除算法會造成不連續的內存空間,也就是內存碎片。如果此時需要給較大的對象分配空間,會發現內存不足,重新觸發一次Full GC。

其次,由于CMS可能會比注重吞吐量的收集器占用更多的CPU資源,但是如果應用程序本身就已經對CPU資源很敏感了,就會導致GC時的可用CPU資源變少,GC的整個時間就會變長,那么就會導致系統的吞吐量降低。

G1

G1全稱Garbage First,業界目前對其評價很高,JDK9中甚至提議將其設置為默認的垃圾收集器。我們前面講過,Parallel Scavenge更加關注于吞吐量,而CMS更加關注于更短的STW時間,那么G1就是在實現高吞吐的同時,盡可能的減少STW的時間。

我們知道,上面聊過的垃圾收集器都會把連續的堆內存空間分為新生代、老年代,新生代則被劃分的更加的細,有Eden和兩個較小的Survivor空間,而且都是連續的內存空間。而G1則與眾不同,它引入了新的概念,叫Region

Region是一堆大小相等但是不連續的內存空間,同樣是采用了分代的思想,但是不存在其他的收集器的物理隔離,屬于新生代和老年代的region分布在堆的各個地方。

如何理解JVM和垃圾回收

上面H則代表大對象,也叫Humongous Object。為了防止大對象的頻繁拷貝,會直接的將其放入老年代。G1相比于其他的垃圾收集器有什么特點呢?

從宏觀上來看,其采用的是標記-整理算法, 而從region到region來看,其采用的是復制算法的,所以G1在運行期間不會像CMS一樣產生內存碎片。

除此之外,G1還可以通過多個CPU,來縮短STW的時間,與用戶線程并發的執行。并且可以建立可預測的停頓時間模型,讓使用者知道在某個時間片內,消耗在GC上的時間不得超過多少毫秒。之所以G1能夠做到這點,是因為沒像其余的收集器一樣收集整個新生代和老年代,而是在有計劃的避免對整個堆進行全區域的垃圾收集。

總結

這個圖來自于參考中的博客,總結的很到位。

收集器串行、并行or并發新生代/老年代算法目標適用場景
Serial串行新生代復制算法響應速度優先單CPU環境下的Client模式
Serial Old串行老年代標記-整理響應速度優先單CPU環境下的Client模式、CMS的后備預案
ParNew并行新生代復制算法響應速度優先多CPU環境時在Server模式下與CMS配合
Parallel Scavenge并行新生代復制算法吞吐量優先在后臺運算而不需要太多交互的任務
Parallel Old并行老年代標記-整理吞吐量優先在后臺運算而不需要太多交互的任務
CMS并發老年代標記-清除響應速度優先集中在互聯網站或B/S系統服務端上的Java應用
G1并發both標記-整理+復制算法響應速度優先面向服務端應用,將來替換CMS


感謝各位的閱讀,以上就是“如何理解JVM和垃圾回收”的內容了,經過本文的學習后,相信大家對如何理解JVM和垃圾回收這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

莱芜市| 沈丘县| 红安县| 三穗县| 龙口市| 永康市| 嘉鱼县| 宜兰市| 宾阳县| 会东县| 东乡县| 偏关县| 泽库县| 衡东县| 景谷| 定边县| 开封县| 郯城县| 奉节县| 龙山县| 桓仁| 高安市| 海阳市| 中阳县| 界首市| 无锡市| 正蓝旗| 大庆市| 大新县| 岱山县| 古交市| 鄯善县| 德江县| 竹溪县| 稷山县| 同心县| 漳浦县| 万山特区| 长寿区| 安达市| 开江县|