這個題很難回答,你的知識儲備要比較強了,他可擴展性太強了,咱們開始講解 :
JVM內存管理分為兩部分:內存分配、內存回收、內存回收經常也被叫做垃圾回收。
很多人迷惑一個問題,既然Java采用自動內存管理,程序員不用關心內存管理的細節,那么為什么我們仍然需要了解Java內存管理的內幕?
1.了解Java內存管理的細節,有助于程序員編寫出性能更好的程序。
比如,在新的線程創建時,JVM會為每個線程創建一個專屬的棧(stack),其棧是先進后出的數據結構,這種方式的特點,讓程序員編程時,必須特別注意遞歸方法要盡量少使用,另外棧的大小也有一定的限制,如果過多的遞歸,容易導致stack overflow。
2.了解Java內存管理的細節,一旦內存管理出現問題,有助于找到問題的根本原因所在。
3.了解Java內存管理的內幕,有助于優化JVM,使自己的應用獲得最好性能體驗。
內存---Java中哪些組件用到內存---內存分配機制---內存回收機制
內存:物理內存和虛擬內存,物理內存就是常說的RAM(隨機存儲器),操作系統作為我們管理計算機物理內存的接口,我們通常都是通過調用計算機操作系統來訪問內存的,在Java中,甚至不需要寫和內存相關的代碼。通常操作系統管理內存申請空間是按照進程來管理的,每個進程都有一段獨立的空間,互不重合,互不訪問。這里所說的內存空間的獨立是指邏輯上的獨立,由操作系統來保證的。虛擬內存的出現使得多個進程可以同時運行時共享物理內存,空間上共享,邏輯仍然互不訪問。虛擬內存提高了內存利用率,擴展了內存地址空間。
內核空間和用戶空間:通常一個4GB的物理內存地址空間并不能完全被使用,因為它被劃分為兩部分:內核空間和用戶空間【內核空間】主要是指操作系統運行時用于程序調度、虛擬內存的使用或者連接硬件資源的程序邏輯。【用戶空間】是用戶運行程序能夠申請使用的空間。
為什么這么劃分呢?
1)有效抵御惡意用戶的窺探,也能防止質量低劣的用戶程序的侵害,從而使系統運行得更穩定可靠。
2)用戶空間與內核空間的權限不同,內核空間擁有所有硬件設備的權限,用戶空間只有普通硬件的權限;兩者隔離可以防止用戶程序直接訪問硬件資源。
但是,每一次系統調用都會在兩個內存空間之間切換,通過網絡傳輸的數據首先被接收到內核空間,然后再從內核空間復制到用戶空間供用戶使用。這樣比較費時,雖然保證了程序運行的安全性和穩定性,但同時也犧牲了一部分效率。(后來出現了一些列優化技術,如Linux提供的sendfile文件傳輸方式)
另外:Windows32位操作系統【內核空間:用戶空間=1:1】、Linux32位操作系統【內核空間:用戶空間=1:3】
Java中的內存就是從物理內存中申請下來的內存,它怎么被劃分的呢?
Java內存模型:一個Java程序的具體執行流程如下:
首先Java源代碼文件(.java后綴)會被Java編譯器編譯為字節碼文件(.class后綴),然后由JVM中的類加載器加載各個類的字節碼文件,加載完畢之后,交由JVM執行引擎執行。在整個程序執行過程中,JVM會用一段空間來存儲程序執行期間需要用到的數據和相關信息,這段空間一般被稱作為Runtime Data Area(運行時數據區),也就是我們常說的JVM內存。
我們常說的Java內存管理就是指這塊區域的內存分配和回收,那么,這塊兒區域具體是怎么劃分的呢?
根據《Java虛擬機規范》的規定,運行時數據區通常包括這幾個部分:
程序計數器(ProgramCounter Register)
Java棧(VM Stack)
本地方法棧(Native MethodStack)
方法區(Method Area)
堆(Heap)
Java堆和方法區是所有線程共享(所有執行引擎可訪問);
【Java堆】用于存儲Java對象,每個Java對象都是這個對象類的副本,會復制包含繼承自它父類的所有非靜態屬性。
【方法區】用于存儲類結構信息,class文件加載進JVM時會被解析成JVM識別的幾個部分分別存儲在不同的數據結構中:常量池、域、方法數據、方法體、構造函數,包括類中的方法、實例初始化、接口初始化等。
方法區被JVM的GC回收器管理,但是比較穩定,并沒有那么頻繁的被GC回收。
java棧和PC寄存器(程序計數器)是線程私有,每個執行引擎啟動時都會創建自己的java棧和PC寄存器;
【Java棧】和線程關聯,每個線程創建的時候,JVM都會為他分配一個對應的Java棧,這個棧含有多個棧幀;棧幀則是個方法關聯,每個方法的運行都會創建一個自己的棧幀,含有內存變量,操作棧、方法返回值。(用于存儲方法參數、局部變量、方法返回值和運算中間結果)
【PC寄存器】則用于記錄下一條要執行的字節碼指令地址和被中斷地址。如果方法是 native的,程序計數器的值不會被定義為空。
【本地方法棧】是為JVM運行Native方法(本地方法:非java語言編寫的方法,被編譯成和處理器相關的代碼)準備的空間,類似于Java棧。
【運行時常量池】關于這個東西要明白三個概念:
常量池(Constant Pool):常量池數據編譯期被確定,是Class文件中的一部分。存儲了類、方法、接口等中的常量,當然也包括字符串常量。
字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中產生的字符串類型數據。
運行時常量池(Runtime Constant Pool):方法區的一部分,所有線程共享。虛擬機加載Class后把常量池中的數據放入到運行時常量池。
Java中哪些組件用到內存
Java堆:Java堆用于存儲Java對象,在JVM啟動時就一次性申請到固定大小的空間,所以,一旦分配,大小不變。
內存空間管理:JVM
對象創建:Java應用程序
對象所占空間釋放:垃圾收集器
線程:JVM運行實際程序的實體就是線程,每個線程創建的時候JVM都為它創建了私有的堆棧和程序計數器(或者叫做PC寄存器);很多應用程序是根據CPU的核數來分配創建的線程數。
類和類加載器:Java中的類和類加載器同樣需要存儲空間,被存儲在永久代(PermGen區)當中。
JVM加載類方式:按需加載,只加載那些你在程序中明確使用到的類,通常只加載一次,如果一直重復加載,可能會導致內存泄露,所以也要注意對PernGen區失效類的卸載內存回收問題。
通常PernGen區滿足內存回收的條件為:
1) 堆中沒有對該類加載器的引用;(java.lang.ClassLoader對象)
2) 堆中沒有對類加載器加載的類的引用;(java.lang.Class對象)
3) 該類加載器加載的類的所有實例化的對象不再存活。
NIO:使用java.nio.ByteBuffer.allocateDirect()方法分配內存,每次分配內存都會調用操作系統函數os::malloc(),所以,分配的內存是本機的內存而不是Java堆上的內存;
另外利用該方法產生的數據和網絡、磁盤發生交互的時候都是在內核空間發生的,不需要復制到用戶空間Java內存中,這種技術避免了Java堆和本機堆之間的數據復制;但是利用該方法生成的數據會作為Java堆GC的一部分來自動清理本機緩沖區。
JNI:技術使本機代碼可調用java代碼,Java代碼的運行本身也依賴于JNI代碼來實現類庫功能,所以JNI也增加內存占用。
JVM內存分配與回收:內存分配、通常的內存分配策略、操作系統中內存分配策略通常分為三類:
【靜態內存分配】編譯時就分配了固定的內存空間(編譯器確定所需空間大小),不允許有可變數據和遞歸嵌套等情況,這樣難以計算具體空間;
【棧內存分配】在程序運行時進入一個程序模塊(程序入口處確定空間大小)知道一個程序模塊分配所需數據區大小并為之分配內存。
【堆內存分配】在程序運行到相應代碼是才會知道所需空間大小。(運行時確定空間大小)
很明顯,三種分配策略中,堆內存分配策略最自由,但是效率也是比較差的。
Java內存分配: 在Java程序運行過程中,JVM定義了各種區域用于存儲運行時數據。其中的有些數據區域在JVM啟動時創建,并只在JVM退出時銷毀;其它的數據區域與每個線程相關。這些數據區域,在線程創建時創建,在線程退出時銷毀。
棧和線程:JVM是基于棧的虛擬機,為每個新創建的線程都分配一個棧,也就是說一個Java程序來說,它的運行就是通過對棧的操作來完成的。棧以幀為單位保存線程的狀態。JVM對棧只進行兩種操作:以幀為單位的壓棧和出棧操作。
某個線程正在執行的方法稱為此線程的當前方法,當前方法使用的幀稱為當前幀。當線程激活一個Java方法,JVM就會在線程的 Java堆棧里新壓入一個幀。這個幀自然成為了當前幀.在此方法執行期間,這個幀將用來保存參數,局部變量,中間計算過程和其他數據。這個幀在這里和編譯原理中的活動紀錄的概念是差不多的。
從Java的這種分配機制來看,可以這樣理解:棧(Stack)是操作系統在建立某個進程時或者線程(在支持多線程的操作系統中是線程)為這個線程建立的存儲區域,該區域具有先進后出的特性。
堆和棧的區別:棧(stack)與堆(heap)都是Java用來在Ram中存放數據的地方 。與C++不同,Java自動管理棧和堆,程序員不能直接地設置棧或堆。
棧的優勢:是存取速度比堆要快 ,僅次于直接位于CPU中的寄存器,缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據可 以共享,詳見第4點。
堆的優勢:是可以動態地分配內存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由于要在運行時動態分配內存,存取速度較慢。
兩者存儲數據類型不同:堆是一個運行時數據區,存放通過new、newayyray.anewarray和mulitanewarray等指令建立的對象,無需代碼顯式的釋放;棧中存放一些基本類型的變量數據和對象句柄(引用);
Java中所有對象的存儲空間都是在堆中分配的,但是這個對象的引用卻是在堆棧中分配;也就是說在建立一個對象時從兩個地方都分配內存,在堆中分配的內存實際建立這個對象,而在堆棧中分配的內存只是一個指向這個堆對象的指針(引用)。
JVM內存回收:幾個問題要搞清楚
問題一:什么叫垃圾回收機制?
垃圾回收是一種動態存儲管理技術,它自動地釋放不再被程序引用的對象,按照特定的垃圾收集算法來實現資源自動回收的功能。當一個對象不再被引用的時候,內存回收它占領的空間,以便空間被后來的新對象使用,以免造成內存泄露。
問題二:java的垃圾回收有什么特點?
Java語言不允許程序員直接控制內存空間的使用。內存空間的分配和回收都是由JRE負責在后臺自動進行的,尤其是無用內存空間的回收操作(garbagecollection,也稱垃圾回收),只能由運行環境提供的一個超級線程進行監測和控制。
問題三:垃圾回收器什么時候會運行?
一般是在CPU空閑或空間不足時自動進行垃圾回收,而程序員無法精確控制垃圾回收的時機和順序等。
問題四:什么樣的對象符合垃圾回收條件?
當沒有任何獲得線程能訪問一個對象時,該對象就符合垃圾回收條件。
問題五:垃圾回收器是怎樣工作的?
垃圾回收器如發現一個對象不能被任何活線程訪問時,他將認為該對象符合刪除條件,就將其加入回收隊列,但不是立即銷毀對象,何時銷毀并釋放內存是無法預知的。垃圾回收不能強制執行,然而java提供了一些方法(如:System.gc()方法),允許你請求JVM執行垃圾回收,而不是要求,虛擬機會盡其所能滿足請求,但是不能保證JVM從內存中刪除所有不用的對象。
問題六:一個java程序能夠耗盡內存嗎?
可以。垃圾收集系統嘗試在對象不被使用時把他們從內存中刪除。然而,如果保持太多活動對象,系統則可能會耗盡內存。垃圾回收器不能保證有足夠的內存,只能保證可用內存盡可能的得到高效的管理。
問題七:程序中的數據類型不一樣存儲地方也不一樣,原生數據類型存儲在java棧中,方法執行結束就會消失;對象類型存儲在Java堆中,可以被共享,不一定隨著方法執行結束而消失。
問題八:如何檢測垃圾?(垃圾檢測機制)
垃圾收集器的兩個任務:正確檢測出垃圾對象和釋放垃圾對象占用的內存空間,而前者是關鍵所在。
垃圾收集器有一個根對象集合,包含的元素:1)方法中局部變量的引用;2)Java操作棧中的對象引用;3)常量池中的對象引用;4)本地方法持有的對象引用;5)類的class對象。
JVM在垃圾回收的時候會檢查堆中的所有對象是否會被根對象直接或間接的引用,能夠被根對象到達的叫做活動對象,否則叫做非活動對象可以被回收。內存回收- gc原理: jvm內存回收采用的是基于分代的垃圾收集算法
Sun的JVM Generational Collecting(垃圾回收)原理是這樣的:把對象分為年青代(Young)、年老代(Tenured)、持久代(Perm),對不同生命周期的對象使用不同的算法。(基于對象生命周期分析)
【設計思路】:把對象按照壽命長短來分組,分為年輕代和年老代,新創建的對象被分在年輕代,如果對象經過幾次回收后仍然存活,那么再把這個對象劃分到年老代。年老代的收集頻度沒有那么頻繁,這樣就減少了每次垃圾收集時所需要的掃描的對象和數量,從而提高垃圾回收效率。
Young(年輕代)
年輕代分三個區。一個Eden區,兩個Survivor區。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被復制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被復制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區復制過來的并且此時還存活的對象,將被復制年老區(Tenured,需要注意,Survivor的兩個區是對稱的,沒先后關系,所以同一個區中可能同時存在從Eden和Survivor區復制過來的對象,而復制到年老區的只有從第一個Survivor過來的對象,而且,Survivor區總有一個是空的。
Tenured(年老代)
年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象;如果Tenured區(old區)也滿了,就會觸發Full GC回收整個堆內存。
Perm(持久代)
用于存放類的Class文件或靜態文件,如Java類、方法等,垃圾回收是由FullGC觸發的。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設置。
舉個例子:當在程序中生成對象時,正常對象會在年輕代中分配空間,如果是過大的對象也可能會直接在年老代生成(據觀測在運行某程序時候每次會生成一個十兆的空間用收發消息,這部分內存就會直接在年老代分配)。年輕代在空間被分配完的時候就會發起內存回收,大部分內存會被回收,一部分幸存的內存會被拷貝至Survivor的from區,經過多次回收以后如果from區內存也分配完畢,就會也發生內存回收然后將剩余的對象拷貝至to區。等到to區也滿的時候,就會再次發生內存回收然后把幸存的對象拷貝至年老區。
通常我們說的JVM內存回收總是在指堆內存回收,確實只有堆中的內容是動態申請分配的,所以以上對象的年輕代和年老代都是指的JVM的Heap空間,而持久代則是之前提到的MethodArea,不屬于Heap。
關于JVM內存管理我們需要注意的幾個地方:
1、程序中的無用對象、中間對象置為null,可加快內存回收。
2、對象池技術如果生成的對象是可重用的對象,只是其中的屬性不同時,可以考慮采用對象池減少對象的生成。如果對象池中有空閑的對象就取出使用,沒有則生成新的對象,提高對象復用率。
3、JVM性能調優通過配置JVM的參數來提高垃圾回收的速度,如果在沒有出現內存泄露且上面兩種辦法都不能保證JVM內存回收時,可以考慮采用JVM調優 的方式來解決,不過一定要經過實體機的長期測試,因為不同的參數可能引起不同的效果。如-Xnoclassgc參數等。
jvm的垃圾回收算法: Java中,垃圾回收(GC,Garbage Collection)的對象是Java堆和方法區(即永久區或持久區)
垃圾指的是在系統運行過程當中所產生的一些無用的對象,這些對象占據著一定的內存空間,如果長期不被釋放,可能導致OOM。后臺專門有一個專門用于垃圾回收的線程來進行監控、掃描,自動將一些無用的內存進行釋放,這就是垃圾收集的一個基本思想,目的在于防止由程序猿引入的人為的內存泄露。
現代java虛擬機常用的垃圾回收算法有三種,分別是標記-清除算法、復制算法、標記-整理算法
標記-清除算法
(1)概念:標記-清除算法是現代垃圾回收算法的思想基礎。它將垃圾回收分為兩個階段:標記階段和清除階段。
標記階段:首先,通過根節點,標記所有從根節點開始的可達對象。未被標記的對象就是未被引用的垃圾對象;
清除階段:然后,清除所有未被標記的對象。
(2)算法詳解原理:當堆中的可用有效內存空間(available memory)被耗盡的時候,就暫停整個程序(也被成為stop the world),然后進行標記和清除兩項工作,然后讓程序恢復運行。
標記:標記的過程其實就是,遍歷所有的GC Roots,然后將所有GC Roots可達的對象標記為存活的對象。
清除:清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。
疑問:為什么非要停止程序的運行呢?
答:不難理解,假設程序與GC線程一起運行,當對象A處于標記階段,被標記為垃圾對象后,試想此時新new了一個對象B,且對象A可達B。但是由于此時A對象已經標記結束,B對象錯過了標記階段。因此當接下來清除階段會被,新對象B會隨著A被標記被清除掉,變為null,這樣就亂套了。如此一來,要想正常清除垃圾資源,GC線程必須要暫停程序。
(3)標記-清除算法的缺點:首先,它的缺點就是效率比較低(遞歸與全堆對象遍歷),暫停程序stop the world的時間比較長。(尤其對于交互式的應用程序來說簡直是無法接受。試想一下,如果你玩一個網站,這個網站一個小時就掛五分鐘,你還玩嗎?)
第二則是這種方式清理出來的空閑內存不連續,這點不難理解,我們的死亡對象都是隨即的出現在內存的各個角落的,現在把它們清除之后,內存的布局自然會亂七八糟。而為了應付這一點,JVM就不得不維持一個內存的空閑列表,這又是一種開銷。而且在分配數組對象的時候,尋找連續的內存空間會不太好找。
2 復制算法(適用于年輕代GC)
(1)概念:內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象復制到未使用的內存塊中,之后,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收。
與標記-清除算法相比,復制算法是一種相對高效的回收方法,且內存連續。
不適用于存活對象較多的場合,如老年代(復制算法適合做新生代的GC)
(2)優點:實現簡單,運行高效,內存連續。每次只要一動指針,就可聯系分配內存存放復制過來的對象。
缺點:空間浪費,只用了一半內存,所以,要想用復制算法,最起碼對象的存活率要非常低才行,而且最重要的是要克服50%內存的浪費。
針對這種缺點,這種算法比較適合,且已經用于年輕代垃圾回收,新生代中的對象98%都是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊比較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是說,每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的空間會被浪費。
當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴于老年代進行分配擔保,所以大對象直接進入老年代。
標記-整理算法(適用于年老代的GC)
(1)引入:如果在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選中這種算法。
(2)概念: 適合用于存活對象較多的場合,如老年代。它在標記-清除算法的基礎上做了一些優化。和標記-清除算法一樣,標記-壓縮算法也首先需要從根節點開始,對所有可達對象做一次標記;但之后,它并不簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存的一端;之后,清理邊界外所有的空間。
標記:它的第一個階段與標記/清除算法是一模一樣的,均是遍歷GC Roots,然后將存活的對象標記。
整理:移動所有存活的對象,按照內存地址次序依次排列,然后將末端內存地址以后的內存全部回收。因此,第二階段才稱為整理階段。(JVM只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷)
(3)優點:標記/整理算法不僅可以彌補標記/清除算法當中,內存區域分散的缺點,也消除了復制算法當中,內存減半的高額代價。
缺點:就是效率也不高。不僅要標記所有存活對象,還要整理所有存活對象的引用地址。從效率上來說,要低于復制算法。
標記-清除算法、復制算法、標記整理算法的總結
三個算法都基于根搜索算法去判斷一個對象是否應該被回收,而支撐根搜索算法可以正常工作的理論依據,就是語法中變量作用域的相關內容。因此,要想防止內存泄露,最根本的辦法就是掌握好變量作用域,而不應該使用C/C++式內存管理方式。
在GC線程開啟時,或者說GC過程開始時,它們都要暫停應用程序(stop the world)。
它們的區別如下:
(1)效率:復制算法 > 標記/整理算法 > 標記/清除算法
(2)內存整齊度:復制算法=標記/整理算法>標記/清除算法
(3)內存利用率:標記/整理算法=標記/清除算法>復制算法
注1:可以看到標記/清除算法是比較落后的算法了,但是后兩種算法卻是在此基礎上建立的。
注2:時間與空間不可兼得。
更多關于“Java培訓”的問題,歡迎咨詢千鋒教育在線名師。千鋒已有十余年的培訓經驗,課程大綱更科學更專業,有針對零基礎的就業班,有針對想提升技術的好程序員班,高品質課程助理你實現java程序員夢想。