JAVA內存區域總結:面試須要瞭解的基本概念

本菜雞參加的大部分java後端面的面試題都有考到。給本身作個總結!java

本文介紹內存區域和各類內存溢出狀況。程序員

1. JAVA內存區域簡介

Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。面試

  • 這些區域有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而一直存在,有些區域則是依賴用戶線程的啓動和結束而創建和銷燬。

具體分爲如下幾個部分:數據庫

  • 程序計數器
  • JAVA虛擬機棧
  • 本地方法棧
  • JAVA堆
  • 方法區
  • 直接內存

2. 程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器後端

因爲Java虛擬機的多線程是經過線程輪流切換、分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。數組

  • 所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲線程私有的內存。

內存溢出狀況

此內存區域是惟一一個在《Java虛擬機規範》中沒有規定任何OutOfMemoryError狀況的區域。緩存

3. JAVA虛擬機棧

虛擬機棧描述的是Java方法執行的線程內存模型:服務器

  • 每一個方法被執行的時候,Java虛擬機都會同步建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態鏈接、方法出口等信息。
  • 每個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
  • Java虛擬機棧是線程私有的,它的生命週期與線程相同。

3.1 局部變量表

局部變量表內容:編譯期可知的各類Java虛擬機的:網絡

  • 基本數據類型(boolean、byte、char、short、int、float、long、double)
  • 對象引用(reference類型,它並不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或者其餘與此對象相關的位置)
  • returnAddress類型(指向了一條字節碼指令的地址)。

局部變量表中的存儲空間以局部變量槽(Slot)來表示,其中64位長度的long和double類型的數據會佔用兩個變量槽。(由虛擬機決定大小)多線程

內存分配時機:局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。

3.2 內存溢出狀況

  1. StackOverflowError異常:當線程請求的棧深度大於虛擬機所容許的深度。
  2. OutOfMemoryError異常: 若是Java虛擬機棧容量能夠動態擴展,當棧擴展時沒法申請到足夠的內存。

4. 本地方法棧

本地方法棧與虛擬機棧所發揮的做用是很是類似的。其區別是:

  • 虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務。
  • 本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。

內存溢出狀況

StackOverflowErrorOutOfMemoryError異常:本地方法棧也會在棧深度溢出或者棧擴展失敗時分別拋出StackOverflowError和OutOfMemoryError異常。

5. JAVA堆

Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。

  • 此內存區域的惟一目的就是存放對象實例,Java世界裏「幾乎」全部的對象實例都在這裏分配內存。
  • Java堆是垃圾收集器管理的內存區域,所以一些資料中它也被稱做「GC堆」。

根據《Java虛擬機規範》的規定,Java堆能夠處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的。

但對於大對象(典型的如數組對象),多數虛擬機實現出於實現簡單、存儲高效的考慮,極可能會要求連續的內存空間。

內存溢出狀況

Java堆既能夠被實現成固定大小的,也能夠是可擴展的。

  • 經過參數-Xmx和-Xms設定。

OutOfMemoryError異常:若是在Java堆中沒有內存完成實例分配,而且堆也沒法再擴展時,Java虛擬機將會拋出。

6. 方法區

方法區是各個線程共享的內存區域。

  • 它用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。

在JDK 8之前,Java程序員都習慣在HotSpot虛擬機上開發、部署程序,因此更願意把方法區稱呼爲永久代

  • 使用永久代來實現方法區的決定並非一個好主意,這種設計致使了Java應用更容易遇到內存溢出的問題。

本質上這二者並非等價的,由於僅僅是當時的HotSpot虛擬機設計團隊選擇把收集器的分代設計擴展至方法區,或者說使用永久代來實現方法區而已,這樣使得HotSpot的垃圾收集器可以像管理Java堆同樣管理這部份內存,省去專門爲方法區編寫內存管理代碼的工做。

