談談Java內存管理

2021-02-15 程序猿

來自:後端技術雜談 _ 颯然Hang

作者:颯然Hang

連結:http://www.rowkey.me/blog/2016/05/07/javamm/(點擊尾部閱讀原文前往)

已獲轉載授權

對於一個Java程式設計師來說,大多數情況下的確是無需對內存的分配、釋放做太多考慮,對Jvm也無需有多麼深的理解的。

但是在寫程序的過程中卻也往往因為這樣而造成了一些不容易察覺到的內存問題,並且在內存問題出現的時候,也不能很快的定位並解決。

因此,了解並掌握Java的內存管理是一個合格的Java程式設計師必需的技能,也只有這樣才能寫出更好的程序,更好地優化程序的性能。

一、背景知識

根據網絡可以找到的資料以及筆者能夠打聽到的消息,目前國內外著名的幾個大型網際網路公司的語言選型概括如下:

Google: C/C++ Go Python Java JavaScript,不得不提的是Google貢獻給java社區的guava包質量非常高,非常值得學習和使用。

Youtube、豆瓣: Python

Fackbook、Yahoo、Flickr、新浪:php(優化過的php vm)

網易、阿里、搜狐: Java、PHP、Node.js

Twitter: Ruby->Java,之所以如此就在於與Jvm相比,Ruby的runtime是非常慢的。並且Ruby的應用比起Java還是比較小眾的。不過最近twitter有往scala上遷移的趨勢。

可見,雖然最近這些年很多言論都號稱java已死或者不久即死,但是Java的語言應用佔有率一直居高不下。

與高性能的C/C++相比,Java具有gc機制,並且沒有那讓人望而生畏的指針,上手門檻相對較低;而與上手成本更低的PHP、Ruby等腳本語言來說,又比這些腳本語言有性能上的優勢(這裡暫時忽略FB自己開發的HHVM)。

對於Java來說,最終是要依靠字節碼運行在jvm上的。目前,常見的jvm有以下幾種:

Sun HotSpot

BEA Jrockit

IBM J9

Dalvik(Android)

其中以HotSpot應用最廣泛。目前sun jdk的最新版本已經到了8,但鑑於新版的jdk使用並未普及,因此本文僅僅針對HotSpot虛擬機的jdk6來講。

二、Jvm虛擬機內存簡介
>>2.1 Java運行時內存區

Java的運行時內存組成如下圖所示:

其中,對於這各個部分有一些是線程私有的,其他則是線程共享的。

線程私有的如下:

①程序計數器

當前線程所執行的字節碼的行號指示器

②Java虛擬機棧

Java方法執行的內存模型,每個方法被執行時都會創建一個棧幀,存儲局部變量表、操作棧、動態連結、方法出口等信息。

每個線程都有自己獨立的棧空間

線程棧只存基本類型和對象地址

方法中局部變量在線程空間中

③本地方法棧

Native方法服務。在HotSpot虛擬機中和Java虛擬機棧合二為一。

線程共享的如下:

①Java堆

存放對象實例,幾乎所有的對象實例以及其屬性都在這裡分配內存。

②方法區

存儲已經被虛擬機加載的類信息、常量、靜態變量、JIT編譯後的代碼等數據。

③運行時常量池

方法區的一部分。用於存放編譯期生成的各種字面量和符號引用。

④直接內存

NIO、Native函數直接分配的堆外內存。DirectBuffer引用也會使用此部分內存。


>>2.2 對象訪問

Java是面向對象的一種程式語言,那麼如何通過引用來訪問對象呢?一般有兩種方式:

①通過句柄訪問

①直接指針

此種方式也是HotSpot虛擬機採用的方式。


>>2.3 內存溢出

在JVM申請內存的過程中,會遇到無法申請到足夠內存,從而導致內存溢出的情況。一般有以下幾種情況:

①虛擬機棧和本地方法棧溢出

②Java堆溢出: 當創建大量對象並且對象生命周期都很長的情況下,會引發OutOfMemoryError

③運行時常量區溢出:OutOfMemoryError:PermGen space,這裡一個典型的例子就是String的intern方法,當大量字符串使用intern時,會觸發此內存溢出

