JVM底層原理相關分析

一、jvm介紹

1、jvm是什麼?

JVM就是Java虛擬機(Java virtual machine)。JVM是JRE的一部分,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。

2、jvm的類別(目前市場有3種虛擬機)

     目前有三大Java虛擬機:HotSpot,oracle JRockit,IBM J9。主流HotSpot虛擬機,由sun公司研發。

3、jvm的體系結構

內部結構分爲三部分:

  • 類加載器(加載.class文件)
  • 執行引擎(執行字節碼或執行本地方法)
  • 數據區(包含PC寄存器,棧,堆,方法區以及本地方法棧)

4、簡要執行過程

1、程序運行時,java文件通過java編譯器轉譯成class文件

2、class文件通過類裝載器以及java類庫,裝載到JVM中

3、VM通過解釋器,即時編譯器等將裝載進來的class文件進行編譯操作。如內存分配,運行處理等

4、最後JVM將相應操作與操作系統、硬件交互

二、jvm運行原理

1、第一步:類加載

(1)類加載的定義和加載器的類別

類的加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱爲系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。

分爲以上4類加載器:啓動類加載器(根類加載器)、擴展類加載器、應用類加載器、自定義類加載器

①Bootstrap ClassLoader

負責加載$JAVA_HOME中jre/lib/rt.jar裏所有的class,由C++實現,不是ClassLoader子類

②Extension ClassLoader

負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

③App ClassLoader

負責記載classpath中指定的jar包及目錄中class

④Custom ClassLoader

屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader

加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視爲已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
 

(2)、類加載過程(分爲三個步驟)

加載:

加載指的是將類的class文件讀入到內存,併爲之創建一個java.lang.Class對象,也就是說,當程序中使用任何類時,系統都會爲之建立一個java.lang.Class對象。

 類加載器通常無須等到“首次使用”該類時才加載該類,Java虛擬機規範允許系統預先加載某些類。

鏈接:

當類被加載之後,系統爲之生成一個對應的Class對象,接着將會進入連接階段,連接階段負責把類的二進制數據合併到JRE中。類連接又可分爲如下3個階段

1、驗證階段:驗證階段用於檢驗被加載的類是否有正確的內部結構,並和其他類協調一致。
           四步驗證階段:
                   文件格式驗證
                   元數據驗證
                   字節碼驗證
                   符號引用驗證 

2、準備階段:類準備階段負責爲類的靜態變量分配內存,並設置默認初始值。

3、解析階段:將類的二進制數據中的符號引用替換成直接引用。說明一下:符號引用:符號引用是以一組符號來描述所引用的目標,符號可以是任何的字面形式的字面量,只要不會出現衝突能夠定位到就行。佈局和內存無關。直接引用:是指向目標的指針,偏移量或者能夠直接定位的句柄。該引用是和內存中的佈局有關的,並且一定加載進來的。

初始化:

初始化是爲類的靜態變量賦予正確的初始值,準備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先字節碼文件被加載到內存後,先進行鏈接的驗證這一步驟,驗證通過後準備階段,給a分配內存,因爲變量a是static的,所以此時a等於int類型的默認初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。

(3)、類加載機制(雙親委派機制)

採用了雙親委派機制,其原理如下:

雙親委派機制原理:
如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式,即每個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子自己纔想辦法去完成。

雙親委派機制的優勢:
採用雙親委派模式的是好處是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係,通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設通過網絡傳遞一個名爲java.lang.Integer的類,通過雙親委託模式傳遞到啓動類加載器,而啓動類加載器在覈心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。

https://blog.csdn.net/m0_38075425/article/details/81627349

 

2、第二步字節碼執行引擎

(1)、定義

執行引擎是 Java 虛擬機最核心的組成部分之一。“虛擬機” 是一個相對於 “物理機” 的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接建立在處理器、硬件、指令集和操作系統層面上的,而虛擬機的執行引擎則是由自己實現的,因此可以自行制定指令集與執行引擎的結構體系,並且能夠執行哪些不被硬件直接支持的指令集格式。

所謂的「虛擬機字節碼執行引擎」其實就是 JVM 根據 Class 文件中給出的字節碼指令,基於棧解釋器的一種執行機制。通俗點來說,也就是 JVM 解析字節碼指令,輸出運行結果的一個過程。

3、第三步數據運行時區域(重點)

   區域的劃分圖:

由上圖分析得出,可分類兩大類,分別是,共享區域和非共享區域:

一、共享區域(堆和方法區):

1、堆(heap):
所謂的堆就是我們new一個對象new出來的實例,所有類的實例就放在這個區域,爲所有線程共享。可以想象       你的一個系統會產生很多實例,因此Java堆的空間也是最大的。如果Java堆空間不足了,程序會拋出OutOfMemoryError異常。

2、方法區:各個線程共享的區域,存放類信息、常量、靜態變量。

二、非共享區域(程序計數器、虛擬機棧、本地方法棧)

1、程序計數器:
指向當前線程正在執行的字節碼的地址 和行號(指令都是在cpu上面運行的,分配到時間片纔去運行的,當多個線程的時候其他線程會被掛起,程序計數器就是記錄被掛起之前的字節碼執行到哪行 地址等,等到重新分配到了時間片然後再繼續執行)。它的作用就是控制程序指令的執行順序。

2、虛擬機棧:
存儲當前線程運行方法所需要的數據,指令,返回的地址。

每個線程創建的同時會創建一個JVM棧,JVM棧中每個棧幀存放的爲當前線程中局部基本類型的變量、部分的返回結果,非基本類型的對象在JVM棧上僅存放一個指向堆上的地址。

而每個棧幀中,又包含如上圖中的:局部變量表,操作數棧,動態鏈接,方法出口,具體作用如圖所解釋。

3、本地方法棧:
本地方法棧是用來存儲本地方法相關的數據。本地方法就是帶有native標識符修飾的方法;

native修飾符修飾的方法並不提供方法體,但因爲其實現體是由非java代碼在在外部實現的,因此不能與abstract連用;

存在的意義:不方便用java語言寫的代碼,使用更爲專業的語言寫更合適;甚至有些JVM的實現就是用c編寫的,所以只能使用c來寫

