天天看點

JVM

每個jvm都有兩種機制:

一個是裝載具有合适名稱的類(類或是接口),叫做類裝載子系統; 另外的一個負責執行包含在已裝載的類或接口中的指令,叫做運作引擎。 每個jvm又包括方法區、堆、 java棧、程式計數器和本地方法棧這五個部分, 這幾個部分和類裝載機制與運作引擎機制一起組成的體系結構圖為:
JVM

問:堆和棧有什麼差別

答:堆是存放對象的,但是對象内的臨時變量是存在棧記憶體中,如例子中的methodvar是在運作期存放到棧中的。

棧是跟随線程的,有線程就有棧,堆是跟随jvm的,有jvm就有堆記憶體。

問:堆記憶體中到底存在着什麼東西?

答:對象,包括對象變量以及對象方法。

問:類變量和執行個體變量有什麼差別?

答:靜态變量是類變量,非靜态變量是執行個體變量,直白的說,有static修飾的變量是靜态變量,沒有static修飾的變量是執行個體變量。靜态變量存在方法區中,執行個體變量存在堆記憶體中。

問:我聽說類變量是在jvm啟動時就初始化好的,和你這說的不同呀!

答:那你是道聽途說,信我的,沒錯。

問:java的方法(函數)到底是傳值還是傳址?

答:都不是,是以傳值的方式傳遞位址,具體的說原生資料類型傳遞的值,引用類型傳遞的位址。對于原始資料類型,jvm的處理方法是從method area或heap中拷貝到stack,然後運作frame中的方法,運作完畢後再把變量指拷貝回去。

問:為什麼會産生outofmemory産生?

答:一句話:heap記憶體中沒有足夠的可用記憶體了。這句話要好好了解,不是說heap沒有記憶體了,是說新申請記憶體的對象大于heap空閑記憶體,比如現在heap還空閑1m,但是新申請的記憶體需要1.1m,于是就會報outofmemory了,可能以後的對象申請的記憶體都隻要0.9m,于是就隻出現一次outofmemory,gc也正常了,看起來像偶發事件,就是這麼回事。 但如果此時gc沒有回收就會産生挂起情況,系統不響應了。

問:我産生的對象不多呀,為什麼還會産生outofmemory?

答:你繼承層次忒多了,heap中 産生的對象是先産生 父類,然後才産生子類,明白不?

問:outofmemory錯誤分幾種?

答:分兩種,分别是“outofmemoryerror:java heap size”和”outofmemoryerror: permgen space”,兩種都是記憶體溢出,heap size是說申請不到新的記憶體了,這個很常見,檢查應用或調整堆記憶體大小。

“permgen space”是因為永久存儲區滿了,這個也很常見,一般在熱釋出的環境中出現,是因為每次釋出應用系統都不重新開機,久而久之永久存儲區中的死對象太多導緻新對象無法申請記憶體,一般重新啟動一下即可。

問:為什麼會産生stackoverflowerror?

答:因為一個線程把stack記憶體全部耗盡了,一般是遞歸函數造成的。

問:一個機器上可以看多個jvm嗎?jvm之間可以互訪嗎?

答:可以多個jvm,隻要機器承受得了。jvm之間是不可以互訪,你不能在a-jvm中通路b-jvm的heap記憶體,這是不可能的。在以前老版本的jvm中,會出現a-jvm crack後影響到b-jvm,現在版本非常少見。

問:為什麼java要采用垃圾回收機制,而不采用c/c++的顯式記憶體管理?

答:為了簡單,記憶體管理不是每個程式員都能折騰好的。

問:為什麼你沒有詳細介紹垃圾回收機制?

答:垃圾回收機制每個jvm都不同,jvm specification隻是定義了要自動釋放記憶體,也就是說它隻定義了垃圾回收的抽象方法,具體怎麼實作各個廠商都不同,算法各異,這東西實在沒必要深入。

問:jvm中到底哪些區域是共享的?哪些是私有的?

答:heap和method area是共享的,其他都是私有的,

問:什麼是jit,你怎麼沒說?