在JDK 8中,徹底廢棄了永久代的概念,改用與JRockit、J9同樣在本地內存中實現的元空間來代替,把JDK 7中永久代還剩餘的內容(主要是類型信息)所有移到元空間中。

JDK 7的HotSpot,已經把本來放在永久代的字符串常量池、靜態變量等移出。

6.1 運行時常量池

運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table)。

  • 用於存放編譯期生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。通常來講,除了保存Class文件中描述的符號引用外,還會把由符號引用翻譯出來的直接引用也存儲在運行時常量池中。
  • 另一個重要特徵是具有動態性,Java語言並不要求常量必定只有編譯期才能產生,也就是說,並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也能夠將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法

6.2 內存溢出狀況

OutOfMemoryError異常:在方法區沒法知足新的內存分配需求時

7. 直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是《Java虛擬機規範》中定義的內存區域。

在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。

內存溢出狀況

OutOfMemoryError異常:通常服務器管理員配置虛擬機參數時,會根據實際內存去設置-Xmx等參數信息,但常常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現OutOfMemoryError異常。

8. 常見內存溢出總結

8.1 靜態集合類

當HashMap、LinkedList等等這些容器爲靜態的。

  • 那麼它們的生命週期與程序一致,則容器中的對象在程序結束以前將不能被釋放,從而形成內存泄漏。

總結長生命週期的對象持有短生命週期對象的引用,儘管短生命週期的對象再也不使用,可是由於長生命週期對象持有它的引用而致使不能被回收。

8.2 各類鏈接

例如:數據庫鏈接、網絡鏈接和IO鏈接等。

在對數據庫進行操做的過程當中,首先須要創建與數據庫的鏈接。當再也不使用時,須要調用close方法來釋放與數據庫的鏈接。

  • 只有鏈接被關閉後,垃圾回收器纔會回收對應的對象。不然,若是在訪問數據庫的過程當中,對Connection、Statement或ResultSet不顯性地關閉,將會形成大量的對象沒法被回收,從而引發內存泄漏。

8.3 變量不合理的做用域

一個變量的定義的做用範圍大於其使用範圍,頗有可能會形成內存泄漏。

  • 函數內部的變量寫在類中,當這個變量沒有做用的時候,可是和類的聲明週期同樣長。
  • 建議使用局部變量。

8.4 內部類持有外部類

若是一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即便那個外部類實例對象再也不被使用,但因爲內部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會形成內存泄露。

8.5 改變哈希值

當一個對象被存儲進HashSet集合中之後,就不能修改這個對象中的那些參與計算哈希值的字段了。

  • 不然,對象修改後的哈希值與最初存儲進HashSet集合中時的哈希值就不一樣了。
  • 在這種狀況下,即便在contains方法使用該對象的當前引用做爲的參數去HashSet集合中檢索對象,也將返回找不到對象的結果。
  • 這也會致使沒法從HashSet集合中單獨刪除當前對象,形成內存泄露。

建議使用final類型的類(String、Integer)。

8.6未顯示置爲null

這種狀況看下面程序。

  • 當進行大量的pop操做時,因爲引用未進行置空,gc是不會釋放的
import java.util.Arrays;

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
複製代碼

8.7 緩存泄漏

內存泄漏的另外一個常見來源是緩存,一旦把對象引用放入到緩存中就很容易遺忘。

  • 對於這個問題,可使用WeakHashMap表明緩存(弱引用)。

此種Map的特色是,當除了自身有對key的引用外,此key沒有其餘引用那麼此map會自動丟棄此值。

8.8 監聽器和回調

內存泄漏常見來源是監聽器和其餘回調,若是客戶端在你實現的API中註冊回調,卻沒有顯示的取消,那麼就會積聚。

  • 須要確保回調當即被看成垃圾回收的最佳方法是隻保存他的若引用,例如將他們保存成爲WeakHashMap中的鍵。

本篇文章參考自:《深刻理解Java虛擬機(第3版)》和部分其餘博客。

相關文章
相關標籤/搜索