(1)、Jvm堆的內存模型

     概要圖

   比例圖

  

  • 堆主要分爲新生代 ( Young ) 與老年代 ( Old ) ,二者比例的值爲 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小,老年代 ( Old ) = 2/3 的堆空間大小。
  • 其中,新生代 ( Young ) 被細分爲 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名爲 from 和 to以示區分。 默認的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。
  • JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來爲對象服務,所以無論什麼時候,總是有一塊 Survivor 區域是空閒着的。 因此,新生代實際可用的內存空間爲 9/10 ( 即90% )的新生代空間。新生代是 GC 收集垃圾的頻繁區域。
  • 當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在經過一次 Minor GC 後,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納 ( 上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置爲1,以後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成爲老年代。但這也不是一定的,對於一些較大的對象 ( 即需要分配一塊較大的連續內存空間 ) 則是直接進入到老年代。
  • From Survivor區域與To Survivor區域是交替切換空間,在同一時間內兩者中只有一個不爲空。
  •    永久代就是HotSpot虛擬機對虛擬機規範中方法區的一種實現方式。我們知道在HotSpot虛擬機中存在三種垃圾回收現象,minor GC、major GC和full GC。對新生代進行垃圾回收叫做minor GC,對老年代進行垃圾回收叫做major GC,同時對新生代、老年代和永久代進行垃圾回收叫做full GC。許多major GC是由minor GC觸發的,所以很難將這兩種垃圾回收區分開。major GC和full GC通常是等價的,收集整個GC堆。
     
  • 在1.8之後已經取消了永久代,改爲元空間,類的元信息被存儲在元空間中。元空間沒有使用堆內存,而是與堆不相連的本地內存區域。所以,理論上系統可以使用的內存有多大,元空間就有多大,所以不會出現永久代存在時的內存溢出問題。這項改造也是有必要的,永久代的調優是很困難的,雖然可以設置永久代的大小,但是很難確定一個合適的大小,因爲其中的影響因素很多,比如類數量的多少、常量數量的多少等。永久代中的元數據的位置也會隨着一次full GC發生移動,比較消耗虛擬機性能。同時,HotSpot虛擬機的每種類型的垃圾回收器都需要特殊處理永久代中的元數據。將元數據從永久代剝離出來,不僅實現了對元空間的無縫管理,還可以簡化Full GC以及對以後的併發隔離類元數據等方面進行優化。
     

新生代 GC (Minor GC)

發生在新生代的垃圾回收動作,頻繁,速度快。

老年代 GC (Major GC / Full GC)

發生在老年代的垃圾回收動作,出現了 Major GC 經常會伴隨至少一次 Minor GC(非絕對)。Major GC 的速度一般會比 Minor GC 慢十倍以上。

大對象直接進入老年代;長期存活的對象將進入老年代

上圖STW解釋:

1、如果new了一個大對象會進入老年區,或者頻繁new對象進入老年區,導致老年區的產生大量的full gc, 這時候jvm就會暫停其他所有的進程,集中全部精力去回收對象(這個步驟就是SWT),這樣就會導致程序卡頓,從而影響用戶體驗,所以要合理分配堆內存,避免大量full gc的操作,如果老年區的內存佔滿了就會出現oom的現象,也就是堆內存的溢出。

2、如果eden區移動到surbivor區new出來的對象佔比內存大於surbivor區的最大內存,這樣會直接進入老年區,如果沒合理分配新區的內存佔比的話,會導致大量的對象會被移動的老年區,老年區就會逐漸被佔滿,導致內存溢出。

jdk1.6,jdk1.7,jdk1.8之間的區別,分配改造了什麼?

它們之間區別在永久代區的區別,永久代是對方法區的一個具體實現,

1.6版本:永久代被劃分在堆內存裏面,其內容包括一些常量池(比如字符串)、靜態變量、字節碼文件。

1.7版本:永久代被劃分在堆內存裏面,沒有太大的變化,但是字符串常量池被劃分移動了到堆內存。

1.8版本:變化很大,字符串常量池還在堆裏面,而是把永久代概念去掉了,變成了元空間,並將移動到了本地內存中,本地內存由操作系統支配。

三、jvm的gc 垃圾回收機制

1、原理:

GC的基本原理:將內存中不再被使用的對象進行回收,GC中用於回收的方法稱爲收集器,由於GC需要消耗一些資源和時間,Java在對對象的生命週期特徵進行分析後,按照新生代、舊生代的方式來對對象進行收集,以儘可能的縮短GC對應用造成的暫停。

2、哪些內存需要回收?

JVM的內存結構包括五大區域:程序計數器、虛擬機棧、本地方法棧、堆區、方法區。其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生、隨線程而滅,因此這幾個區域的內存分配和回收都具備確定性,就不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟隨着回收了。而Java堆區和方法區則不一樣,這部分內存的分配和回收是動態的,正是垃圾收集器所需關注的部分。

3、回收分析算法的分析

1、引用計數法:
引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數。當一個對象被創建時,就將該對象實例分配給一個變量,該變量計數設置爲1。當任何其它變量被賦值爲這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。

優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。

缺點:無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0。

2、可達性分析算法:
程序把所有的引用關係看作一張圖,從一個節點GC ROOTS開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點,無用的節點將會被判定爲是可回收的對象。

在Java語言中,可作爲GC Roots的對象包括下面幾種:

  a) 虛擬機棧中引用的對象(棧幀中的本地變量表);

  b) 方法區中類靜態屬性引用的對象;

  c) 方法區中常量引用的對象;

  d) 本地方法棧中JNI(Native方法)引用的對象。

不同的對象引用類型, GC會採用不同的方法進行回收,JVM對象的引用分爲了四種類型:

強引用:在程序代碼中普遍存在的,類似Object obj=new Object()這類引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用:用來描述一些還有用但並非必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的內存,纔會拋出內存溢出異常。

弱引用:也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

虛引用:也叫幽靈引用或幻影引用(名字真會取,很魔幻的樣子),是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。它的作用是能在這個對象被收集器回收時收到一個系統通知(用來得知對象是否被GC)。


總結:
無論引用計數算法還是可達性分析算法都是基於強引用而言的。

即使在可達性分析算法中不可達的對象,也並非是“非死不可”,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程。

第一次標記:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記;

第二次標記:第一次標記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法,在finalize()方法中沒有重新與引用鏈建立關聯關係的,將被進行第二次標記。

第二次標記成功的對象將真的會被回收,如果對象在finalize()方法中重新與引用鏈建立了關聯關係,那麼將會逃離本次回收,繼續存活。

4、方法區如何判斷是否需要回收:

方法區主要回收的內容有:廢棄常量和無用的類。對於廢棄常量也可通過引用的可達性來判斷,但是對於無用的類則需要同時滿足下面3個條件:

(1)該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例

(2)加載該類的ClassLoader已經被回收

