Java內存模型-本機內存

Java內存模型-本機內存

BangQ IT哈哈
Java堆空間是在編寫Java程序中被咱們使用得最頻繁的內存空間,平時開發過程,開發人員必定遇到過OutOfMemoryError,這種結果有可能來源於Java堆空間的內存泄漏,也多是由於堆的大小不夠而致使的,有時候這些錯誤是能夠依靠開發人員修復的,可是隨着Java程序須要處理愈來愈多的併發程序,可能有些錯誤就不是那麼容易處理了。有些時候即便Java堆空間沒有滿也可能拋出錯誤,這種狀況下須要瞭解的就是JRE(Java Runtime Environment)內部到底發生了什麼。Java自己的運行宿主環境並非操做系統,而是Java虛擬機,Java虛擬機自己是用C編寫的本機程序,天然它會調用到本機資源,最多見的就是針對本機內存的調用。本機內存是能夠用於運行時進程的,它和Java應用程序使用的Java堆內存不同,每一種虛擬化資源都必須存儲在本機內存裏面,包括虛擬機自己運行的數據,這樣也意味着主機的硬件和操做系統在本機內存的限制將直接影響到Java應用程序的性能。java

  i.Java運行時如何使用本機內存:

  1)堆空間和垃圾回收

  Java運行時是一個操做系統進程(Windows下通常爲java.exe),該環境提供的功能會受一些位置的用戶代碼驅動,這雖然提升了運行時在處理資源的靈活性,可是沒法預測每種狀況下運行時環境須要何種資源,這一點Java堆空間講解中已經提到過了。在Java命令行可使用-Xmx和-Xms來控制堆空間初始配置,mx表示堆空間的最大大小,ms表示初始化大小,這也是上提到的啓動Java的配置文件能夠配置的內容。儘管邏輯內存堆能夠根據堆上的對象數量和在GC上花費的時間增長或者減小,可是使用本機內存的大小是保持不變的,並且由-Xms的值指定,大部分GC算法都是依賴被分配的連續內存塊的堆空間,所以不能在堆須要擴大的時候分配更多的本機內存,全部的堆內存必須保留下來,請注意這裏說的不是Java堆內存空間是本機內存。
  本機內存保留和本機內存分配不同,本機內存被保留的時候,沒法使用物理內存或者其餘存儲器做爲備用內存,儘管保留地址空間塊不會耗盡物理資源,可是會阻止內存用於其餘用途,由保留從未使用過的內存致使的泄漏和泄漏分配的內存形成的問題其嚴重程度差很少,但使用的堆區域縮小時,一些垃圾回收器會回收堆空間的一部份內容,從而減小物理內存的使用。對於維護Java堆的內存管理系統,須要更多的本機內存來維護它的狀態,進行垃圾收集的時候,必須分配數據結構來跟蹤空閒存儲空間和進度記錄,這些數據結構的確切大小和性質因實現的不一樣而有所差別。算法

  2)JIT

  JIT編譯器在運行時編譯Java字節碼來優化本機可執行代碼,這樣極大提升了Java運行時的速度,而且支持Java應用程序與本地代碼至關的速度運行。字節碼編譯使用本機內存,並且JIT編譯器的輸入(字節碼)和輸出(可執行代碼)也必須存儲在本機內存裏面,包含了多個通過JIT編譯的方法的Java程序會比一些小型應用程序使用更多的本機內存。編程

  3)類和類加載器

  Java 應用程序由一些類組成,這些類定義對象結構和方法邏輯。Java 應用程序也使用 Java 運行時類庫(好比 java.lang.String)中的類,也可使用第三方庫。這些類須要存儲在內存中以備使用。存儲類的方式取決於具體實現。Sun JDK 使用永久生成(permanent generation,PermGen)堆區域,從最基本的層面來看,使用更多的類將須要使用更多內存。(這可能意味着您的本機內存使用量會增長,或者您必須明確地從新設置 PermGen 或共享類緩存等區域的大小,以裝入全部類)。記住,不只您的應用程序須要加載到內存中,框架、應用服務器、第三方庫以及包含類的 Java 運行時也會按需加載並佔用空間。Java 運行時能夠卸載類來回收空間,可是隻有在很是嚴酷的條件下才會這樣作,不能卸載單個類,而是卸載類加載器,隨其加載的全部類都會被卸載。只有在如下狀況下才能卸載類加載器bootstrap

  • Java 堆不包含對錶示該類加載器的 java.lang.ClassLoader 對象的引用。
  • Java 堆不包含對錶示類加載器加載的類的任何 java.lang.Class 對象的引用。
  • 在 Java 堆上,該類加載器加載的任何類的全部對象都再也不存活(被引用)。
      須要注意的是,Java 運行時爲全部 Java 應用程序建立的 3 個默認類加載器( bootstrap、extension 和 application )都不可能知足這些條件,所以,任何系統類(好比 java.lang.String)或經過應用程序類加載器加載的任何應用程序類都不能在運行時釋放。即便類加載器適合進行收集,運行時也只會將收集類加載器做爲 GC 週期的一部分。一些實現只會在某些 GC 週期中卸載類加載器,也可能在運行時生成類,而不去釋放它。許多 Java EE 應用程序使用 JavaServer Pages (JSP) 技術來生成 Web 頁面。使用 JSP 會爲執行的每一個 .jsp 頁面生成一個類,而且這些類會在加載它們的類加載器的整個生存期中一直存在 —— 這個生存期一般是 Web 應用程序的生存期。另外一種生成類的常見方法是使用 Java 反射。反射的工做方式因 Java 實現的不一樣而不一樣,當使用 java.lang.reflect API 時,Java 運行時必須將一個反射對象(好比 java.lang.reflect.Field)的方法鏈接到被反射到的對象或類。這能夠經過使用 Java 本機接口(Java Native Interface,JNI)訪問器來完成,這種方法須要的設置不多,可是速度緩慢,也能夠在運行時爲您想要反射到的每種對象類型動態構建一個類。後一種方法在設置上更慢,但運行速度更快,很是適合於常常反射到一個特定類的應用程序。Java 運行時在最初幾回反射到一個類時使用 JNI 方法,但當使用了若干次 JNI 方法以後,訪問器會膨脹爲字節碼訪問器,這涉及到構建類並經過新的類加載器進行加載。執行屢次反射可能致使建立了許多訪問器類和類加載器,保持對反射對象的引用會致使這些類一直存活,並繼續佔用空間,由於建立字節碼訪問器很是緩慢,因此 Java 運行時能夠緩存這些訪問器以備之後使用,一些應用程序和框架還會緩存反射對象,這進一步增長了它們的本機內存佔用。

      4)JNI

      JNI支持本機代碼調用Java方法,反之亦然,Java運行時自己極大依賴於JNI代碼來實現類庫功能,好比文件和網絡I/O,JNI應用程序能夠經過三種方式增長Java運行時對本機內存的使用:數組

  • JNI應用程序的本機代碼被編譯到共享庫中,或編譯爲加載到進程地址空間中的可執行文件,大型本機應用程序可能僅僅加載就會佔用大量進程地址空間
  • 本機代碼必須與Java運行時共享地址空間,任何本機代碼分配或本機代碼執行的內存映射都會耗用Java運行時內存
  • 某些JNI函數可能在它們的常規操做中使用本機內存,GetTypeArrayElements和GetTypeArrayRegion函數能夠將Java堆複製到本機內存緩衝區中,提供給本地代碼使用,是否複製數據依賴於運行時實現,經過這種方式訪問大量Java堆數據就可能使用大量的本機內存堆空間

      5)NIO

      JDK 1.4開始添加了新的I/O類,引入了一種基於通道和緩衝區執行I/O的新方式,就像Java堆上的內存支持I/O緩衝區同樣,NIO添加了對直接ByteBuffer的支持,ByteBuffer受本機內存而不是Java堆的支持,直接ByteBuffer能夠直接傳遞到本機操做系統庫函數,以執行I/O,這種狀況雖然提升了Java程序在I/O的執行效率,可是會對本機內存進行直接的內存開銷。ByteBuffer直接操做和非直接操做的區別以下:
    Java內存模型-本機內存
      對於在何處存儲直接 ByteBuffer 數據,很容易產生混淆。應用程序仍然在 Java 堆上使用一個對象來編排 I/O 操做,但持有該數據的緩衝區將保存在本機內存中,Java 堆對象僅包含對本機堆緩衝區的引用。非直接 ByteBuffer 將其數據保存在 Java 堆上的 byte[] 數組中。直接ByteBuffer對象會自動清理本機緩衝區,但這個過程只能做爲Java堆GC的一部分執行,它不會自動影響施加在本機上的壓力。GC僅在Java堆被填滿,以致於沒法爲堆分配請求提供服務的時候,或者在Java應用程序中顯示請求它發生。緩存

      6)線程:

      應用程序中的每一個線程都須要內存來存儲器堆棧(用於在調用函數時持有局部變量並維護狀態的內存區域)。每一個 Java 線程都須要堆棧空間來運行。根據實現的不一樣,Java 線程能夠分爲本機線程和 Java 堆棧。除了堆棧空間,每一個線程還須要爲線程本地存儲(thread-local storage)和內部數據結構提供一些本機內存。儘管每一個線程使用的內存量很是小,但對於擁有數百個線程的應用程序來講,線程堆棧的總內存使用量可能很是大。若是運行的應用程序的線程數量比可用於處理它們的處理器數量多,效率一般很低,而且可能致使糟糕的性能和更高的內存佔用。安全

      ii.本機內存耗盡:

      Java運行時善於以不一樣的方式來處理Java堆空間的耗盡和本機堆空間的耗盡,可是這兩種情形具備相似症狀,當Java堆空間耗盡的時候,Java應用程序很難正常運行,由於Java應用程序必須經過分配對象來完成工做,只要Java堆被填滿,就會出現糟糕的GC性能,而且拋出OutOfMemoryError。相反,一旦 Java 運行時開始運行而且應用程序處於穩定狀態,它能夠在本機堆徹底耗盡以後繼續正常運行,不必定會發生奇怪的行爲,由於須要分配本機內存的操做比須要分配 Java 堆的操做少得多。儘管須要本機內存的操做因 JVM 實現不一樣而異,但也有一些操做很常見:啓動線程、加載類以及執行某種類型的網絡和文件 I/O。本機內存不足行爲與 Java 堆內存不足行爲也不太同樣,由於沒法對本機堆分配進行控制,儘管全部 Java 堆分配都在 Java 內存管理系統控制之下,但任何本機代碼(不管其位於 JVM、Java 類庫仍是應用程序代碼中)均可能執行本機內存分配,並且會失敗。嘗試進行分配的代碼而後會處理這種狀況,不管設計人員的意圖是什麼:它可能經過 JNI 接口拋出一個 OutOfMemoryError,在屏幕上輸出一條消息,發生無提示失敗並在稍後再試一次,或者執行其餘操做。服務器

      iii.例子:

      這篇文章一致都在講概念,這裏既然提到了ByteBuffer,先提供一個簡單的例子演示該類的使用:網絡

      ——[$]使用NIO讀取txt文件——

