本身以前複習記錄的隨筆,沒有什麼格式 請勿嫌棄java
雙親委派機制的好處是安全算法
雙親委派機制爲何安全 前面談到雙親委派機制是爲了安全而設計的,可是爲何就安全了呢?舉個例子,ClassLoader加載的class文件來源不少,好比編譯器編譯生成的class、或者網絡下載的字節 碼。而一些來源的class文件是不可靠的,好比我能夠自定義一個java.lang.Integer類來覆蓋jdk中默認的Integer類,例以下面這樣:bootstrap
package java.lang;數組
/**緩存
初始化這個Integer的構造器是會退出JVM,破壞應用程序的正常進行,若是使雙親委派機制的話該Integer類永遠不會被調用,覺得委託BootStrapClassLoader加載後會加載JDK中的Integer類而不會加載自定義的這個安全
jvm 是一個進程 jvm怎麼肯定是同一個類的惟一性:類的全限定類名 和其 類加載器 肯定了 這個類,若是類加載器不同,在jvm 中,這兩個不是同一個類 堆和棧 1.棧:棧是解決程序運行的問題, 2.堆:堆是用來解決數據存儲問題 一個線程會有一個線程棧,內 線程獨有,堆是線程共享的, 堆存對象 棧存基本數據類型和堆中對象的引用(基本數據類型是固定的不會出現引用,因此放在了棧中),局部變量 若是堆中的成員變量是對象,這個成員變量也是放在堆中的 方法區中 存的是常量和常量的引用。被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。 class的成員變量,是放在 堆內存中的 局部變量和基本數據類型:是放在了棧中的。jvm會把【-128-127】之間的數 放在常量池裏,若是一個new integer(2),對象自己是放在了常量池中的, final 修飾的都是常量。類編譯的時候會吧 類,方法,接口中的常量,和string 的直接量放到常量池中 調用一個類的常量,並不會致使初始化該類。(final修飾的類變量且在編譯時已經賦值) class demo(){ String s = 「abc 「;//成員變量,對象自己和對象的引用變量 都放在了 堆內存中,abc是直接量,放在常量池 Public void mm(){ String b = 「efg」; //變量b 在棧中,efg是直接量, 在編譯的時候放入常量池 Int I = 12; //基本數據類型,12是值,直接賦給了i,放在了棧中 Integer in = new integer(1); //局部變量in放在了 棧中,new integer(1)的對象(是常量)放在了 常量池中 } } 好比,局部變量:int i =10,i 放在了棧中 全局變量:static: int I=10, i 和10自己都是在 方法區 方法區中 放置的是,在編譯的時候 的類變量,常量,靜態變量,是在最開始的時候加入的 。固然也能夠經過string。intern的方法加入。因此 運行時產生的對象 都放在了棧裏面 棧表明了處理邏輯,堆表明了數據。面向對象就是堆棧的結合,好比一個對象,對象自己是放在堆中國年做爲數據,對象的行爲(方法)放在了棧中,做爲運行邏輯 常量池:服務器
什麼是常量
用final修飾的成員變量表示常量,值一旦給定就沒法改變!
final修飾的變量有三種:靜態變量、實例變量和局部變量,分別表示三種類型的常量。
複製代碼
常量池主要用於存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量至關於Java語言層面常量的概念,如文本字符串,聲明爲final的常量值等,符號引用則屬於編譯原理方面的概念,包括了以下三種類型的常量:類和接口的全限定名,字段名稱和描述符,方法名稱和描述符 2.基本數據類型的包裝類和常量池 (1)默認建立了數值[-128,127]的相應類型的緩存數據,可是超出此範圍仍然會去建立新的對象。 例如:Integer i1=40,編譯時會變成i1=Integer.valueOf(40);,從而使用常量池中的對象。 (2)string 的「」 和new 「 」是直接從常量池拿對象,經過「 」 和+號 ,產生的字符串會被放入常量池 new 是新生成對象 堆棧:http://pengjiaheng.iteye.com/blog/518623 http://www.jianshu.com/nb/1659284 1.JDK>JRE>JVM>JIT, JIT分爲client模式和server模式兩種,代碼若是執行頻率太高就會被JIT編譯器轉換成二進制執行, JVM是Java跨平臺的依賴,JVM在不一樣平臺中開闢一塊屬於本身的內存,獨立運行本身的任務, JRE除了包含JVM之外,還包括SUN開發出來的接口與實現方便程序猿們利用JAVA語言開發,除了須要瞭解開放的部分Code之外,咱們一般須要熟悉核心的API來知足咱們開發的需求, JDK除了包含JRE還包括一些自帶的命令,如將JAVA代碼轉換成class文件的javac命令、執行class文件的java命令、將註解變成API的javadoc命令、反編譯javap、將java文件轉換成jar包的jar命令,以及一些故障檢測工具和命令等等.....網絡
加載順序:jar包等 2.java 字節碼 是介於java 語言和機器語言之間的中間語言,是部署java代碼的最小單位。java編譯器把java語言變成jvm 能夠理解的java字節碼,字節碼與平臺無關,運行在jvm上數據結構
3.java class Jvm規定了一個方法的大小不能超過65535字節 jvm對低版本的class文件保持向後兼容,即低版本的能夠運行在高版本上多線程
5.運行時數據區 java 運行時會把它管理的內存劃分爲 Java虛擬機在執行Java程序的過程當中會把它管理的內存劃分爲若干個不一樣的數據區域 。 1.程序計數器: 程序計數器能夠看作是當前線程所執行的字節碼的行號指示器。在JVM的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令。 線程私有的內存區域:保證線程輪流切換以後能恢復到正確的執行位置。 線程執行的是java方法,計數器記錄的是正在執行的虛擬機字節碼指令的地址。若是是native方法,值爲空 惟一一個沒有任何outofmemory的區域 native 方法:就是用java調用非java代碼的接口,一個native方法 就是這樣一個java方法:該方法的實現由非java 語言實現,好比c
經過使用本地方法,咱們得以用java實現了jre的與底層系統的交互,甚至JVM的一些部分就是用C寫的,還有,若是咱們要使用一些java語言自己沒有提供封裝的操做系統的特性時,咱們也須要使用本地方法。
2.java 虛擬機棧 (1)Java方法執行的內存模型:每一個方法執行的同時會建立一個棧幀,棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。 (2)線程私有的,它的生命週期與線程相同。
(3)主要關注的stack棧內存, 虛擬機棧中局部變量表部分。局部變量表存放了編譯時期可知的各類基本數據類型和對象引用。 局部變量表所需的內存空間在編譯時期完成分配,當進入一個方法時,這個方法須要在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。 (4)異常狀況: •若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError 異常; •若是虛擬機棧能夠動態擴展,若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常;
3.本地方法棧 (1)同虛擬機棧。區別不過是虛擬機棧爲虛擬機執行Java方法服務(也就是字節碼),而本地方法棧爲虛擬機使用到的Native方法服務。 (2)異常也相似: 同虛擬機棧相同,Java虛擬機規範對這個區域也規定了兩種異常狀況StackOverflowError 和 OutOfMemoryError異常。
4.java堆 (1)線程共享的內存區域,虛擬機建立時使用,用來存放實例對象。幾乎全部對象實例都在這裏分配內存。全部線程共享的一塊內存 (2)Java堆是垃圾回收器管理的主要區域,所以也被稱爲"GC堆"。 從內存回收的角度看,因爲如今收集器基本都採用分代收集算法,因此Java堆能夠細分爲:新生代、老生代; 從內存分配的角度看,線程共享的Java堆可能劃分出多個線程私有的分配緩衝區(TLAB); (3)java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可。可擴展 (4)異常:在堆上沒有完成內存分配,並且堆上也不能再擴展的時候,會拋出OutOfMemoryError異常; (5)內存泄露和內存溢出 Java堆內存的OOM異常是很是常見的異常狀況,重點是根據內存中的對象是不是必要的,來弄清楚究竟是出現了內存泄露(Memory Leak)仍是內存溢出(Memory Overflow). •內存泄露:指程序中一些對象不會被GC所回收,它始終佔用內存,即被分配的對象引用鏈可達但已無用。(可用內存減小) •內存溢出:程序運行過程當中沒法申請到足夠的內存而致使的一種錯誤。內存溢出一般發生於OLD段或Perm段垃圾回收後,仍然無內存空間容納新的Java對象的狀況。 •內存泄露是內存溢出的一種誘因,不是惟一因素。
5.方法區 (1)全部的線程共享的一塊內存區域。存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。 (2)不須要連續的內存,能夠選擇固定大小或者可擴展以外,還能夠選擇不實現垃圾回收。 hotspot
這區域的內存回收目標主要是針對常量池的回收和類型的卸載,通常而言,這個區域的內存回收比較難以使人滿意,尤爲是類型的回收,條件至關苛刻,可是這部分區域的內存回收確實是必要的。 (3)方法區沒法知足內存分配的需求時,將拋出OutOfMemoryError異常。 (4)運行時常量池 Class文件中有一部分是常量池於存放編譯期生成的各類字面量和符號引用(常量和常量對象的引用,如a=‘111’),這部份內容將在類加載後進入方法區的運行時常量池中存放 動態性:不須要必定在編譯期才產生,運行期間也能夠加入常量池,好比String類的intern()方法
String類的intern()方法 String.intern()是一個Native方法,它的做用是:若是字符串常量池中已經包含了一個等於此String對象的字符串,則返回表明池中這個字符串的String對象;不然,將此String對象包含的字符串添加到常量池中,而且返回此字符串的引用。
類加載機制:(類加載是在方法區中生成Class對象,以後全部類的實例化對象都經過這個Class對象產生) 1.定義:虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。Java能夠動態擴展的語言特性就是依賴運行期間動態加載和動態連接這個特色實現的。 1.5 三種機制: 全盤負責:加載一個類時把這個類依賴的引用的都加載 父類委託:加載一個類時先讓父類加載器加載,若是加載不了才從本身的類路徑加載 緩存機制:加載過的class 被緩存,先讀緩存,沒有的時候在加載 2.類的生命週期: 類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個部分統稱爲鏈接。 。 這些階段一般是相互交叉的進行,好比加載未完成可是鏈接可能已經開始進行 3.類加載的時機 主動引用:5種狀況對類進行初始化 1.遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。 2.反射的時候若是沒有初始化 3.當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化 4.虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個類 5.若是一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化 接口的初始化: 和類的初始化一致,區別:接口在初始化時,並不要求其父接口都所有完成了初始化,只有在真正用到父接口的時候纔會初始化(如使用接口的靜態變量)
被動引用:不進行初始化 1.經過子類引用父類的靜態字段,不會致使子類初始化。 2.常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。
3、類加載的過程 虛擬機的類加載機制:虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型, 下面咱們來詳細瞭解類加載的全過程,也就是加載、驗證、準備、解析和初始化這五個階段的過程。 (1)加載:虛擬機須要完成如下三件事情: 1)經過一個類的全限定名來獲取定義此類的二進制字節流。 2)將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。:把二進制字節流按照虛擬機所需的格式存儲在方法區 3)在內存中實例化一個java.lang.Class對象,這個對象將做爲程序訪問方法區中這些類型數據的外部接口 注意:可控;能夠用系統的類加載器完成,也能夠自定義的類加載成器,去控制字節流的獲取方式。
(2)驗證:重要但不必定是必要的階段 驗證是鏈接階段的第一步,目的:確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。 總體上看,驗證階段會完成下面4個階段的檢驗動做:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。 符號引用驗證:是在虛擬機將符號引用轉化爲直接引用的時候進行校驗,這個轉化動做是發生在解析階段。符號引用能夠看作是對類自身之外(常量池的各類符號引用)的信息進行匹配性的校驗。
(3)準備:爲類變量分配內存並設置類變量初始值 ,這些內存都將在方法區中進行分配。 ((1))類變量(被static修飾的變量),不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒被分配在Java堆中。 ((2))初始值爲默認的零值,而不是設置的值。特殊狀況爲ConstantValue屬性:final定義的屬性,準備階段會被賦值 A.例如public static int value = 123 ;value在準備階段後的初始值是0而不是123,由於此時還沒有執行任何的Java方法,而把value賦值爲123的putStatic指令是程序被編譯後,存放在類構造器()方法之中,把value賦值爲123的動做將在初始化階段纔會執行。 b.例如public static final int value = 123 編譯時javac將會爲value生成ConstantValue屬性,在準備階段將變量賦值爲123。 (4)解析:解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。 符號引用(Symbolic Reference): •符號引用以一組符號來描述所引用的目標,引用的目標並不必定已經加載在內存中 直接引用(Direct Reference): •直接引用能夠是直接指向目標的指針、能間接定位到目標的句柄,若是有了直接引用,那引用的目標一定已經在內存中存在。對於同一個符號引用可能會出現屢次解析請求,虛擬機可能會對第一次解析的結果進行緩存。
我的理解:一個java類將會編譯成一個class文件。在編譯時,java類並不知道引用類的實際內存地址,所以只能使用符號引用來代替。好比org.simple.People類引用org.simple.Tool類,在編譯時People類並不知道Tool類的實際內存地址,所以只能使用符號org.simple.Tool(假設)來表示Tool類的地址。而在類加載器加載People類時,此時能夠經過虛擬機獲取Tool類的實際內存地址,所以即可以既將符號org.simple.Tool替換爲Tool類的實際內存地址,及直接引用地址。
(5)初始化:真正開始執行類中定義的Java程序代碼,初始化階段是執行類構造器()方法的過程。 ()方法:做用爲全部類變量(static)的賦值和靜態語句塊(static{}塊) 訪問限制:靜態語句塊中只能訪問到定義在它以前的變量,定義在它以後的變量,能夠賦值,不能訪問。
(2)()不須要顯式調用父類的clinit,虛擬機保證 父類的()方法會在子類的()方法執行以前執行完畢, (3) ()方法對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成()方法。 (6) 多線程:若是有多個線程去同時初始化一個類,那麼只會有一個線程去執行這個類的()方法,其它線程都須要阻塞等待,直到活動線程執行()方法完畢。若是在一個類的()方法中有耗時很長的操做,那麼就可能形成多個進程阻塞。
類加載器:把類的字節碼文件加載到內存中 Java採用雙親委派機制來實現類的加載:雙親委派機制能很好地解決類加載的統一性問題
距離:好比兩個類A和類B都要加載System類:
若是不用委託而是本身加載本身的,那麼類A就會加載一份System字節碼,而後類B又會加載一份System字節碼,這樣內存中就出現了兩份System字節碼。 若是使用委託機制,會遞歸的向父類查找,也就是首選用Bootstrap嘗試加載,若是找不到再向下。這裏的System就能在Bootstrap中找到而後加載,若是此時類B也要加載System,也從Bootstrap開始,此時Bootstrap發現已經加載過了System那麼直接返回內存中的System便可而不須要從新加載,這樣內存中就只有一份System的字節碼了。
能不能本身寫個類叫java.lang.System?
答案:一般不能夠,但能夠採起另類方法達到這個需求。 解釋:爲了避免讓咱們寫System類,類加載採用委託機制,這樣能夠保證爸爸們優先,爸爸們能找到的類,兒子就沒有機會加載。而System類是Bootstrap加載器加載的,就算本身重寫,也老是使用Java系統提供的System,本身寫的System類根本沒有機會獲得加載。
雙親委派機制:特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,若是父類加載器能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,才本身去加載。
類的惟一性:類的全限定類名 和其 類加載器 肯定了 這個類,若是類加載器不同,在jvm 中,這兩個不是同一個類
4.java的動態裝載特性:他會在第一次引用一個class的時候對他進行裝載和鏈接,而不是在編譯期進行 特色:
層級結構:Java裏的類裝載器被組織成了有父子關係的層級結構。Bootstrap類裝載器是全部裝載器的父親。
代理模式:基於層級結構,類的裝載能夠在裝載器之間進行代理。當裝載器裝載一個類時,首先會檢查它是否在父裝載器中進行裝載了。若是上層的裝載器已經裝載了這個類,這個類會被直接使用。反之,類裝載器會請求裝載這個類。
可見性限制:一個子裝載器能夠查找父裝載器中的類,可是一個父裝載器不能查找子裝載器裏的類。
不容許卸載:類裝載器能夠裝載一個類可是不能夠卸載它,不過能夠刪除當前的類裝載器,而後建立一個新的類裝載器。
複製代碼
流程:類加載器被請求裝載的時候,先在緩存裏查看這個類是否已經被本身裝載過了,若是沒有的話,繼續查找父類的緩存,直到在bootstrap類裝載器裏也沒有找到的話,它就會本身在文件系統裏去查找而且加載這個類
啓動類加載器(Bootstrap class loader):這個類裝載器是在JVM啓動的時候建立的。它負責裝載Java API,包含Object對象。和其餘的類裝載器不一樣的地方在於這個裝載器是經過native code來實現的,而不是用Java代碼。(具體的什麼類)
擴展類加載器(Extension class loader):它裝載除了基本的Java API之外的擴展類。它也負責裝載其餘的安全擴展功能。
系統類加載器(System class loader):若是說bootstrap class loader和extension class loader負責加載的是JVM的組件,那麼system class loader負責加載的是應用程序類。它負責加載用戶在$CLASSPATH裏指定的類。
用戶自定義類加載器(User-defined class loader):這是應用程序開發者用直接用代碼實現的類裝載器。
複製代碼
若是自定義一個類 在哪裏加載? system加載器
自定義類加載器:場景:好比網絡傳過來的字節碼,加密了 繼承classloader 類, 從新findclass方法 URLClassLoader() 從遠程主機 加載
啓動(Bootstrap)類加載器 :引導類裝入器是用 本地代碼 實現的類裝入器,它負責將 <Java_Runtime_Home>/lib 下面的類庫加載到內存中。因爲引導類加載器涉及到虛擬機本地實現細節,開發者沒法直接獲取到啓動類加載器的引用,因此不容許直接經過引用進行操做。
標準擴展(Extension)類加載器 :擴展類加載器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 實現的。它負責將 < Java_Runtime_Home >/lib/ext 或者由系統變量 java.ext.dir 指定位置中的類庫加載到內存中。開發者能夠直接使用標準擴展類加載器。
系統(System)類加載器 :系統類加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫加載到內存中。開發者能夠直接使用系統類加載器。
6.Java堆中對象建立、佈局、訪問全過程 new關鍵字生成實例化對象時,先經過ApiClassloader判斷testdemo類的Class對象是否被加載,若是未加載,則加載,最後經過這個Class對象在堆中建立一個testdemo實例對象
類加載檢查:檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類的加載過程。
爲對象分配內存
對象所需內存的大小在類加載完成後便徹底肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從Java堆中劃分出來。
2.1 根據Java堆中是否規整有兩種內存的分配方式:
2.2 分配內存時解決併發問題的兩種方案:
指針碰撞(Bump the pointer):(併發下非線程安全)
Java堆中的內存是規整的,用過的內存都放在一邊,空閒的內存在另外一邊,中間放着一個指針做爲分界點的指示器,分配內存也就是把指針向空閒空間那邊移動一段與內存大小相等的距離。
空閒列表(Free List):
不是規整的,已使用的內存和空閒的內存相互交錯。虛擬機必須維護一張列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。
同步:
對分配內存空間的動做進行同步處理
每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB)。哪一個線程要分配內存,就在哪一個線程的TLAB上分配。只有TLAB用完並分配新的TLAB時,才須要同步鎖定。
內存空間初始化
將分配到的內存空間都初始化爲零(不包括對象頭)
做用:保證了對象的實例的字段在使用中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。默認值
對象設置
對對象進行必要的設置,哪一個類的實例、類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭之中。
<init>
在上面的工做都完成以後,從虛擬機的角度看,一個新的對象已經產生了。
可是從Java程序的角度看,對象的建立纔剛剛開始<init>方法尚未執行,全部的字段都仍是零。
2、對象的內存佈局
複製代碼
在HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
對象頭:
HotSpot虛擬機的對象頭包括兩部分信息。
1.1 對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
1.2 類型指針,即指向它的類數據的指針,肯定這個對象是哪一個類的實例。
(並非全部的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據並不必定要通過對象自己,可參考 三對象的訪問定位)
實例數據:
對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型的字段內容。。
對齊填充:佔位符
若是實例數據部分 沒有對齊,經過對齊填充來補全。由於對象的起始地址必須是8字節的整數倍,對象頭部分正好是8字節的倍數
3、對象的訪問定位:使用句柄和直接指針
複製代碼
Java程序須要經過棧上的引用數據來操做堆上的具體對象。
使用句柄:
Java堆中將會劃分出一塊內存來做爲句柄池,引用中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。
優點:引用中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而引用自己不須要修改。
直接指針:
若是使用直接指針訪問,引用中存儲的直接就是對象地址。
優點:速度更快,節省了一次指針定位的時間開銷。因爲對象的訪問在Java中很是頻繁,所以這類開銷聚沙成塔後也是很是可觀的執行成本。(例如HotSpot)
複製代碼
垃圾回收 一,哪些內存須要回收 二,何時回收 三,怎麼回收
1、GC的工做區域(哪些內存須要回收?) 1.線程私有的不是工做重點:程序計數器、虛擬機棧和本地方法棧。棧幀中:(局部變量表(各類基本數據類型和對象引用)、操做數棧、動態連接、方法出口,)內存大小已知(每一個棧幀中分配多少內存基本上是在類結構肯定下來時就已知的) 2.重點關注:堆和方法方法區的部分:由於一個接口中的多個實現類須要的內存可能不同,一個方法的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,因此垃圾回收器所關注的主要是這部分的內存。 2、垃圾對象的斷定(何時回收?):Java堆中存放着幾乎全部的對象實例,怎麼斷定有用
判斷對象是否存活的算法:
引用計數算法 給對象添加一個引用計數器,每當有個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1,任什麼時候刻計數器爲0的對象就是不可能再被使用的。 (1)優勢:引用計數算法的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的選擇. (2)缺點:Java虛擬機並無選擇這種算法來進行垃圾回收,主要緣由是它很難解決對象之間的相互循環引用問題。 對象objA和objB都有字段instance,賦值令objA.instance = objB;以及objB.instance = objA;,除此以外,這兩個對象再無任何其餘引用,實際上這兩個對象已經不可能再被訪問,可是由於它們互相引用着對方,致使它們的引用計數值都不爲0,引用計數算法沒法通知GC收集器回收它們。 可達性分析算法 這種算法的基本思路是經過一系列名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,就證實此對象是不可用的。 Java語言是經過可達性分析算法來判斷對象是否存活的。
在Java語言裏,可做爲GC Roots的對象包括下面幾種: 虛擬機棧(棧幀中的本地變量表)中引用的對象。 方法區中的類靜態屬性引用的對象。 方法區中的常量引用的對象。 本地方法棧中JNI(Native方法)的引用對象。
正確理解引用: 傳統定義:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。 咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。 引用類型:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
強引用:直接引用。相似Object obj = new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。 軟引用:還有用但並不是必需的對象。軟引用關聯的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。提供了SoftReference類來實現軟引用。 弱引用:非必需對象的,強度更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2以後,提供了WeakReference類來實現弱引用。 虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2以後,提供了PhantomReference類來實現虛引用。
對象死亡的標記過程: 即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:
若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。 若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個由虛擬機自動創建的、低優先級的Finalizer線程去執行它。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣作的緣由是,若是一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的狀況),將極可能會致使F-Queue隊列中其餘對象永久處於等待,甚至致使整個內存回收系統崩潰。 finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在 finalize()中成功拯救本身——只要從新與引用鏈上的任何一個對象創建關聯便可,譬如把本身(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。 下面的代碼演示了兩點:
1.對象能夠在被GC時自我拯救。
2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次 PS : finalize()的運行代價高昂,不肯定性大,沒法保證各個對象的調用順序,應該儘可能避免使用。
回收方法區: 不少人認爲方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規範中確實說過能夠不要求虛擬機在方法區實現垃圾收集,並且在方法區中進行垃圾收集的「性價比」通常比較低:在堆中,尤爲是在新生代中,常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。 回收廢棄常量與回收Java堆中的對象很是相似。
以常量池中字面量的回收爲例,假如一個字符串「abc」已經進入了常量池中,可是當前系統沒有任何一個String對象是叫作「abc」的,換句話說,就是沒有任何String對象引用常量池中的「abc」常量,也沒有其餘地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個「abc」常量就會被系統清理出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
斷定一個常量是不是「廢棄常量」比較簡單,而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面3個條件才能算是「無用的類」:
虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣,不使用了就必然會回收。是否對類進行回收,須要虛擬機的參數進行控制。
在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。
該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
加載該類的ClassLoader已經被回收。
該類對應的java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
複製代碼
3、垃圾收集算法 ( 如何回收?) 因爲垃圾收集算法的實現涉及大量的程序細節,並且各個平臺的虛擬機操做內存的方法又各不相同,如下只是介紹幾種算法的思想及其發展過程。
標記-清除算法:
最基礎的收集算法是「標記-清除」(Mark-Sweep)算法,如同它的名字同樣,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。
缺點:
(1)效率問題,標記和清除兩個過程的效率都不高;
(2)空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
複製算法:
爲了解決效率問題,一種稱爲「複製」(Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。
(1)優勢:每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。
(2)缺點:算法的代價是將內存縮小爲了原來的一半,未免過高了一點。
如今的商業虛擬機都採用這種收集算法來回收新生代,研究代表,新生代中的對象98%是「朝生夕死」的,因此並不須要按照1∶1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。
當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用內存空間爲整個新生代容量的90%,只有10%的內存會被「浪費」。
固然,90%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴其餘內存(這裏指老年代)進行分配擔保(Handle Promotion)。
標記-整理算法:
複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。
根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
分代收集算法:
當前商業虛擬機的垃圾收集都採用「分代收集」(Generational Collection)算法,這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。
(1)在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。
(2)在老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用"標記—清理"或者"標記—整理"算法來進行回收。
複製代碼
方法區永久代 新生代對象轉爲老年代對象: (1)在survivor中的對象有一個計數器,用來記錄通過的垃圾回收次數,當計數器值超過指定數值(默認是15)時採用複製算法複製到老年代中 (2)當新生代剩餘的超過10%,超出的部分放入老年代
垃圾回收器: 併發:多個垃圾回收線程並行工做,用戶線程等待 並行:用戶線程不中止,和回收線程同時執行
新生代GC(Minor GC):指發生在新生代的垃圾收集動做 老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC。比minor 慢10倍以上
Gc收集器: serial: 複製算法,單線程,新生代收集器,會中止工做線程 stop the world 適用:client 模式下的 新生代收集器 優勢:簡單高效,單個CPU,沒有線程交互開銷,效率更高
ParNew serial的多線程版, 適用:server模式下的新生代收集器,能和cms配合 優勢:能與cms 收集器配合工做 比較:在單個CPU的狀況下 不必定有serial好,由於有線程交互 爲何能夠與cms 配合: 由於parallel 和g1 都沒有用傳統的gc收集器框架
Parallel Scavenge,吞吐量收集器 並行的多線程收集器,複製算法,新生代,「吞吐量優先」 目標則是達到一個可控制的吞吐量,而不是縮短用戶線程停頓的時間 適用:多個CPU上,對暫停時間沒有特別高的要求,在後臺處理不須要太多交互的任務 區別:parnew:JVM會根據當前系統運行狀況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,gc自適應的調節策略
Parallel Scavenge vs cms 吞吐量優先
Parallel Scavenge vs parnew Parallel Scavenge 自適應調節策略:Parallel Scavenge收集器有一個參數-XX:+UseAdaptiveSizePolicy。打開以後虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量
Serial Old serial的老年版,單線程,用整理標記算法 適用:client模式,
server 模式:和Parallel Scavenge搭配 或者做爲cms的後預案
Parallel Old parallel的老年版,多線程,標記整理算法 適用:server模式,多cpu,注重吞吐量
CMS收集器,低停頓收集器(Concurrent Mark Swee) 獲取最短回收停頓時間爲目標的收集器.但願響應時間最短的適用。:併發收集、低停頓 CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。 特色: 老年代, 標記-清除算法(內存碎片) 併發收集、低停頓 Cms的流程 :初始標記(標記GC Roots能到的,須要stop the word )-併發標記(GC Roots Tracing的過程)-從新標記(修正標記期間用戶線程運做產生的影響的記錄,停頓比初始標記長 )-併發清除 1).初始標記階段 暫停全部的其餘線程,並記錄下直接與root相連的對象。 2).併發標記階段 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前全部的可達對象。由於用戶線程可能會不斷的更新引用域,因此GC線程沒法保證可達性分析的實時性。因此這個算法裏會跟蹤記錄這些發生引用更新的地方。 3).最終確認標記階段 將上一階段作了指針更新的區域和root合併爲一個僞root集合,並對其作tracing。從而能夠保證真正可達的對象必定被標記了。但同時也會產生一部分被標記爲可達,但其實已是不可達的區域,因爲已經沒有了到達這個區域的路徑,因此並無辦法將它的標誌位置爲0,則形成了一個暫時的內存泄漏,但這部分空間會在下一次收集階段被清掃掉。 4).併發清掃階段 開啓用戶線程,同時GC線程開始對爲標記的區域作清掃。這個過程要注意不要清掃了剛被用戶線程分配的對象。一個小trick就是在這個階段,將全部新分配的對象置爲可達的。
5) 併發重製
清理數據結構,爲下一個併發收集作準備. 適用:與用戶交互較多的場景 第一次實現了垃圾收集線程和用戶線程同時工做
缺點: 1.對cpu 敏感,佔用資源 ,下降吞吐率 2.不能清理浮動垃圾(浮動垃圾:當次gc,因用戶線程而產生的垃圾,本次gc不會回收 因此叫作浮動垃圾) 3.空間碎片
G1 收集器 用來替代cms, 特色: 1.利用多個cpu縮短stop-the-world的時間,併發讓用戶線程執行 2.分代收集:整個gc堆,不需配合。劃分region而不是絕對的新生代老年代 3.不產生空間碎片:(分配爲多個region,從總體看相似標記-整理,從局部(兩個region)看相似複製算法) 4.可預測的停頓,低停頓高吞吐量:優先回收價值最大的Region
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
複製代碼
其中-XX:+UseG1GC爲開啓G1垃圾收集器,-Xmx32g 設計堆內存的最大內存爲32G,-XX:MaxGCPauseMillis=200設置GC的最大暫停時間爲200ms。
每一個區域大小相等,在1M~32M之間。JVM最多支持2000個區域,可推算G1能支持的最大內存爲2000*32M=62.5G。 它們是邏輯的,使用一些非連續的區域(Region)組成的。
適用場景:服務器端應用,大內存,多處理器,應用是爲須要低GC延遲,並具備大堆
比cms更好的場景: (1)超過50%的Java堆被活動數據佔用; (2)GC停頓時間過長(長於0.5至1秒)。
過程: 初始標記(關聯對象,修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對 象,stop-the-word 較短)- 併發標記(找出存活對象,可併發)- 最終標記(修正用戶線程產生的影響)- 篩選回收(region 回收價值篩選)
G1 收集器在老年代堆內存中執行下面的這些階段. 注意有些階段也是年輕代垃圾收集的一部分. 階段
說明 (1) 初始標記(Initial Mark)
(Stop the World Event,全部應用線程暫停) 此時會有一次 stop the world(STW)暫停事件. 在G1中, 這附加在(piggybacked on)一次正常的年輕代GC. 標記可能有引用指向老年代對象的survivor區(根regions). (2) 掃描根區域(Root Region Scanning)
在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。該階段與應用程序(非 STW)同時運行,而且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。 (3) 併發標記(Concurrent Marking)
在整個堆中查找活着的對象. 此階段應用程序的線程正在運行. 此階段能夠被年輕代GC打斷(interrupted). (4) 再次標記(Remark)
(Stop the World Event,全部應用線程暫停) 完成堆內存中存活對象的標記. 使用一個叫作 snapshot-at-the-beginning(SATB, 起始快照)的算法, 該算法比CMS所使用的算法要快速的多. (5) 清理(Cleanup)
(Stop the World Event,全部應用線程暫停,併發執行)
在存活對象和徹底空閒的區域上執行統計(accounting). (Stop the world)
擦寫 Remembered Sets. (Stop the world)
重置空heap區並將他們返還給空閒列表(free list). (Concurrent, 併發) (*) 拷貝(Copying)
(Stop the World Event,全部應用線程暫停) 產生STW事件來轉移或拷貝存活的對象到新的未使用的heap區(new unused regions). 只在年輕代發生時日誌會記錄爲 [GC pause (young)]
. 若是在年輕代和老年代一塊兒執行則會被日誌記錄爲 [GC Pause (mixed)]
.
-XX:+UseG1GC
對象內存分配處理策略: Serial / Serial Old收集器下(ParNew / Serial Old收集器組合的規則也基本一致) 分配:在堆上分配,若是開啓了本地線程緩衝,有如今tlab上,少許也會在老年代 1.對象優先在eden分配 能夠設置java堆大小, 2.大對象直接進入老年代: 虛擬機提供了一個-XX:PretenureSizeThreshold參數,大於這個直接進入老年代(參數只對Serial和ParNew兩款收集器有效) 3.晉升老年代的 閥值,默認15次,參數-XX:MaxTenuringThreshold 4.動態年齡判斷 survivor空間相同年齡的對象佔一半,大於這個年齡的進入老年代 5.空間分配擔保 minor gc以前,會檢查老年代的最大可用連續空間 是否大於新生代全部對象,若是大於, 安全 不大於:是否大於歷次平均值,大於能夠嘗試,可是冒險 擔保:若是剩餘空間 surveyor 不夠裝,會進入老年代 若是擔保失敗,會進行full gc
還有一個問題是,垃圾回收動做什麼時候執行?
當年輕代內存滿時,會引起一次普通GC,該GC僅回收年輕代。須要強調的時,年輕代盡是指Eden代滿,Survivor滿不會引起GC
當年老代滿時會引起Full GC,Full GC將會同時回收年輕代、年老代
當永久代滿時也會引起Full GC,會致使Class、Method元信息的卸載
】
複製代碼
Q:爲何崩潰前垃圾回收的時間愈來愈長? A:根據內存模型和垃圾回收算法,垃圾回收分兩部分:內存標記、清除(複製),標記部分只要內存大小固定時間是不變的,變的是複製部分,由於每次垃圾回收都有一些回收不掉的內存,因此增長了複製量,致使時間延長。因此,垃圾回收的時間也能夠做爲判斷內存泄漏的依據 Q:爲何Full GC的次數愈來愈多? A:所以內存的積累,逐漸耗盡了年老代的內存,致使新對象分配沒有更多的空間,從而致使頻繁的垃圾回收
Q:爲何年老代佔用的內存愈來愈大? A:由於年輕代的內存沒法被回收,愈來愈多地被Copy到年老代
jvm調優:
java -Xms128m -Xmx2g MyApp JVM的初始和最大堆內存大小,(然而JVM能夠在運行時動態的調整堆內存的大小,因此理論上來講咱們有可能會看到堆內存的大小小於初始化堆內存的大小。可是即便在很是低的堆內存使用下,我也歷來沒有遇到過這種狀況。) 等同於:$ java -XX:InitialHeapSize=128m -XX:MaxHeapSize=2g MyApp
-XX:+HeapDumpOnOutOfMemoryError 讓JVM在發生內存溢出時自動的生成堆內存快照 -XX:OnOutOfMemoryError 當內存溢發生時,咱們甚至能夠能夠執行一些指令
$ java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:OnOutOfMemoryError ="sh ~/cleanup.sh" MyApp Perm不屬於堆內存,有虛擬機直接分配,但能夠經過 -XX:PermSize and -XX:MaxPermSize 永久帶的初始大小和最大
(4)在配置較好的機器上(好比多核、大內存),能夠爲年老代選擇並行收集算法: -XX:+UseParallelOldGC ,默認爲Serial收集
(4)能夠經過下面的參數打Heap Dump信息
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
經過下面參數能夠控制OutOfMemoryError時打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError
複製代碼
jvm啓動流程
配置JVM裝載環境 Java代碼執行時須要一個JVM環境,JVM環境的建立包括兩部分:JVM.dll文件的查找和裝載。 虛擬機參數解析 裝載完JVM環境以後,須要對啓動參數進行解析,其實在裝載JVM環境的過程當中已經解析了部分參數 設置線程棧大小 執行main方法 jvm調優 -Xms 初始堆大小 -Xmx 最大堆大小 -Xmn 新生代大小 -XX:NewRatio 新生代與老年代的比例 =2,新生代1/3 老年代2/3 -XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。默認值爲 8 -XX:PermSize 永久代(方法區)的初始大小 -XX:MaxPermSize 永久代(方法區)的最大值 -XX:+HeapDumpOnOutOfMemoryError 內存溢出時的快照
Jvm老年代: 大對象(須要大量連續內存空間的java對象,好比很長的字符串or數組)會直接分配在老年代 Full gc觸發條件: 一、程序調用System.gc() 二、老年代空間不足、永久代(jdk8以後不存在永久代)空間不足 三、CMS GC時出現promotion failed和concurrent mode failure,對於採用CMS進行老年代GC的程序而言,尤爲要注意GC日誌中是否有promotion failed和concurrent mode failure兩種情況,當這兩種情況出現時可能會觸發Full GC。promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入老年代,而此時老年代也放不下形成的;concurrent mode failure是在執行CMS GC的過程當中同時有對象要放入老年代,而此時老年代空間不足形成的(有時候「空間不足」是CMS GC時當前的浮動垃圾過多致使暫時性的空間不足觸發Full GC)。應對措施爲:增大survivor space、老年代空間或調低觸發併發GC的比率