基礎概念
數據類型
引用類型
Java內存模型
01.方法區
02.堆區--------------垃圾回收
03.棧區
04.程序計數器
05.原生方法棧
常見問題彙總
Jvm運行原理
基本垃圾回收算法
垃圾回收算法面臨的問題
分代垃圾回收
Jvm調優常見配置彙總
類加載過程
類加載器
內存泄漏和內存溢出
-一、基礎概念
01.數據類型
① 基本數據類型:byte,short,int,long,char,float,double,Boolean
② 引用數據類型:類類型,接口類型和數組
02.引用類型
① 強引用:聲明對象時虛擬機生成的引用,如果被強引用,則不會被垃圾回收。
強引用其實也就是我們平時 A a = new A()這個意思。
② 軟引用:一般被作爲緩存來使用,當內存緊張的時候,這種類型引用的空間會被回收;
軟引用可用來實現內存敏感的高速緩。
③ 弱引用:與軟引用差不多,也是作爲緩存使用,但是每次垃圾回收肯定會被回收;
虛引用(PhantomReference)
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
-Java內存模型
jvm五大區 每個區的存儲 作用
線程共享: java堆 方法區
線程私有: 虛擬機棧 本地方法棧 程序計數器
01.方法區(method area)
被虛擬機加載的類信息,靜態變量,常量,即時編譯器編譯後的代碼等數據, 運行常量池是方法區的一部分,class文件除了有 類的版本,字段 ,方法,接口,等描述信息外,還有一項信息常量池保存編譯期生成的字面量和符號引用。
字面量: int i = 1;把整數1賦值給int型變量i,整數1就是Java字面量, 同樣,String s = "abc";中的abc也是字面量。 |
符號引用: 符號引用是一組符號,用來描述所引用的目標,符號是以任何形式存在的字面量。對於符號引用Java虛擬機並沒有嚴格的限制。規定只需要使用的時候能夠無歧義定位到目標就可以。 符號引用屬於常量池中的內容,那麼是不是說符號引用的目標已經加載到內存中了呢?答案是否定的,因爲符號引用與虛擬機的內存佈局無關,符號引用的目標並不一定已經加載到內存中了。 |
運行時常量池具備動態性,在運行期也可能將新的常量放入池中,這種特性被開發人員利用得較多得是String類的intern()方法。
02.堆區(heap)
在虛擬機啓動時創建,唯一目的是存放對象的實例。
java虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配,但是隨着JIT編譯器的發展與逃逸技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象的分配在堆上也逐漸變得不是那麼“絕對”了。
java堆是垃圾收集器管理的主要區域,從內存回收的角度,現在收集器基本都採用分代收集算法,所以java堆中還可以細分爲新生代和老年代。從內存分配的角度看,線程共享的java堆可能劃分出多個線程私有的分配緩衝區(TLAB),不過無論怎麼劃分,都與存放的內容無關。
java堆可以出於物理上不連續的內存空間中,只要邏輯上連續即可。
在進行jvm調優時,關於堆的操作,java堆溢出,出現OutOfMemoryError異常,-Xmx 堆最大值 和 -Xms 堆最小值兩個參數十分重要,出現異常時,分析時內存泄漏還是內存溢出。
Java中堆是由所有的線程共享的一塊內存區域。
03.Java棧區(Java stack)
棧也叫棧內存,是 Java 程序的運行區,是在線程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來說不存在垃圾回收問題,只要線程一結束,該棧就 Over。
描述的是java方法執行的內存模型,每個方法執行同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表,操作數棧,動態鏈表,方法出口等信息,每個方法從調用到執行完成的過程,就對應一個棧幀在虛擬機中入棧到出棧的過程
局部變量表存儲的是編譯期間可知的各種基本數據類型,對象引用和returnAddress類型,所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量的大小。
異常: 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常 如果擴展時無法申請到足夠的內存,將會拋出OutOfMemoryError異常。
在單個線程下,無論是由於棧幀太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。
在建立多線程時會導致內存溢出,在不能減少線程數或者更換64位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。
04.程序計數器寄存器(pc register)
是當前線程所執行的字節碼的行號指示器,若線程正在執行一個java方法,則計數器記錄的是正在執行的虛擬機字節碼指令的地址,若正在執行的是Native方法,計數器值爲空。
它是虛擬機規範中唯一一個沒有規定任何OutOfMemoryError情況的區域。
05.原生方法棧(native method stack)
原生方法棧與 Java 方法棧相類似,這裏不再贅述
常見問題彙總
01.堆和棧有什麼區別
答:堆是存放對象的,但是對象內的臨時變量是存在棧內存中,如例子中的 methodVar 是在運行期存放到棧中的。
棧是跟隨線程的,有線程就有棧,堆是跟隨 JVM 的,有 JVM 就有堆內存。
① 棧是運行時單位,而堆是存儲的單位。
② 堆中存的是對象,棧中存的是基本數據類型和堆中對象的引用。
③ 由於程序運行在棧中進行,所以傳遞參數的時候,只存在傳遞基本數據類型和對象引用的問題,所以它都是進行傳值調用。
④ Java中棧的大小通過-Xss來設置。
02.堆內存中到底存在着什麼東西?
答:對象,包括對象變量以及對象方法。
03.類變量和實例變量有什麼區別?
答:靜態變量是類變量,非靜態變量是實例變量,直白的說,有 static 修飾的變量是靜態變量,沒有 static 修飾的變量是實例變量。靜態變量存在方法區中,實例變量存在堆內存中。
04.Java 的方法(函數)到底是傳值還是傳址?
答:都不是,是以傳值的方式傳遞地址,具體的說原生數據類型傳遞的值,引用類型傳遞的地址。對於原始數據類型,JVM 的處理方法是從 Method Area 或 Heap 中拷貝到 Stack,然後運行 frame中的方法,運行完畢後再把變量指拷貝回去。
05.爲什麼會產生 OutOfMemory 產生?
答:一句話:Heap 內存中沒有足夠的可用內存了。這句話要好好理解,不是說 Heap 沒有內存了,是說新申請內存的對象大於 Heap 空閒內存,比如現在 Heap 還空閒 1M,但是新申請的內存需要 1.1M,於是就會報 OutOfMemory 了,可能以後的對象申請的內存都只要 0.9M,於是就只出現一次 OutOfMemory,GC 也正常了,看起來像偶發事件,就是這麼回事。 但如果此時 GC 沒有回收就會產生掛起情況,系統不響應了。
我產生的對象不多呀,爲什麼還會產生 OutOfMemory?
答:你繼承層次忒多了,Heap 中 產生的對象是先產生 父類,然後才產生子類,明白不?
06.OutOfMemory 錯誤分幾種?
答:分兩種,分別是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen
space”,兩種都是內存溢出,heap size 是說申請不到新的內存了,這個很常見,檢查應用或調整堆內存大小。
“PermGen space”是因爲永久存儲區滿了,這個也很常見,一般在熱發佈的環境中出現,是
因爲每次發佈應用系統都不重啓,久而久之永久存儲區中的死對象太多導致新對象無法申請內存,
一般重新啓動一下即可。
07.爲什麼會產生 StackOverflowError?
答:因爲一個線程把 Stack 內存全部耗盡了,一般是遞歸函數造成的。
08.一個機器上可以看多個 JVM 嗎?JVM 之間可以互訪嗎?
答:可以多個 JVM,只要機器承受得了。JVM 之間是不可以互訪,你不能在 A-JVM 中訪問B-JVM 的 Heap 內存,這是不可能的。在以前老版本的 JVM 中,會出現 A-JVM Crack 後影響到B-JVM,現在版本非常少見。
09.爲什麼 Java 要採用垃圾回收機制,而不採用 C/C++的顯式內存管理?
答:爲了簡單,內存管理不是每個程序員都能折騰好的。
010.爲什麼你沒有詳細介紹垃圾回收機制?
答:垃圾回收機制每個 JVM 都不同,JVM Specification 只是定義了要自動釋放內存,也就是說它只定義了垃圾回收的抽象方法,具體怎麼實現各個廠商都不同,算法各異,這東西實在沒必要深入。
011.JVM 中到底哪些區域是共享的?哪些是私有的?
答:Heap 和 Method Area 是共享的,其他都是私有的,
-JVM運行原理
01.向操作系統申請空閒內存:jvm對操作系統說“給我64M空閒內存”,操作系統給jvm分配內存以後,jvm準備加載類文件。
02.分配內存:給head,stack等分配內存。
03.檢查文件:檢查分析class文件,若發現錯誤立即返回錯誤。
04.加載類;
05.執行引擎執行方法。
-基本垃圾回收算法
01.按照基本回收策略分
1) 引用計數法:
2) 複製:
3) 標記整理:
02.按系統線程分
① 串行收集:使用單線程處理所有垃圾回收工作;
② 並行收集:使用多線程處理所有垃圾回收工作;
③ 併發收集:前面 兩個在進行垃圾會收的時候需要暫停整個運行環境,而只有垃圾回收線程在運行,併發收集不需要暫停。
-垃圾回收面臨的問題
(參考:
http://www.importnew.com/18694.html
http://www.importnew.com/18740.html
http://www.importnew.com/18747.html
http://www.importnew.com/19255.html)
01.如何區分垃圾:
引用計數法與可達性分析算法。
棧是真正進程開始執行的地方,一個棧是與一個進程相對應的,如果有多個線程的話,必須對這些線程對應的棧進行檢查。
除了棧外還有系統運行時的寄存器,也是存儲程序運行時的數據。
這樣以棧和寄存器的引用爲起點,我們就可以找到堆中的對象,又從這些對象找到堆中其他對象的引用,這種逐步擴展,最終以null引用或者基本數據類型結束,這樣就形成了一顆以Java棧中所對應的對象爲根結點的對象樹,如果有多個引用就會有多個對象樹。在對象樹上,都是當前所需要的對象,不能被垃圾回收。而其他剩餘對象,則被視爲無法被引用的對象,可以被當做垃圾回收。
02.如何處理碎片
“複製方法”和“標記-整理”都可以。
03.如何解決同時存在的對象創建和對象回收問題
垃圾回收線程是回收內存的,而程序運行線程則是消耗內存的,存在矛盾。如果採用先暫停,進行垃圾回收,然後開啓,這樣的問題是:當堆空間持續增大,垃圾回收時間也會增大,對應暫停時間也會增大。可以採用併發垃圾回收。
-分代垃圾回收
在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鉤,因此生命週期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命週期會比較短,比如:String對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。
虛擬機中的共劃分爲三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。
01.分代垃圾回收
年輕代:
所有新生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的對象,將被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置爲多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。
年老代:
在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。
持久代:
用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=<N>進行設置。
持久代和Metaspace
移除永久代的工作從JDK1.7就開始了,JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。在 JDK 1.8 中, 取而代之是一個叫做 Metaspace(元空間) 的東西。
元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少爲分配空間所導致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少爲釋放空間所導致的垃圾收集
02.觸發垃圾回收的條件
GC的兩種類型: minorGC 和Full GC;
minorGC :
當對象生成,並且在Eden區中申請空間失敗,就會出發,對eden區進行清理非存活對象,並且將存活對象放在survivor區中。
Full GC:
年老代被寫滿;
持久代被寫滿;
System.gc()被顯示調用;
上一次GC之後Head的各域分配策略動態變化。
03.選擇合適的垃圾回收算法
串行收集器:使用小型應用;
並行收集器:後臺處理,科學計算;
併發收集器:Web服務器/應用服務器、電信交換、集成開發環境。
-常見配置彙總
不能通過寫Java代碼來干預Java的垃圾回收。
影響Java垃圾回收的參數主要有-Xms,-Xmx(適當設置,避免頻繁的垃圾回收,垃圾回收一般是在內存不滿足要求的時候進行的)
-類加載過程
對於初始化階段,虛擬機規範嚴格規定了有且只有5種情況必須立即對類進行初始化:
1、遇到new getstatic putstatic 或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化,
最常見的java代碼場景是:使用new關鍵字實例化對象時,讀取或者設置一個類的靜態字段(被final修飾,已在編譯期把結果放入常量池的靜態字段除外)時,調用一個類的靜態方法的時候
2、使用反射對類進行調用時,若類沒有初始化,則需要先觸發其初始化。
3、當初始化一個類,其父類還沒有初始化,則需要先觸發其父類的初始化
4、當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main方法的那個類),虛擬機會先初始化這個主類
5,當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後解析結果REF_getStatic REF_putStatic REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
-類加載器
1. 什麼是類加載器
* 把.class文件加載到JVM的方法區中,變成一個Class對象!
2. 得到類加載器
* Class#getClassLoader()
3. 類加載器的分類
它們都是片警!
* 引導:類庫!
* 擴展:擴展jar包
* 系統:應用下的class,包含開發人員寫的類,和第三方的jar包!classpath下的類!
系統類加載器的上層領導:擴展
擴展類加載器的上層領導:引導
引導沒上層,它是BOSS
======================================
4. 類加載器的委託機制(雙親委派模型)
* 代碼中出現了這麼一行:new A();
> 系統發現了自己加載的類,其中包含了new A(),這說明需要系統去加載A類
> 系統會給自己的領導打電話:讓擴展去自己的地盤去加載A類
> 擴展會給自己的領導打電話:讓引導去自己的地盤去加載A類
> 引導自己真的去rt.jar中尋找A類
* 如果找到了,那麼加載之,然後返回A對應的Class對象給擴展,擴展也會它這個Class返回給系統,結束了!
* 如果沒找到:
> 引導給擴展返回了一個null,擴展會自己去自己的地盤,去尋找A類
* 如果找到了,那麼加載之,然後返回A對應的Class對象給系統,結束了!
* 如果沒找到
> 擴展返回一個null給系統了,系統去自己的地盤(應用程序下)加載A類
* 如果找到了,那麼加載之,然後返回這個Class,結束了!
* 如果沒找到,拋出異常ClassNotFoundException
雙親委派模型
Java 類加載器的作用就是在運行時加載類。
Java 類加載器基於三個機制:委託性、可見性和單一性
1.委託機制是指雙親委派模型。
2.可見性原理是子類的加載器可以看見所有的父類加載器加載的類,而父類加載器看不到子類加載器加載的類。
3.單一性原理是指僅加載一個類一次,這是由委託機制確保子類加載器不會再次加載父類加載器加載過的類
5. 類的解析過程
class MyApp {//被系統加載
main() {
A a = new A();//也由系統加載
String s = new String();//也由系統加載!
}
}
class String {//引導
private Integer i;//直接引導加載
}
=====================
6.自定義類加載器
* 繼承ClassLoader
* 重寫findClass()
內存泄漏和內存溢出
內存泄露
內存泄露是指一個不再被使用的對象或變量還在內存中佔有存儲空間。
內存泄露例子:
Vector v = new Vector(10); for(int i = 1;i < 10;i++){ Object o = new Object(); v.add(o); }
在上述循環中,不斷有新的對象加到vector中,當退出循環後,o的作用域將會結束,但是由於v在使用這些對象,因此垃圾回收器無法將其回收,因此造成內存泄露。解決辦法是把v置爲null。
造成內存泄露的原因:
01)靜態集合類,例如HashMap和Vector。(如上例)
02)各種連接,例如數據庫連接、網絡連接以及IO連接。比如數據庫連接,對於connection,statement,result在使用完之後要及時關閉連接。
03)變量不合理的作用域。
內存泄露的解決方案:
1、避免在循環中創建對象。
2、儘早釋放無用對象的引用。(最基本的建議)
3、儘量少用靜態變量,因爲靜態變量存放在永久代(方法區),永久代基本不
參與垃圾回收。
4、使用字符串處理,避免使用 String,應大量使用 StringBuffer,每一個 String
對象都得獨立佔用內存一塊區域。
2.內存溢出
內存溢出:指程序運行過程中無法申請到足夠的內存而導致的一種錯誤。
內存溢出的幾種情況(OOM 異常):
OutOfMemoryError 異常:
除了程 序計數器外 ,虛擬機內 存的其他幾 個運行時區 域都有發生OutOfMemoryError(OOM)異常的可能。
1.虛擬機棧和本地方法棧溢出
如 果 線 程 請 求 的 棧 深 度 大 於 虛 擬 機 所 允 許 的 最 大 深 度 , 將 拋 出StackOverflowError 異常。
如 果 虛 擬 機 在 擴 展 棧 時 無 法 申 請 到 足 夠 的 內 存 空 間 , 則 拋 出OutOfMemoryError 異常。
2.堆 溢出
一般的異常信息:java.lang.OutOfMemoryError:Java heap spaces。
3.方法區溢出
異常信息:java.lang.OutOfMemoryError:PermGen space。
4.運行時常量池溢出
異常信息:java.lang.OutOfMemoryError:PermGen space。
導致內存溢出的原因:
1.內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;
2.集合類中有對對象的引用,使用完後未清空,使得 JVM 不能回收;
3.代碼中存在死循環或循環產生過多重複的對象實體;
4.啓動參數內存值設定的過小。
內存溢出的解決方法:
第一步,修改 JVM 啓動參數,直接增加內存。(-Xms,-Xmx 參數一定不要忘記加。一般要將-Xms 和-Xmx 選項設置爲相同,以避免在每次 GC 後調整堆的大小;建議堆的最大值設置爲可用內存的最大值的 80%)。
第二步,檢查錯誤日誌,查看“OutOfMemory”錯誤前是否有其它異常或錯誤。
第三步,對代碼進行走查和分析,找出可能發生內存溢出的位置。
第四步,使用內存查看工具動態查看內存使用情況(Jconsole)。