package org.susan.java.io;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class ExplicitChannelRead {
    public static void main(String args[]){
        FileInputStream fileInputStream;
        FileChannel fileChannel;
        long fileSize;
        ByteBuffer byteBuffer;
        try{
            fileInputStream = new FileInputStream("D://read.txt");
            fileChannel = fileInputStream.getChannel();
            fileSize = fileChannel.size();
            byteBuffer = ByteBuffer.allocate((int)fileSize);
            fileChannel.read(byteBuffer);
            byteBuffer.rewind();
            for( int i = 0; i < fileSize; i++ )
                System.out.print((char)byteBuffer.get());
            fileChannel.close();
            fileInputStream.close();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }
}

  在讀取文件的路徑放上該txt文件裏面寫入:Hello World,上邊這段代碼就是使用NIO的方式讀取文件系統上的文件,這段程序的輸入就爲:數據結構

Hello World

  ——[$]獲取ByteBuffer上的字節轉換爲Byte數組——

package org.susan.java.io;

import java.nio.ByteBuffer;

public class ByteBufferToByteArray {
    public static void main(String args[]) throws Exception{
        // 從byte數組建立ByteBuffer
        byte[] bytes = new byte[10];
        ByteBuffer buffer = ByteBuffer.wrap(bytes);

        // 在position和limit,也就是ByteBuffer緩衝區的首尾之間讀取字節
        bytes = new byte[buffer.remaining()];
        buffer.get(bytes, 0, bytes.length);

        // 讀取全部ByteBuffer內的字節
        buffer.clear();
        bytes = new byte[buffer.capacity()];
        buffer.get(bytes, 0, bytes.length);
    }
}

  上邊代碼就是從ByteBuffer到byte數組的轉換過程,有了這個過程在開發過程當中可能更加方便,ByteBuffer的詳細講解我保留到IO部分,這裏僅僅是涉及到了一些,因此提供兩段實例代碼。

  iv.共享內存:

  在Java語言裏面,沒有共享內存的概念,可是在某些引用中,共享內存卻很受用,例如Java語言的分佈式系統,存着大量的Java分佈式共享對象,不少時候須要查詢這些對象的狀態,以查看系統是否運行正常或者瞭解這些對象目前的一些統計數據和狀態。若是使用的是網絡通訊的方式,顯然會增長應用的額外開銷,也增長了沒必要要的應用編程,若是是共享內存方式,則能夠直接經過共享內存查看到所須要的對象的數據和統計數據,從而減小一些沒必要要的麻煩。

  1)共享內存特色:

  • 能夠被多個進程打開訪問
  • 讀寫操做的進程在執行讀寫操做的時候其餘進程不能進行寫操做
  • 多個進程能夠交替對某一個共享內存執行寫操做
  • 一個進程執行了內存寫操做事後,不影響其餘進程對該內存的訪問,同時其餘進程對更新後的內存具備可見性
  • 在進程執行寫操做時若是異常退出,對其餘進程的寫操做禁止自動解除
  • 相對共享文件,數據訪問的方便性和效率  

      2)出現狀況:

  • 獨佔的寫操做,相應有獨佔的寫操做等待隊列。獨佔的寫操做自己不會發生數據的一致性問題;
  • 共享的寫操做,相應有共享的寫操做等待隊列。共享的寫操做則要注意防止發生數據的一致性問題;
  • 獨佔的讀操做,相應有共享的讀操做等待隊列;
  • 共享的讀操做,相應有共享的讀操做等待隊列;

      3)Java中共享內存的實現:

      JDK 1.4裏面的MappedByteBuffer爲開發人員在Java中實現共享內存提供了良好的方法,該緩衝區其實是一個磁盤文件的內存映象,兩者的變化會保持同步,即內存數據發生變化事後會當即反應到磁盤文件中,這樣會有效地保證共享內存的實現,將共享文件和磁盤文件簡歷聯繫的是文件通道類:FileChannel,該類的加入是JDK爲了統一外圍設備的訪問方法,而且增強了多線程對同一文件進行存取的安全性,這裏可使用它來創建共享內存用,它創建了共享內存和磁盤文件之間的一個通道。打開一個文件可以使用RandomAccessFile類的getChannel方法,該方法直接返回一個文件通道,該文件通道因爲對應的文件設爲隨機存取,一方面能夠進行讀寫兩種操做,另一個方面使用它不會破壞映象文件的內容。這裏,若是使用FileOutputStream和FileInputStream則不能理想地實現共享內存的要求,由於這兩個類同時實現自由讀寫很困難。
      下邊代碼段實現了上邊說起的共享內存功能