④方法區溢出:方法區存放Class等元數據信息,如果產生大量的類(使用cglib),那麼就會引發此內存溢出,OutOfMemoryError:PermGen space,在使用Hibernate等框架時會容易引起此種情況。


三、垃圾收集
>>3.1 理論基礎在通常情況下,我們掌握java的內存管理就是為了應對網站/服務訪問慢,慢的原因一般有以下幾點:

其中,垃圾收集對性能的影響一般有以下幾個:


先來看垃圾收集的一些基本概念


Concurrent Collector:收集的同時可運行其他的工作進程

Parallel Collector: 使用多CPU進行垃圾收集

Stop-the-word(STW):收集時必須暫停其他所有的工作進程

Sticky-reference-count:對於使用「引用計數」(reference count)算法的GC,如果對象的計數器溢出,則起不到標記某個對象是垃圾的作用了,這種錯誤稱為sticky-reference-count problem,通常可以增加計數器的bit數來減少出現這個問題的機率,但是那樣會佔用更多空間。一般如果GC算法能迅速清理完對象,也不容易出現這個問題。

Mutator:mutate的中文是變異,在GC中即是指一種JVM程序,專門更新對象的狀態的,也就是讓對象「變異」成為另一種類型,比如變為垃圾。

On-the-fly:用來描述某個GC的類型:on-the-fly reference count garbage collector。此GC不用標記而是通過引用計數來識別垃圾。

Generational gc:這是一種相對於傳統的「標記-清理」技術來說,比較先進的gc,特點是把對象分成不同的generation,即分成幾代人,有年輕的,有年老的。這類gc主要是利用電腦程式的一個特點,即「越年輕的對象越容易死亡」,也就是存活的越久的對象越有機會存活下去(薑是老的辣)。


牽扯到垃圾收集,還需要搞清楚吞吐量與響應時間的含義


吞吐量與訪問時間的關係很複雜,有時可能以響應時間為代價而得到較高的吞吐量,而有時候又要以吞吐量為代價得到較好的響應時間。而在其他情況下,一個單獨的更改可能對兩者都有提高。

通常,平均響應時間越短,系統吞吐量越大;平均響應時間越長,系統吞吐量越小; 但是,系統吞吐量越大, 未必平均響應時間越短;因為在某些情況(例如,不增加任何硬體配置)吞吐量的增大,有時會把平均響應時間作為犧牲,來換取一段時間處理更多的請求。

針對於Java的垃圾回收來說,不同的垃圾回收器會不同程度地影響這兩個指標。例如:並行的垃圾收集器,其保證的是吞吐量,會在一定程度上犧牲響應時間。而並發的收集器,則主要保證的是請求的響應時間。

對於GC(垃圾回收)的流程的基本描述如下:

找出堆中活著的對象

釋放死對象佔用的資源

定期調整活對象的位置


GC算法一般有以下幾種:

Mark-Sweep 標記-清除

Mark-Sweep-Compact 標記-整理

Copying Collector 複製算法

Mark-標記

從」GC roots」開始掃描(這裡的roots包括線程棧、靜態常量等),給能夠沿著roots到達的對象標記為」live」,最終所有能夠到達的對象都被標記為」live」,而無法到達的對象則為」dead」。效率和存活對象的數量是線性相關的。

Sweep-清除

掃描堆,定位到所有」dead」對象,並清理掉。效率和堆的大小是線性相關的。

Compact-壓縮

對於對象的清除,會產生一些內存碎片,這時候就需要對這些內存進行壓縮、整理。包括:relocate(將存貨的對象移動到一起,從而釋放出連續的可用內存)、remap(收集所有的對象引用指向新的對象地址)。效率和存活對象的數量是線性相關的。

Copy-複製

將內存分為」from」和」to」兩個區域,垃圾回收時,將from區域的存活對象整體複製到to區域中。效率和存活對象的數量是線性相關的。

其中,Copy對比Mark-sweep


分代收集是目前比較先進的垃圾回收方案

對於分代收集,有以下幾個相關理論

HotSpot虛擬機的分代收集,分為一個Eden區、兩個Survivor去以及Old Generation/Tenured區,其中Eden以及Survivor共同組成New Generatiton/Young space。