(3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

5、常用典型的幾種垃圾回收算法:

常用4種算法:複製算法(Copying)、標記-清除算法(Mark-Sweep)、標記-整理算法、分代收集算法。

(1)、標記-清除(Mark-Sweep)算法:

標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。

主要缺點:

1、一個是效率問題,標記和清除過程的效率都不高。

2、另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致:當程序在以後的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前出發另一次垃圾收集動作。

(2)、複製(Copying)算法:

爲了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因爲能夠使用的內存縮減到原來的一半。很顯然,Copying算法的效率跟存活對象的數目多少有很大的關係,如果存活對象很多,那麼Copying算法的效率將會大大降低。

(2)、標記-整理(Mark-Compact)算法:

爲了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。

(3)、分代收集(Generational Collection)算法:

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不同的區域。一般情況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。

根據不同區域的特點,可以選擇適合的回收算法

1、年輕代(Young Generation)的回收算法:在年輕代中jvm使用的是Mark-copy(標記-複製)算法。

2、老年代(Old Generation)的回收算法:老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact(標記-整理)算法。

3、永久代(Permanent Generation)的回收算法:永久代(permanent generation)也稱爲“方法區(method area)”,他存儲class對象和字符串常量。所以這塊內存區域絕對不是永久的存放從老年代存活下來的對象的。在這塊內存中有可能發生垃圾回收。發生在這裏垃圾回收也被稱爲major GC。

四、gc調優策略

1、代大小優化:

最關鍵參數:-Xms、 -Xmx 、-Xmn 、-XX:SurvivorRatio、-XX:MaxTenuringThreshold、-XX:PermSize、-XX:MaxPermSize

-Xms、 -Xmx 通常設置爲相同的值,避免運行時要不斷擴展JVM內存,這個值決定了JVM heap所能使用的最大內存。

-Xmn 決定了新生代空間的大小,新生代Eden、S0、S1三個區域的比率可以通過-XX:SurvivorRatio來控制(假如值爲 4  表示:Eden:S0:S1 = 4:3:3 )

-XX:MaxTenuringThreshold 控制對象在經過多少次minor GC之後進入老年代,此參數只有在Serial 串行GC時有效。

-XX:PermSize、-XX:MaxPermSize 用來控制方法區的大小,通常設置爲相同的值。

1.避免新生代大小設置過小:
當新生代設置過小時,會產生兩種比較明顯的現象,一是minor GC次數頻繁,二是可能導致 minor GC對象直接進入老年代。當老年代內存不足時,會觸發Full GC。

2.避免新生代大小設置過大:
新生代設置過大,會帶來兩個問題:一是老年代變小,可能導致Full  GC頻繁執行;二是 minor GC 執行回收的時間大幅度增加。

3.避免Survivor區過大或過小:
-XX:SurvivorRatio參數的值越大,就意味着Eden區域變大,minor GC次數會降低,但兩塊Survivor區域變小,如果超過Survivor區域內存大小的對象在minor GC後仍沒被回收,則會直接進入老年代,
-XX:SurvivorRatio參數值設置過小,就意味着Eden區域變小,minor GC觸發次數會增加,Survivor區域變大,意味着可以存儲更多在minor GC後任存活的對象,避免其進入老年代。

4.合理設置對象在新生代存活的週期:
新生代存活週期的值決定了新生代對象在經過多少次Minor GC後進入老年代。因此這個值要根據自己的應用來調優,Jvm參數上這個值對應的爲-XX:MaxTenuringThreshold,默認值爲15次。

2、減少GC開銷的措施:

1)不要顯式調用System.gc()。此函數建議JVM進行主GC,雖然只是建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。大大的影響系統性能。

2)儘量減少臨時對象的使用。臨時對象在跳出函數調用後,會成爲垃圾,少用臨時變量就相當於減少了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減少了主GC的機會。

3)對象不用時最好顯式置爲Null。一般而言,爲Null的對象都會被作爲垃圾處理,所以將不用的對象顯式地設爲Null,有利於GC收集器判定垃圾,從而提高了GC的效率。

4)儘量使用StringBuffer,而不用String來累加字符串。由於String是固定長的字符串對象,累加String對象時,並非在一個String對象中擴增,而是重新創建新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程中會產生多個垃圾對象,因爲對次作“+”操作時都必須創建新的String對象,但這些過渡對象對系統來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。

5)能用基本類型如Int,Long,就不用Integer,Long對象。基本類型變量佔用的內存資源比相應對象佔用的少得多,如果沒有必要,最好使用基本變量。