// 得到一個只讀的隨機存取文件對象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 得到相應的文件通道
FileChannel fc = RAFile.getChannel();
// 取得文件的實際大小
int size = (int)fc.size();
// 得到共享內存緩衝區,該共享內存只讀 
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);
// 得到一個可讀寫的隨機存取文件對象 
RAFile = new RandomAccessFile(filename,"rw");
// 得到相應的文件通道 
fc = RAFile.getChannel();
// 取得文件的實際大小,以便映像到共享內存 
size = (int)fc.size();
// 得到共享內存緩衝區,該共享內存可讀寫 
mapBuf = fc.map(FileChannel.MAP_RW,0,size);
// 獲取頭部消息:存取權限 
mode = mapBuf.getInt();

  若是多個應用映象使用同一文件名的共享內存,則意味着這多個應用共享了同一內存數據,這些應用對於文件能夠具備同等存取權限,一個應用對數據的刷新會更新到多個應用中。爲了防止多個應用同時對共享內存進行寫操做,能夠在該共享內存的頭部信息加入寫操做標記,該共享文件的頭部基本信息至少有:

  • 共享內存長度
  • 共享內存目前的存取模式
      共享文件的頭部信息是私有信息,多個應用能夠對同一個共享內存執行寫操做,執行寫操做和結束寫操做的時候,可使用以下方法:
public boolean startWrite()
{
    if(mode == 0) // 這裏mode表明共享內存的存取模式,爲0表明可寫
    {
        mode = 1; // 意味着別的應用不可寫
        mapBuf.flip();
        mapBuf.putInt(mode);    //寫入共享內存的頭部信息
        return true;
    }
    else{
        return false; //代表已經有應用在寫該共享內存了,本應用不可以針對共享內存再作寫操做
    }
}

public boolean stopWrite()
{
    mode = 0; // 釋放寫權限
    mapBuf.flip();
    mapBuf.putInt(mode);    //寫入共享內存頭部信息
    return true;
}

  【:上邊提供了對共享內存執行寫操做過程的兩個方法,這兩個方法其實理解起來很簡單,真正須要思考的是一個針對存取模式的設置,其實這種機制和最前面提到的內存的鎖模式有點相似,一旦當mode(存取模式)設置稱爲可寫的時候,startWrite才能返回true,不只僅如此,某個應用程序在向共享內存寫入數據的時候還會修改其存取模式,由於若是不修改的話就會致使其餘應用一樣針對該內存是可寫的,這樣就使得共享內存的實現變得混亂,而在中止寫操做stopWrite的時候,須要將mode設置稱爲1,也就是上邊註釋段提到的釋放寫權限。】
  關於鎖的知識這裏簡單作個補充【
