物理內存和虛擬內存
(1)在java中,分配內存和回收內存都由JVM自動完成,甚至不需要寫和內存相關的代碼
(2)物理內存即RAM還有寄存器(一種存儲單元,用於存儲計算機單元執行指令(如整形浮點等運算)的中間結果)是處理器通過地址總線連接的。地址總線:其寬度決定了一次可以存寄存器或者RAM中獲取多少個bit和處理器最大的可以尋址的范圍,每個地址會引用一個字節,所以如果是32位的總線則可以有4G的內存空間。(通常情況下地址總線和RAM或寄存器有相同的位數)
(3)通常操作系統的內存申請空間是按照進程來管理的,每個進程間不會互相重合,操作系統保證每個進程擁有一段獨立的地址空間。(邏輯上獨立,物理空間不一定獨立,如虛擬內存,虛擬內存是計算機系統內存管理的一種技術。它使得應用程序認為它擁有連續的可用的內存(一個連續完整的地址空間),而實際上,它通常是被分隔成多個物理內存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數據交換。)
(4)由於程序越來越龐大何設計的多任務性,物理內存無法滿足要求,出現了虛擬內存,虛擬內存使得多個進程可以共享物理內存,並且邏輯上獨立。虛擬內存提高了內存利用率,並且可以擴展內存空間,使得一個虛擬的地址可以映射到物理內存,文件或者其他可以尋址的存儲上。如一個進程在不活動的情況下,操作系統將這個物理內存中的數據移到一個磁盤文件下(頻繁地交換物理內存和磁盤上的數據,會導致效率低下,需要關注)。
內核空間和用戶空間
(1)電腦的內存地址空間將被划分為內核地址空間和用戶空間,程序只能使用用戶空間的內存(指程序能夠申請的內存)(如windows32為默認內核空間和用戶空間的比例是1:1,linux32為默認的比例是1:3)
(2)內核空間主要指操作系統用於程序調度、虛擬內存或者連接硬件資源等的程序邏輯。程序不能訪問操作系統的空間,並且不能直接訪問硬件資源,必須通過系統提供的接口調用。(每一次系統調用都會引起內核空間和內存空間的切換,這一操作比較耗時)
Java中的那些組件需要使用內存
(1).java堆:用於存儲java對象的內存區域,可以通過Xmx(最大大小)和Xms(初始大小)來控制大小,默認空余堆內存少於40%時就擴大到Xmx,空余堆內存大於70%時就縮小到Xms,因此,服務器一般把xmx和xms設置成一樣,避免在GC后調節堆的大小。
(2).每個線程創建時,JVM都會為它創建一個運行方法棧、局部變量的堆還有操作棧。
(3).類和類加載器:類和類的加載器本身同樣需要存儲空間,存儲在永久代PermGen(屬於方法區,即java堆的永久區部分)
ps:
- JVM是按需加載類的,隱式加載只會加載那些應用程序中明確使用到的。
- 加載類超過PermGen區大小的話可能會導致內存溢出,所以對於自己實現的類加載器可能會導致類的重復加載時,可能需要實現對類的卸載,需滿足:
-
- Java堆中沒有對表示該類的類加載器的java.lang.ClassLoader對象的引用,
- Java對中沒有該類的對應加載器的java.lang.class對象的引用,
- Java堆上任何該類的類加載器的任何類的所有對象都不存活。
而JVM的默認類加載器都不滿足該條件,所以他們加載的類都不能卸載。
(4)NIO:NIO使用ByteBuffer.allocateDirect()方法分配內存,可以避免數據從內核空間到用戶空間的復制,提高效率,但是該方法直接使用的是本機內存而不是java堆內存,直接的ByteBuffer對象可以自動清理本機緩存區,但是其只是作為GC時的一部分執行,而GC只在Java堆被填滿或者顯示調用System.gc()來執行(也就是自動的GC只檢查Java堆是否滿,而不知道NIO操作的本機內存是否需要釋放。),以至於NIO在很多框架中是通過顯示調用System.gc()執行NIO內存的釋放的(其實2對象本身有clean方法可以釋放,見jdk隨筆額,heap&direct)
(5)JNI:JNI使得本機代碼(如C語言)可以調用java方法,JVM會准備空間以供運行本地方法,也會增加java運行時的本機內存占用。
JVM內存結構
JVM是按照運行時數據的存儲結構來划分內存結構的,根據不同的格式存儲在不同的區域。運行時數據包括java程序本身的數據信息和JVM運行Java程序需要的額外數據信息,java虛擬機規范將Java運行時數據分為6種:PC寄存器數據、Java棧、堆、方法區、本地方法區、運行時常量池。
PC寄存器數據:
用於保存當前執行程序的內存地址,也就是記錄某線程當前執行的方法的那一條指令,如線程的執行被中斷后就會依靠這些數據來恢復(JVM規范之定義了對java方法需要記錄指針,對本地方法則沒有規范)
Java棧:
java棧與線程相關聯,每創建一個線程就會為該線程創建一個棧,而線程中運行的每一個方法則與棧中的每一個棧幀關聯起來,棧幀中包含局部變量,操作棧,方法返回值等。每一個方法完成,就會彈出棧幀的元素(操作棧的棧頂元素),作為返回值,清除這個棧幀。java棧的棧頂就是當前正在執行的活動棧,PC寄存器會指向這個方法的地址。Java棧和線程對應起來,這些數據不是線程共享的,不存在一致性問題
堆:
存儲Java對象的地方,由於時所有線程共享的,所以需要關心數據的一致性問題。
方法區:
用於存儲類結構信息,如常量池、域,方法數據、方法體,構造函數、包括類中的專用方法、實力初始化、接口初始化等
- 方法區同樣屬於java堆的永久代
- 如果使用動態編譯時要注意這部分是否能滿足類的存儲
- 這個區域並不像其他java堆一樣頻繁地被GC回收
運行時常量池:
包括編譯器的數字常量,方法或者域的引用。(注意,這一區域屬於方法區)
本地方法棧:
JVM為運行native方法准備的空間。由於很多native方法是用c語言實現的,所以又叫C棧。這個區域jvm並沒有嚴格的限制,由不同的JVM實現者自由實現。
JVM內存分配策略
- 靜態內存分配策略:在編譯期間必須知道內存空間(8個基本類型)的大小才可以分配(所以可以在編譯期間分配內存,但java棧中的局部變量和引用等數據同樣使用靜態內存分配,該空間大小是在編譯期間知道,但是在程序加載時才正式分配的,並且這一部分內存在java棧上分配),不允許可變數據類型或者遞歸、嵌套等結構的出現。
- 棧內存分配:不需要在編譯時知道程序對數據的需求、但在進入程序模塊時必須知道數據的要求才可以分配內存。並且按照后進先出的原則進行內存的分配
- 堆內存分配:可以在運行到相應代碼才知道內存空間的大小,靈活但是效率較差
JVM的內存分配主要基於堆和棧,
棧:
- 棧的分配時和線程綁定的,為每一個線程創建一個棧,為線程每調用一個新的方法創建一個棧幀
- 棧中主要保存基本類型數據和對象的句柄(引用、指針),棧的數據大小和生存期都必須是確定的,而本地變量和操作棧的大小都可以在編譯時(class字節碼)確定
- 存取速度比堆要快,僅次於寄存器,這也是為什么運算要留在操作棧中執行
- 棧的內存分配是在程序運行時進行的,只是分配的大小是在編譯時確定的
堆
- 堆可供所有線程訪問,主要存放實例數據,由於時動態分配內存大小的,所以存取速度較慢,同樣通過GC回收內存
- 新對象如何分配內存:根據對應Constant_Class_info類型數據執行new指令,賦值,調用init初始化構造器最后才賦值給變量(所以在初始化完成前不應該把實例指針公布,可類比“對象逸出”的問題),棧中存放的只是指針(引用),而真正的實例數據是存放在堆中的
- 堆在運行時請求操作系統分配內存,靈活但效率低
JVM內存回收策略
靜態內存的分配和回收:類中的局部變量和對象的引用都是靜態內存分配的(這一部分內存空間在棧上分配),在編譯時這一部分空間已經確定,只是在程序被加載時一次性分配,而當方法運行結束時隨着對應棧幀的撤銷回收。
動態內存分配和回收:像實例等數據只有在JVM解析類對象后才能知道具體需要分配多少空間,並且堆中的這些數據只有在對象不再被引用時才會被回收。只要某個對象不再被其他活動對象所引用就可以被回收,而活動對象是指可以被根集合對象所到達的對象。根集合對象所包含的對象跟jvm具體實現有關,但是大都會包含如下一些元素:方法中局部變量的引用、java操作棧中的對象引用、常量池中的對象引用、本地方法中持有的對象引用、類的class對象(當該Class對象不再被使用時同樣會被回收)。
基於分代的垃圾收集算法:分為young、old、perm三個區
young區分為eden區和兩個survivor區,eden區滿后會觸發minorGC,minorGC后仍存活的對象將放到survivor區(若另一個survivor區存在活動對象將放到同一個區中,保證一個survivor區是空的)
old區中已滿將會觸發FullGC,old區中存放的是:
- Young的survivor區中已滿后minorGC仍然存活的對象
- survivor區中足夠老的對象
- Eden中已滿,並且minorGC后存在,並因為servivor已滿無法存放的對象。
perm區主要存放類的class對象,只有在FullGC時才會被回收
三類垃圾收集算法,Serial Collector、Parallel Colllector、CMS Collector
Serial Collector
JVM在client模式下的默認的GC方式(可以通過配置jvm參數 -XX:+UserSerialGC來配置實用該算法)-XX:+PrintGCDetails 可以配置打印GC日志。所有創建的對象都將在Eden區分配,如果創建的對象超過Eden區的大小或者超過PretenureSizeThreshold配置(-XX:PretenureSizeThreshold=123)參數的大小都只能在old區分配。當Eden區空間不足時會觸發minorGC,但是觸發minorGC之間會檢查晉升到Old區的平均對象大小是否大於old的剩余空間,如果大於則觸發FullGC,如果小於則根據HandlePromotionFailure(是否允許擔保失敗)參數,如果為true則僅觸發MinorGC,否則觸發FullGC。MinorGC時除了將Eden區的非活動對象回收外,還會把一些年老的對象晉升到Old區,而這個年老對象的‘歲數’則通過 -XX:MaxTenuringThreshold=10設置(在survivor的from/to區之間移動一次則為一歲),另外如果To的Servivor區空間不足移入對象時,這些對象也會直接放入Old區。如果old區或者Perm區空間不足時就會觸發FullGC。GC時因為是串行的,所以動作是單線程完成的,JVM中的其他應用程序會全部停止。
Parallel Collector
Parallel GC根據MinorGC和FullGC的不同分為三種,分別是ParNewGC、ParallelGC和ParallelOldGC。
ParNewGC:
可以通過參數 -XX:+UseParNewGC參數來指定,與Serail Collector相似,只是回收是多線程並行的,並且通過一個UseAdaptiveSizePolicy配置參數來控制對象經過多少次回收后可以直接放入old區。
ParallelGC:
是server模式JVM下的默認GC方式,可以通過 -XX:+UserParallelGC參數來強制指定,並行回收的線程數可以通過 -XX:ParallelGCThreads來指定,這個值有個計算公式,如果cpu核數小於8,則可以和核數一樣,如果大於8值為:3+(核數*5)/8,可以通過 -Xmn:10m來控制Young區的大小,而Eden、FROM區的大小比例可以通過 -XX:SurvivorRatio=8來設置Eden和FromSpace的比值是8:1(當然To區也占1)。當在Eden區中申請內存空間時,如果Eden區不夠,則比較當前申請空間時否大於Eden的一半,是的話則直接在old中分配,不是的話則會執行MinorGC,但是執行MinorGC之前會檢查old區的平均晉升大小是否大於剩余空間,大於則觸發FullGC,並且在執行FullGC后會再一次檢查old的晉升的平均大小是否大於剩余空間,不是的話會再次觸發FullGC,也就是說可能會觸發兩次FullGC。,Young區的晉升規則可以通過以下參數設置:AlwaysTenure:默認為false,為true則表示只要在MinorGC時存活則晉升,NeverTenure,默認為false,是true則永不晉升。如果上面兩個參數都沒有配置的情況下設置UseAdaptiveSizePolicy,則啟動時將以InitialTenuringThreshold值作為存活次數的閥值,並且在每次GC后調整。如果不使用UseAdaptiveSizePolicy則將以MaxTenuringThreshold為准(通過-XX:-UseAdaptiveSizePolicy設置)另外如果MinorGC時Servivor的To區空間不夠,也會直接放到old區。old或者Perm區滿時會觸發FullGC,如果配置了參數ScavengeBeforeFullGC則在FullGC之前會觸發MinorGC
PrarllelOldGC:
可以通過 -XX:+UseParallelOldGC參數來強制指定,同樣可以通過-XX:ParallelGCThreads來指定線程數,這個值有個計算公式,如果cpu核數小於8,則可以和核數一樣,如果大於8值為: 3+(核數*5)/8。與ParallelGC的不同在於FullGC,它的FullGC動作為清空整個Heap對中的垃圾對象,清楚Perm區中已經被卸載的類信息,並進行壓縮,而ParallelGC只清楚部分heap堆中的垃圾對象,並對部分空間進行壓縮。
CMS Collector
可以通過 -XX:+UseConcMarkSweepGC來指定,並發的默認線程為4,也可以通過ParallelCMSThreads指定。CMS Collector使用CMS GC、Minor GC、FullGC。而CMS GC不同於其他兩種GC,觸發規律是基於Old區、和Perm區的使用率(觸發后回收對應old或perm區的內存),達到一定比例就會觸發(默認是92%),該比例可以通過CMSInitiatingOccupancyFraction來指定,另外設置讓Perm區也使用CMS GC可以通過參數 -XX:+CMSClassUnloadingEnabled來指定。這個模式下的minorGC與Serial Collector基本一致,只是采用多線程。FullGC只在兩種情況觸發,一種是Eden分配失敗后分配到To區,To區滿分配到Old區,Old區不夠則觸發FullGC,另外一種是當CMS GC向Old申請內存失敗時會觸發FullGC。Hotspot1.6下使用這種算法並顯示調用System.gc(如Nio可能需要顯示調用),且設置了ExplicitFCInvokesConcurrent參數,將會導致內存泄露。
內存問題分析
日志格式:
[GC [<Collector>:<starting occupancy1> -> <ending occupancy1> (total size1) , <paise time1> secs ] <starting occupancy2> -> <ending occupancy2> (total size2), <paise time2> secs ]
<Collector> 收集器的名稱
<starting occupancy1>Young區GC前內存
<endingoccupancy1>Young區GC后內存
<paise time1>YOUNG區局部收集時JVM的暫停時間
<starting occupancy2>表示JVMHeap GC前內存
<endingoccupancy2>表示JVMHeap GC后內存
<paise time2>GC過程中JVM的暫停總時間
GC日志對內存泄露的判斷:
- 根據<starting occupancy1> - <ending occupancy1>得到young區被回收或晉升的內存大小,根據<starting occupancy2> - <ending occupancy2>得到當前整個堆的大小變化,兩者的差值就是young區晉升到Old區的值
- 假如<ending occupancy2>隨時間的延長一直增長,並且伴隨頻繁的GC,則很有可能是內存泄露,可使用jstat工具分析
- 堆快照的分析,可以通過參數: -XX:+HeadDumpOnOutOfMemoryError來配置在內存耗盡時記錄下內存快照,同時可以通過-XX:HeadDumpPath來指定文件路徑,這個文件的命名格式如java_[pid].hprof
JVM Crash日志分析:
- 可以通過-XX:ErrorFile = /tmp/log/hs_error_%p.log來指定jvm的日志文件
- 文件信息主要分為四種,退出原因分類、導致退出的Thread信息(棧信息,具體哪行代碼出錯)、退出時的Process狀態信息(所有線程及線程處於的狀態,jvm的堆信息)、退出時與操作系統相關信息
- 退出原因主要三種:
-
- EXCEPTION_ACCESS_VIOLATION:運行的是JVM自己的代碼,很可能是JVM的BUG
- SIGSEGV:JVM在執行本地代碼或者JNI的代碼,很可能是第三方本地庫有問題
- EXCEPTION_STACK_OVERFLOW:這個是本地棧溢出的錯誤,可以將JVM的棧的尺寸調大,主要是兩個參數-Xss 和 -XX:StackShadowPages=n