答:jit是指just in time,有的文檔把jit作為jvm的一個部件來介紹,有的是作為執行引擎的一部分來介紹,這都能了解。java剛誕生的時候是一個解釋性語言,别噓,即使編譯成了位元組碼(byte code)也是針對jvm的,它需要再次翻譯成原生代碼(native code)才能被機器執行,于是效率的擔憂就提出來了。sun為了解決該問題提出了一套新的機制,好,你想編譯成原生代碼,沒問題,我在jvm上提供一個工具,把位元組碼編譯成原生碼,下次你來通路的時候直接通路原生碼就成了,于是jit就誕生了,就這麼回事。

問:jvm還有哪些部分是你沒有提到的?

答:jvm是一個異常複雜的東西,寫一本磚頭書都不為過,還有幾個要說明的:

常量池(constant pool):按照順序存放程式中的常量,并且進行索引編号的區域。比如int i =100,這個100就放在常量池中。

安全管理器(security manager):提供java運作期的安全控制,防止惡意攻擊,比如指定讀取檔案,寫入檔案權限,網絡通路,建立程序等等,class loader在security manager認證通過後才能加載class檔案的。

方法索引表(methods table),記錄的是每個method的位址資訊,stack和heap中的位址指針其實是指向methods table位址。

問:為什麼不建議在程式中顯式的生命system.gc()?

答:因為顯式聲明是做堆記憶體全掃描,也就是full gc,是需要停止所有的活動的(stop the world collection),你的應用能承受這個嗎?

問:jvm有哪些調整參數?

答:非常多,自己去找,堆記憶體、棧記憶體的大小都可以定義,甚至是堆記憶體的三個部分、新生代的各個比例都能調整。

jvm包括下列幾個運作時資料區域:

1.程式計數器(program counter register):

每一個java線程都有一個程式計數器來用于儲存程式執行到目前方法的哪一個指令,對于非native方法,這個區域記錄的是正在執行的vm原語的位址,如果正在執行的是natvie方法,這個區域則為空(undefined)。此記憶體區域是唯一一個在vm spec中沒有規定任何outofmemoryerror情況的區域。

2.java虛拟機棧(java virtual machine stacks)

與程式計數器一樣,vm棧的生命周期也是與線程相同。vm棧描述的是java方法調用的記憶體模型:每個方法被執行的時候,都會同時建立一個幀(frame)用于存儲本地變量表、操作棧、動态連結、方法出入口等資訊。每一個方法的調用至完成,就意味着一個幀在vm棧中的入棧至出棧的過程。在後文中,我們将着重讨論vm棧中本地變量表部分。 經常有人把java記憶體簡單的區分為堆記憶體(heap)和棧記憶體(stack),實際中的區域遠比這種觀點複雜,這樣劃分隻是說明與變量定義密切相關的記憶體區域是這兩塊。其中所指的“堆”後面會專門描述,而所指的“棧”就是vm棧中各個幀的本地變量表部分。本地變量表存放了編譯期可知的各種标量類型(boolean、byte、char、short、int、float、long、double)、對象引用(不是對象本身,僅僅是一個引用指針)、方法傳回位址等。其中long和double會占用2個本地變量空間(32bit),其餘占用1個。本地變量表在進入方法時進行配置設定,當進入一個方法時,這個方法需要在幀中配置設定多大的本地變量是一件完全确定的事情,在方法運作期間不改變本地變量表的大小。 在vm spec中對這個區域規定了2中異常狀況:如果線程請求的棧深度大于虛拟機所允許的深度,将抛出stackoverflowerror異常;如果vm棧可以動态擴充(vm spec中允許固定長度的vm棧),當擴充時無法申請到足夠記憶體則抛出outofmemoryerror異常。

3.本地方法棧(native method stacks)

本地方法棧與vm棧所發揮作用是類似的,隻不過vm棧為虛拟機運作vm原語服務,而本地方法棧是為虛拟機使用到的native方法服務。它的實作的語言、方式與結構并沒有強制規定,甚至有的虛拟機(譬如sun hotspot虛拟機)直接就把本地方法棧和vm棧合二為一。和vm棧一樣,這個區域也會抛出stackoverflowerror和outofmemoryerror異常。