:上邊代碼的這種模式能夠理解爲一種簡單的鎖模式】:通常狀況下,計算機編程中會常常遇到鎖模式,在整個鎖模式過程當中能夠將鎖分爲兩類(這裏只是輔助理解,不是嚴格的鎖分類)——共享鎖和排他鎖(也稱爲獨佔鎖),鎖的定位是定位於針對全部與計算機有關的資源好比內存、文件、存儲空間等,針對這些資源均可能出現鎖模式。在上邊堆和棧一節講到了Java對象鎖,其實不只僅是對象,只要是計算機中會出現寫入和讀取共同操做的資源,都有可能出現鎖模式。
  共享鎖——當應用程序得到了資源的共享鎖的時候,那麼應用程序就能夠直接訪問該資源,資源的共享鎖能夠被多個應用程序拿到,在Java裏面線程之間有時候也存在對象的共享鎖,可是有一個很明顯的特徵,也就是內存共享鎖只能讀取數據,不可以寫入數據,不管是什麼資源,當應用程序僅僅只能拿到該資源的共享鎖的時候,是不可以針對該資源進行寫操做的。
  獨佔鎖——當應用程序得到了資源的獨佔鎖的時候,應用程序訪問該資源在共享鎖上邊多了一個權限就是寫權限,針對資源自己而言,一個資源只有一把獨佔鎖,也就是說一個資源只能同時被一個應用或者一個執行代碼程序容許寫操做,Java線程中的對象寫操做也是這個道理,若某個應用拿到了獨佔鎖的時候,不只僅能夠讀取資源裏面的數據,並且能夠向該資源進行數據寫操做。
  數據一致性——當資源同時被應用進行讀寫訪問的時候,有可能會出現數據一致性問題,好比A應用拿到了資源R1的獨佔鎖,B應用拿到了資源R1的共享鎖,A在針對R1進行寫操做,而兩個應用的操做——A的寫操做和B的讀操做出現了一個時間差,s1的時候B讀取了R1的資源,s2的時候A寫入了數據修改了R1的資源,s3的時候B又進行了第二次讀,而兩次讀取相隔時間比較短暫並且初衷沒有考慮到A在B的讀取過程修改了資源,這種狀況下針對鎖模式就須要考慮到數據一致性問題。獨佔鎖的排他性在這裏的意思是該鎖只能被一個應用獲取,獲取過程只能由這個應用寫入數據到資源內部,除非它釋放該鎖,不然其餘拿不到鎖的應用是沒法對資源進行寫入操做的。
  按照上邊的思路去理解代碼裏面實現共享內存的過程就更加容易理解了。
  若是執行寫操做的應用異常停止,那麼映像文件的共享內存將再也不能執行寫操做。爲了在應用異常停止後,寫操做禁止標誌自動消除,必須讓運行的應用獲知退出的應用。在多線程應用中,能夠用同步方法得到這樣的效果,可是在多進程中,同步是不起做用的。方法能夠採用的多種技巧,這裏只是描述一可能的實現:採用文件鎖的方式。寫共享內存應用在得到對一個共享內存寫權限的時候,除了判斷頭部信息的寫權限標誌外,還要判斷一個臨時的鎖文件是否能夠獲得,若是能夠獲得,則即便頭部信息的寫權限標誌爲1(上述),也能夠啓動寫權限,其實這已經代表寫權限得到的應用已經異常退出,這段代碼以下:

// 打開一個臨時文件,注意統一共享內存,該文件名必須相同,能夠在共享文件名後邊添加「.lock」後綴
RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
// 獲取文件通道
FileChannel lockFileChannel = files.getChannel();
// 獲取文件的獨佔鎖,該方法不產生任何阻塞直接返回
FileLock fileLock = lockFileChannel.tryLock();
// 若是爲空表示已經有應用佔有了
if( fileLock == null ){
    // ...不可寫
}else{
    // ...能夠執行寫操做
}

  4)共享內存的應用:

  在Java中,共享內存通常有兩種應用:
  [1]永久對象配置——在java服務器應用中,用戶可能會在運行過程當中配置一些參數,而這些參數須要永久 有效,當服務器應用從新啓動後,這些配置參數仍然能夠對應用起做用。這就能夠用到該文 中的共享內存。該共享內存中保存了服務器的運行參數和一些對象運行特性。能夠在應用啓動時讀入以啓用之前配置的參數。
  [2]查詢共享數據——一個應用(例 sys.java)是系統的服務進程,其系統的運行狀態記錄在共享內存中,其中運行狀態多是不斷變化的。爲了隨時瞭解系統的運行狀態,啓動另外一個應用(例 mon.java),該應用查詢該共享內存,彙報系統的運行狀態。

  v.小節:

  提供本機內存以及共享內存的知識,主要是爲了讓讀者可以更順利地理解JVM內部內存模型的物理原理,包括JVM如何和操做系統在內存這個級別進行交互,理解了這些內容就讓讀者對Java內存模型的認識會更加深刻,並且不容易遺忘。其實Java的內存模型遠不及咱們想象中那麼簡單,並且其結構極端複雜,看過《Inside JVM》的朋友應該就知道,結合JVM指令集去寫點小代碼測試.class文件的裏層結構也不失爲一種好玩的學習方法。

相關文章
相關標籤/搜索