分代收集中典型的垃圾收集算法組合描述如下:

年青代通常使用Copy算法收集,會stop the world

老年代收集一般採用Mark-sweep-compact, 有可能會stop the world,也可以是concurrent或者部分concurrent。


>>3.2 HotSpot垃圾收集器

上圖即為HotSpot虛擬機的垃圾收集器組成。


Serial收集器:

此收集器的一個工作流程如下如所示:

收集前:

收集後:


ParNew收集器:

對比Serial收集器如下圖所示:


Parallel Scavenge收集器:
Serial Old收集器:
Parallel Old收集器:

-XX:+UseParallelGC -XX:+UseParallelOldGC啟用此收集器

Server模式的默認老年代收集器

Parallel Scavenge的老年代版本,使用多線程和」mark-sweep」算法

關注點在吞吐量以及CPU資源敏感的場合使用

一般使用Parallel Scavenge + Parallel Old可以達到最大吞吐量保證


CMS收集器:

並發低停頓收集器

四個步驟:

初始標記 Stop the world: 只標記GC roots能直接關聯到的對象,速度很快。

並發標記:進行GC roots tracing,與用戶線程並發進行

重新標記 Stop the world:修正並發標記期間因程序繼續運行導致變動的標記記錄

並發清除

對比serial old收集器如下圖所示:

CMS有以下的缺點:

CMS是唯一不進行compact的垃圾收集器,當cms釋放了垃圾對象佔用的內存後,它不會把活動對象移動到老年代的一端

對CPU資源非常敏感。不會導致線程停頓,但會導致程序變慢,總吞吐量降低。CPU核越多越不明顯

無法處理浮動垃圾。可能出現「concurrent Mode Failure」失敗, 導致另一次full GC ,可以通過調整-XX:CMSInitiatingOccupancyFraction來控制內存佔用達到多少時觸發gc

大量空間碎片。這個可以通過設置-XX:UseCMSCompacAtFullCollection(是否在full gc時開啟compact)以及-XX:CMSFullGCsBeforeCompaction(在進行compact前full gc的次數)


G1收集器:

G1算法在Java6中還是試驗性質的,在Java7中正式引入,但還未被廣泛運用到生產環境中。它的特點如下:

使用標記-清理算法


>>3.3 調優經驗


需要打開gc日誌並讀懂gc日誌:-XX:PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps

垃圾回收的最佳狀態是只有young gc,也就是避免生命周期很長的對象的存在。

從young gc開始,儘量給年青代大點的內存,避免full gc

注意Survivor大小

注意內存牆:4G~5G


GC日誌簡介


老年代使用建議

①Parallel GC(-XX:+UseParallel[Old]GC)

②確實有必要才改成CMS或G1(for old gen collections)


開發建議


①小對象allocate的代價很小,通常10個CPU指令;收集掉新對象也非常廉價;不用擔心活的很短的小對象

②大對象分配的代價以及初始化的代價很大;不同大小的大對象可能導致java堆碎片,尤其是CMS, ParallelGC 或 G1還好;儘量避免分配大對象

③避免改變數據結構大小,如避免改變數組或array backed collections / containers的大小;對象構建(初始化)時最好顯式批量定數組大小;改變大小導致不必要的對象分配,可能導致java堆碎片

④對象池可能潛在的問題


四/Java7、8帶來的一些變化

Java7帶來的內存方面的一個很大的改變就是String常量池從Perm區移動到了Heap中。調用String的intern方法時,如果存在堆中的對象,則會直接保存對象的引用,而不會重新創建對象。

Java7正式引入G1垃圾收集器用於替換CMS。

Java8中,取消掉了方法區(永久代),使用「元空間」替代,元空間只與系統內存相關。

Java 8 update 20所引入的一個很棒的優化就是G1回收器中的字符串去重(String deduplication)。由於字符串(包括它們內部的char[]數組)佔用了大多數的堆空間,這項新的優化旨在使得G1回收器能識別出堆中那些重複出現的字符串並將它們指向同一個內部的char[]數組,以避免同一個字符串的多份拷貝,那樣堆的使用效率會變得很低。可以使用-XX:+UseStringDeduplication這個JVM參數來試一下這個特性。