4.java堆(java heap)

對于絕大多數應用來說,java堆是虛拟機管理最大的一塊記憶體。java堆是被所有線程共享的,在虛拟機啟動時建立。java堆的唯一目的就是存放對象執行個體,絕大部分的對象執行個體都在這裡配置設定。這一點在vm spec中的描述是:所有的執行個體以及數組都在堆上配置設定(原文:the heap is the runtime data area from which memory for all class instances and arrays is allocated),但是在逃逸分析和标量替換優化技術出現後,vm spec的描述就顯得并不那麼準确了。 java堆内還有更細緻的劃分:新生代、老年代,再細緻一點的:eden、from survivor、to survivor,甚至更細粒度的本地線程配置設定緩沖(tlab)等,無論對java堆如何劃分,目的都是為了更好的回收記憶體,或者更快的配置設定記憶體,在本章中我們僅僅針對記憶體區域的作用進行讨論,java堆中的上述各個區域的細節,可參見本文第二章《jvm記憶體管理:深入垃圾收集器與記憶體配置設定政策》。 根據vm spec的要求,java堆可以處于實體上不連續的記憶體空間,它邏輯上是連續的即可,就像我們的磁盤空間一樣。實作時可以選擇實作成固定大小的,也可以是可擴充的,不過目前所有商業的虛拟機都是按照可擴充來實作的(通過-xmx和-xms控制)。如果在堆中無法配置設定記憶體,并且堆也無法再擴充時,将會抛出outofmemoryerror異常。

5.方法區(method area)

叫“方法區”可能認識它的人還不太多,如果叫永久代(permanent generation)它的粉絲也許就多了。它還有個别名叫做non-heap(非堆),但是vm spec上則描述方法區為堆的一個邏輯部分(原文:the method area is logically part of the heap),這個名字的問題還真容易令人産生誤解,我們在這裡就不糾結了。 方法區中存放了每個class的結構資訊,包括常量池、字段描述、方法描述等等。vm space描述中對這個區域的限制非常寬松,除了和java堆一樣不需要連續的記憶體,也可以選擇固定大小或者可擴充外,甚至可以選擇不實作垃圾收集。相對來說,垃圾收集行為在這個區域是相對比較少發生的,但并不是某些描述那樣永久代不會發生gc(至少對目前主流的商業jvm實作來說是如此),這裡的gc主要是對常量池的回收和對類的解除安裝,雖然回收的“成績”一般也比較差強人意,尤其是類解除安裝,條件相當苛刻。

6.運作時常量池(runtime constant pool)

class檔案中除了有類的版本、字段、方法、接口等描述等資訊外,還有一項資訊是常量表(constant_pool table),用于存放編譯期已可知的常量,這部分内容将在類加載後進入方法區(永久代)存放。但是java語言并不要求常量一定隻有編譯期預置入class的常量表的内容才能進入方法區常量池,運作期間也可将新内容放入常量池(最典型的string.intern()方法)。 運作時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法在申請到記憶體時會抛出outofmemoryerror異常。

7.本機直接記憶體(direct memory)

直接記憶體并不是虛拟機運作時資料區的一部分,它根本就是本機記憶體而不是vm直接管理的區域。但是這部分記憶體也會導緻outofmemoryerror異常出現,是以我們放到這裡一起描述。 在jdk1.4中新加入了nio類,引入一種基于管道與緩沖區的i/o方式,它可以通過本機native函數庫直接配置設定本機記憶體,然後通過一個存儲在java堆裡面的directbytebuffer對象作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在java對和本機堆中來回複制資料。 顯然本機直接記憶體的配置設定不會受到java堆大小的限制,但是即然是記憶體那肯定還是要受到本機實體記憶體(包括swap區或者windows虛拟記憶體)的限制的,一般伺服器管理者配置jvm參數時,會根據實際記憶體設定-xmx等參數資訊,但經常忽略掉直接記憶體,使得各個記憶體區域總和大于實體記憶體限制(包括實體的和作業系統級的限制),而導緻動态擴充時出現outofmemoryerror異常。