6)儘量少用靜態對象變量。靜態變量屬於全局變量,不會被GC回收,它們會一直佔用內存。

7)分散對象創建或刪除的時間。集中在短時間內大量創建新對象,特別是大對象,會導致突然需要大量內存,JVM在面臨這種情況時,只能進行主GC,以回收內存或整合內存碎片,從而增加主GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現了大量的垃圾對象,空閒空間必然減少,從而大大增加了下一次創建新對象時強制主GC的機會

3、內存溢出問題:

1、內存泄漏memory leak :是指程序在申請內存後,無法釋放已申請的內存空間,一次內存泄漏似乎不會有大的影響,但內存泄漏堆積後的後果就是內存溢出。

2、內存溢出 out of memory :指程序申請內存時,沒有足夠的內存供申請者使用,或者說,給了你一塊存儲int類型數據的存儲空間,但是你卻存儲long類型的數據,那麼結果就是內存不夠用,此時就會報錯OOM,即所謂的內存溢出。 

二者的關係:

  • 內存泄漏的堆積最終會導致內存溢出
  • 內存溢出就是你要的內存空間超過了系統實際分配給你的空間,此時系統相當於沒法滿足你的需求,就會報內存溢出的錯誤。
  • 內存泄漏是指你向系統申請分配內存進行使用(new),可是使用完了以後卻不歸還(delete),結果你申請到的那塊內存你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程序。就相當於你租了個帶鑰匙的櫃子,你存完東西之後把櫃子鎖上之後,把鑰匙丟了或者沒有將鑰匙還回去,那麼結果就是這個櫃子將無法供給任何人使用,也無法被垃圾回收器回收,因爲找不到他的任何信息。
  • 內存溢出:一個盤子用盡各種方法只能裝4個果子,你裝了5個,結果掉倒地上不能吃了。這就是溢出。比方說棧,棧滿時再做進棧必定產生空間溢出,叫上溢,棧空時再做退棧也產生空間溢出,稱爲下溢。就是分配的內存不足以放下數據項序列,稱爲內存溢出。說白了就是我承受不了那麼多,那我就報錯。

內存溢出的原因及解決方法:

內存溢出原因: 
1.內存中加載的數據量過於龐大,如一次從數據庫取出過多數據; 
2.集合類中有對對象的引用,使用完後未清空,使得JVM不能回收; 
3.代碼中存在死循環或循環產生過多重複的對象實體; 
4.使用的第三方軟件中的BUG; 
5.啓動參數內存值設定的過小
內存溢出的解決方案: 
第一步,修改JVM啓動參數,直接增加內存。(-Xms,-Xmx參數一定不要忘記加。)

第二步,檢查錯誤日誌,查看“OutOfMemory”錯誤前是否有其 它異常或錯誤。

第三步,對代碼進行走查和分析,找出可能發生內存溢出的位置。

內存分配

對象的內存分配,往大方向上講就是在堆上分配,對象主要分配在新生代的Eden Space和From Space,少數情況下會直接分配在老年代。如果新生代的Eden Space和From Space的空間不足,則會發起一次GC,如果進行了GC之後,Eden Space和From Space能夠容納該對象就放在Eden Space和From Space。在GC的過程中,會將Eden Space和From  Space中的存活對象移動到To Space,然後將Eden Space和From Space進行清理。如果在清理的過程中,To Space無法足夠來存儲某個對象,就會將該對象移動到老年代中。在進行了GC之後,使用的便是Eden space和To Space了,下次GC時會將存活對象複製到From Space,如此反覆循環。當對象在Survivor區躲過一次GC的話,其對象年齡便會加1,默認情況下,如果對象年齡達到15歲,就會移動到老年代中。

  一般來說,大對象會被直接分配到老年代,所謂的大對象是指需要大量連續存儲空間的對象,最常見的一種大對象就是大數組,比如:

  byte[] data = new byte[4*1024*1024]

  這種一般會直接在老年代分配存儲空間。

  當然分配的規則並不是百分之百固定的,這要取決於當前使用的是哪種垃圾收集器組合和JVM的相關參數。

https://blog.csdn.net/yy339452689/article/details/104019202

https://blog.csdn.net/know9163/article/details/80574488

https://blog.csdn.net/qq_41701956/article/details/81664921

https://www.bilibili.com/video/BV1dJ411G7YJ?p=2

https://www.cnblogs.com/chengpeng15/p/9850690.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章