引言
什么是JVM
定義:
Java VirtualMachine -java 程序的運行環境 (ava 二進制字節碼的運行環境)
好處:
- 一次編寫,到處運行
- 自動內存管理,垃圾回收功能
- 數組下標越界檢查,
- 多態
比較:
jvm jre jdk
學習jvm的作用
- 面試
- 理解底層實現原理
- 中高級程序員的必備技能
常見的jvm
自己百度查找
jvm的組成
?內存結構
程序計數器
定義
Program Counter Register 程序計數器(寄存器)
作用
如下圖所示
右邊就是簡單的java代碼打印操作,編譯成左側的二進制字節碼。
經過解釋器——>機器碼——>CPU執行。
程序計數器在這里面的作用就是記住下一條jvm指令的執行地址。
第一條指令地址是0,第一條指令交給解釋器去執行的同時會把第二條指令的地址3放入程序計數器。第一條執行完之后,解釋器會去取出3來執行......
物理實現: 通過CPU中寄存器(速度快)實現
?特點:
線程私有
每個線程都有自己的程序計數器。
每一個線程會有被分配一個時間片,在當前時間片內不能執行完會去執行別的線程的代碼,直到輪到下一個時間片。
切換到別的線程時要記住當前執行到哪里,還是要用到程序計數器。通過私有的程序計數器知道下一行代碼的地址。
?唯一不會存在內存溢出的區
虛擬機棧
棧是一種普通的先進后出的數據結構。
java的虛擬機棧則是線程運行需要的內存空間。
一段代碼有多個方法組成,一個棧幀表示一次方法的調用,棧幀就是每個方法運行需要的內存。
運行:調用第一個方法時會給第一個方法劃分一個棧幀空間,并壓入棧內,執行完后會出棧,也會釋放該方法占用的內存。
然后方法1調用方法2時會產生一個方法2的棧幀并入棧,然后方法2調用方法3也會產生并入棧,如下圖所示。
?定義
Java Virtual Machine Stacks (Java 虛擬機棧)
- 每個線程運行時所需要的內存,稱為虛擬機棧
- 每個棧由多個棧幀 (Frame) 組成,對應著每次方法調用時所占用的內存
- 每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法?
棧幀大小由方法里的參數以及局部變量的個數決定?
問題辨析
1.垃圾回收是否涉及棧內存?
? ?棧內存是一次次方法調用產生的棧幀內存,調用結束后會彈出棧,會自動回收,不需要垃圾回收? ? ?管理,垃圾回收是回收堆內存中的無用對象。
2.棧內存分配越大越好嗎?
運行java代碼時是可以指定棧內存大小的,使用-Xss size,下圖還有不同系統下默認棧內存的大小和設定內存的示例。
棧內存越大會讓線程數變少,512mb的物理內存下,每個線程的棧內存設置1mb大小可以運行512個,設置2mb大小可以運行256個線程。不會提高線程效率,但可以增加遞歸的層數。
????????
3.方法內的局部變量是否線程安全?
? ? ? ? 根據該變量是每個線程共享還是線程私有判斷。下圖是一個方法,方法內有一個局部變量。
該方法被調用兩次時會有兩個不同的棧。每個線程都會有私有的局部變量。因此這里不會有線程干擾的問題。
?假如將x改為static int x=0;的話就會出現線程干擾,如果不加保護的話會有線程安全問題。
總結:共享需要考慮線程安全,私有就不需要考慮。
- 如果方法內的局部變量沒有逃離方法的作用范圍,則是線程安全。
- 如果是局部變量引用了對象,并逃離方法的作用方法,需要考慮線程安全(引用傳遞和值傳遞的問題)
棧內存溢出
- 棧幀過多導致棧內存溢出(棧幀過多爆棧)? :? 通常在的遞歸導致。
- 單片棧幀過大導致棧內存溢出(太大了,已經塞滿了)
?一般不會有單片過大,棧幀里都是方法參數和局部變量。可以通過設置棧內存大小達到
?在將對象轉換成json對象時也會有棧溢出,這種兩個類的循環問題會導致json解釋器出現問題。
?可以通過一個@JsonIgnore注解達到在json轉換對象時忽略變量的效果。?
?
線程運行診斷
案例1: cpu 占用過多
?linux環境下運行一段java代碼導致cpu占用過高,可以使用top命令定位到哪一個進程占用,但看不見是哪一個線程導致的。
在linux下使用ps H -eo pid,tid,%cpu 命令可以看見所有線程的pid(進程號),tid(線程號),%cpu(cpu占用)。
使用ps H -eo pid,tid,%cpu | grep 32655? ?后面加上| grep pid過濾無關進程的線程。
- 用top定位哪個進程對cpu的占用過高
- ps H -eo pid,tid,%cpu | grep pid (用ps命令進一步定位是哪個線程引起的cpu占用過高)
- jstack 進程id? (可以根據線程id 找到有問題的線程,進一步定位到問題代碼的源碼行號)
生產環境不推薦jstack,因為打印線程信息jvm會暫停其他線程?
然后將線程編號32665轉換成16進制(7F99)在輸出內容中查找?
?在jstack 輸出內容中可以看見一個nid=Ox7f99的線程,狀態為RUNNABLE.
看見問題出在第8行代碼。如下圖源碼第8行是個死循環。
?nid、pid 和 tid 是計算機系統中常用的三個標識
- nid (Node ID) 是指在分布式系統中,每個節點的唯一標識
- pid (Process ID) 是指操作系統中每個進程的唯一標識。
- tid (Thread ID) 是指操作系統中每個線程的唯一標識。
案例2: 程序運行很長時間沒有結果
線程死鎖導致的無結果下使用jstack命令查看,下輸出內容最后可以看見有關死鎖信息。
?兩個線程都想獲得a,b,但是都在等對方放開擁有的對象,然后陷入死鎖。
產生死鎖的四個必要條件:互斥、不可剝奪、請求和保持、循環等待。
本地方法棧
定義:? ? java虛擬機在調用本地方法時需要給本地方法提供的內存空間
在Object這個類中就有很多,比如Object的clone方法的聲明是native,這個native的實現是c/c++,java代碼是間接調用native
?
堆
定義
通過 new 關鍵字,創建對象都會使用堆內存
特點:
- 它是線程共享的,堆中對象都需要考慮線程安全的問題
- 有垃圾回收機制 (不再被引用的對象會被回收)?
堆內存溢出
下圖所示方法中String類型的對象a會一次次變大,直至堆溢出。
?運行結果:? 溢出內存錯誤: java 堆 空間
使用-Xmx size改變堆空間大小。
?
?修改前26次才溢出,修改后17次溢出。
有可能堆內存較大,運行時間短,在系統前期看不出問題,后期才會爆掉,故測試時可以將堆內存設置較小進行排查。
堆內存診斷
相關工具:
1.jps 工具
????????查看當前系統中有哪些 java 進程
2. jmap 工具
????????查看堆內存占用情況 jmap -heap 進程id?(只能看某一瞬間的情況)
3.jconsole 工具?
????????圖形界面的,多功能的監測工具,可以連續監測
4.jvisualVM 工具
? ? ? ? 圖形化界面,可以抓取當前快照?
案例1
?new一個10MB的數組對象,后面置為null,然后gc顯式回收。
運行后通過jps查看進程id,jmap -heap 18756在1~2,2~3,3之后三個時間點抓取快照信息。?
最大堆內存占用MaxHeapSize是4個G?
?
Eden Space就是專門為new 出來的對象準備的。?
?1~2之間
數組創建之前使用了6Mb
?2~3之間
創建數組對象之后使用16mb,
?3之后
垃圾回收之后變成1.2mb
使用jconsole工具的界面。
?案例2
垃圾回收之后,內存占用任然很高。
新生代被回收了,老年代沒有被回收。
?新生代剩8mb
?老年代剩200mb
使用新的工具jvisualvm可視化虛擬機
?
保存快照之后進行查找最大的類?
?查看最大的ArrayList實例的具體信息
?源代碼
兩百個Student對象,每個都開了一個1mb大小的byte數組。并且一直在作用范圍內,無法回收,內存占用居高不下。
?通過可視化界面的堆 dump按鈕進行排查。?
方法區
定義
按照jdk_jvm_1.8中的定義
- 方法區是所有java虛擬區線程共享的區域。
- 存儲了和類的結構相關的信息。
- 有成員變量filed,method data方法數據,成員方法、構造器方法的代碼以及運行時常量值run-time constant pool等等
- 在虛擬機啟動時被創建
- 邏輯上是堆的組成部分(1.8以前用的堆內存,1.8以后用的是系統內存)
- 方法區也會導致內存溢出
組成
永久代和元空間都是方法區這個概念的實現。
永久代和元空間最本質的區別就是 前者使用的是jvm內存 后者使用的是操作系統內存。
圖中常量池是運行時常量池。
?方法區內存溢出
- 1.8 以前會導致永久代內存溢出
- 1.8 之后會導致元空間內存溢出
下圖代碼就是一個加載了10000個類的代碼,最外層繼承實現了類加載器,在循環內指定版本號,類名,包名,父類,接口等信息創建一個新類。
這里元空間和永久代都沒有設置上限,這里需要設置元空間和永久代大小。
-XX:MaxMetaspaceSize=8m? 元空間
-XX:MaxPermSize=8m? 永久代
元空間運行報異常?
?永久代報異常
?場景:
- spring
- mybatis
spring和mybatis都使用到了cglib技術。
運行時常量池
下面的這段代碼的二進制字節碼含有如下信息。?
使用如下命令查看該代碼反編譯后的結果
javap -v HelloWorld.class
常量池部分
?虛擬機指令部分
執行指令時下面第一條就是獲取靜態變量,#2在常量池里面找。
ldc是找到一個引用地址。
?定義:
- 常量池,就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息
- 運行時常量池,常量池是*.class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,并把里面的符號地址變為真實地址
?運行時常量池里面#1,#2...這些會變成內存地址。