本文編號1768,以後想閱讀這篇文章直接輸入1768即可。

●本文分類「Java」,搜索分類名可以獲得相關文章。

●輸入m可以獲取到文章目錄

Java編程

安卓開發↓

更多推薦請看15個技術類公眾微信


涵蓋:程序人生、算法與數據結構、黑客技術與網絡安全、大數據技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。傳播計算機學習經驗、推薦計算機優秀資源:點擊前往《值得關注的15個技術類微信公眾號

相關焦點

  • 淺析java內存管理機制
    內存管理是計算機編程中的一個重要問題,一般來說,內存管理主要包括
  • Java內存分配和String類型的深度解析
    一、引題在java語言的所有數據類型中,String類型是比較特殊的一種類型,同時也是面試的時候經常被問到的一個知識點,本文結合java內存分配深度分析關於String的許多令人迷惑的問題。下面是本文將要涉及到的一些問題,如果讀者對這些問題都了如指掌,則可忽略此文。1、java內存具體指哪塊內存?這塊內存區域為什麼要進行劃分?是如何劃分的?
  • java線程前傳——jvm內存結構、內存模型和cpu結構
    ,這個過程是少不了的一個線程肯定是要運行在一個核上的,多個線程可以運行在不同的核上,這個時候,因為緩存的存在,如果沒有同步機制,那一個線程修改了緩存的數據,另一個線程也修改了緩存的數據,這個時候這兩個線程修改後的數據都需要寫入到內存當中,就會出現問題jvm為了方便,將這些緩存抽象出來,構造了自己的內存模型,即主內存和工作內存的數據交互,即java 內存模型(jmm)
  • Java典型面試題 ——談談你對Java平臺的理解?
    今天我要問你的問題是,談談你對 Java 平臺的理解?「Java 是解釋執行」,這句話正確嗎?Java 本身是一種面向對象的語言,最顯著的特性有兩個方面,一是所謂的「書寫一次,到處運行」(Write once, run anywhere),能夠非常容易地獲得跨平臺能力;另外就是垃圾收集(GC, Garbage Collection),Java 通過垃圾收集器(Garbage Collector)回收分配內存
  • Java堆外內存排查小結
    通過本文,你應該了解:pmap 命令gdb 命令perf 命令內存 RSS、VSZ的區別java NMT起因這幾天遇到一個比較奇怪的問題,覺得有必要和大家分享一下。我們的一個服務,運行在docker上,在某個版本之後,佔用的內存開始增長,直到docker分配的內存上限,但是並不會OOM。
  • java創建對象的過程詳解(從內存角度分析)
    java對象的創建操作其實我在《JVM系列之類的加載機制》一文曾經提到過,包含兩個過程:類的初始化和實例化。為此為了理解的深入,我們還需要再來看一下類的生命周期。一張圖表示:從上面我們可以看到,對象的創建其實包含了初始化和使用兩個階段。有了這個印象之後,我們就能開始今天的文章了。
  • Java內存模型與volatile關鍵字
    Java的內存模型大概樣子還是有必要了解下的,今天就學習了下,順便學習了一點volatile關鍵字!Java內存模型主內存中存儲一些可以共享的變量比如實例欄位、靜態欄位和構成數組對象的元素,但是不包括局部變量與方法參數,因為它們是線程私有的,不會被共享。
  • java面試題總結:java的接口類和抽象類的特點
    java的接口類和抽象類的特點java中抽象類的特點:
  • 筆記07 - Java內存模型
    )中的,這一過程如下所示:隨著CPU技術的發展,CPU的執行速度越來越快,但是內存技術並沒有太大的改變,這就導致內存中數據的讀寫速度和CPU處理數據的速度差距越來越大,CPU需要較長時間等待內存的讀寫,這就意味著CPU會出現空轉的情況。
  • Java面試題參考答案I
    簡單談談JVM內存模型,以及volatile關鍵字:雖然java程序所有的運行都是在虛擬機中,涉及到的內存等信息都是虛擬機的一部分,但實際也是物理機的,只不過是虛擬機作為最外層的容器統一做了處理。虛擬機的內存模型,以及多線程的場景下與物理機的情況是很相似的,可以類比參考。
  • 簡述Java內存模型
    2.3.1什麼是Java內存模型在介紹Java內存模型(JMM)前,我要打消讀者一個錯誤的認知,那就是JMM與JVM到底是什麼關係,現在告訴大家,Java虛擬機模型(JVM)與Java內存模型(JMM)沒有本質上的聯繫。
  • 面試官:小夥子先來說一下可能引起Java內存洩露的場景吧
    本文分析一下可能引起java內存洩露的場景:通過 finalize() 方法終結器finalizers的使用是潛在內存洩漏問題的另一個來源。每當類的 finalize() 方法被重寫時,該類的對象不會立即被垃圾回收。相反,GC將它們排隊等待最後確定,這將在稍後的時間點發生。
  • JVM中的五大內存區域劃分詳解及快速掃盲
    2.內存如何管理?3.線程資源如何利用?腦袋裡有個印象即可,帶著問題去學習。4. 運行java文件的大概流程想要運行java的源文件,必須要經過javac編譯器編譯成.class文件,也就是字節碼文件。
  • JVM內存區域之線程私有區域
    概述:對於從事C、C++開發的程式設計師來說,在內存管理領域,他們既是擁有最高權力的「皇帝」,又是從事最基礎工作的勞動人民——既擁有每個對象的「所有權」,又擔負著每一個對象從開始到終結的維護職責。對於java程式設計師來說,在虛擬機自動內存管理機制的幫助下,不再需要為沒一個new操作去配對的free/delete(C、C++語言對對象的刪除和內存釋放操作),不容易出現內存洩漏和內存溢出問題,看起來由虛擬機管理內存一切看起來很美好。
  • Java面試寶典---Java基礎部分(7)
    java的內存分為兩類,一類是棧內存,一類是堆內存。棧內存是指程序進入一個方法時,會為這個方法單獨分配一塊私屬存儲空間,用於存儲這個方法內部的局部變量,當這個方法結束時,分配給這個方法的棧會釋放,這個棧中的變量也將隨之釋放。堆是與棧作用不同的內存,一般用於存放不放在當前方法棧中的那些數據,例如,使用new創建的對象都放在堆裡,所以,它不會隨方法的結束而消失。
  • JAVA生成JPG縮略圖
    為了管理方便,大家可能不願意每次都用ACDsee修改它,而直接上傳到伺服器。但是這種做法在客戶端看來就沒有那麼輕鬆了,對於撥號上網的用戶簡直是一場惡夢,雖然你可以在圖片區域設置wide和high!    問題的解決之道來了!我們可以在類中處理一張大圖,並縮小它。  前提是需要JDK1.4,這樣才能進行處理。
  • 為什麼都說java比較容易入門?
    先來看看,C語言的特性吧:1.有方法,沒有類的概念;2.具有強數據類型的概念;3.具有強正負號的概念;4.具有強大小寫的概念;5.指針與內存管理的概念;下面在來看看C++的特性:1.有方法,有類的概念(有類就有多態,繼承與封裝);2.具有強數據類型的概念;3.具有強正負號的概念;4.具有強大小寫的概念;5.指針與內存管理的概念
  • 給Java新手的一些建議——Java知識點歸納(Java基礎部分)
    在JVM這個大類中,我認為需要掌握的知識有:JVM內存模型和結構GC原理,性能調優調優:Thread Dump, 分析內存結構class 二進位字節碼結構, class loader 體系 , class加載過程 , 實例創建過程方法執行過程Java各個大版本更新提供的新特性(需要簡單了解)
  • JAVA應用可視化診斷工具
    JConsole:Java監視與管理控制臺通過JDK/bin目錄下的jconsole.exe啟動1.內存監控:相當於可視化的jstat命令監控區域有PS Eden SpacePS Survivor SpacePS Old SpaceMetaspaceCode Cache
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    java源文件編譯為class字節碼 類加載器把字節碼加載到虛擬機的方法區。 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口 加載階段完成後,這些二進位字節流按照虛擬機所需的格式存儲在方法區之中。