目錄
一:JVM引言
1. 什么是 JVM ?
2. 常見的 JVM
3. 學(xué)習(xí)路線
二:JVM內(nèi)存結(jié)構(gòu)
1. 程 序 計 數(shù) 器(PC Register)
2. 虛 擬 機 棧(JVM Stacks)
3. 本 地 方 法 棧(Native Method Stacks)
4. 堆(Heap)
5. 方 法 區(qū)(Method Area)
三:直接內(nèi)存
tips:首先給大家推薦兩款好用的免費軟件:動圖抓取軟件:ScreenToGif和錄屏工具:oCam,可用來作為日常的制作動圖Gif和錄屏,網(wǎng)盤鏈接:夸克網(wǎng)盤分享
一:JVM引言
1. 什么是 JVM ?
定義:Java Virtual Machine - java 程序的運行環(huán)境(java 二進制字節(jié)碼的運行環(huán)境)
好處:
①一次編寫,到處運行;?
②自動內(nèi)存管理,具有垃圾回收功能;
③數(shù)組下標(biāo)越界檢查,拋出異常;
④多態(tài),面向?qū)ο蟮幕?/p>
比較:JVM、 JRE、 JDK
從圖中我們也可以看出是逐級向上的、包含的關(guān)系!
2. 常見的 JVM
最常用的就是:HotSpot,Oracle JDK edition、Eclipse OPenJ9;接下來的講解都是基于HotSpot!
3. 學(xué)習(xí)路線
主要分為三大塊:類加載器ClassLoader、JVM內(nèi)存結(jié)構(gòu)、執(zhí)行引擎。
學(xué)習(xí)順序:先學(xué)習(xí)JVM內(nèi)存結(jié)構(gòu)、然后學(xué)習(xí)GC垃圾回收機制、再學(xué)習(xí)JavaClass字節(jié)碼、然后學(xué)習(xí)類加載器ClassLoader、最后學(xué)習(xí)執(zhí)行引擎的其它內(nèi)容。
二:JVM內(nèi)存結(jié)構(gòu)
1. 程 序 計 數(shù) 器(PC Register)
(1)定義
ProgramCounterRegister 程序計數(shù)器 ( 寄 存 器 )
特 點: 是線程私有的、 不會存在內(nèi)存溢 出!
(2)作用
執(zhí)行過程:Java源代碼---》經(jīng)過編譯生成二進制字節(jié)碼(一些JVM指令)---》經(jīng)過解釋器---》解釋成機器碼---》最后交給CPU來執(zhí)行!
程序計數(shù)器的作用:在程序執(zhí)行的過程中記住下一條JVM指令的執(zhí)行地址(前面的數(shù)字就可以理解為執(zhí)行地址)。例如:拿到第一條getstatic指令交給解釋器、解釋器變成機器碼、機器碼交給CPU;于此同時會把下一條指令(astore_1)的地址(3),放入程序計數(shù)器,等到第一條指令執(zhí)行結(jié)束,解釋器就會程序計數(shù)器中取下一條指令(astore_1)的地址(3),依次重復(fù)!
思考:如果沒有程序計數(shù)器會有什么問題?
就會造成接下來不知道執(zhí)行哪一條指令!實際上程序計數(shù)器在物理上是通過寄存器實現(xiàn)的!
2. 虛 擬 機 棧(JVM Stacks)
棧:是一種數(shù)據(jù)結(jié)構(gòu),先進后出或者說后進先出;一個線程一個棧!
(1)定義
Java Virtual Machine Stacks (Java 虛擬機棧)
①每個線程運行時所需要的內(nèi)存,稱為虛擬機棧;
②每個棧由多個棧幀(Frame)組成,對應(yīng)著每次方法調(diào)用時所占用的內(nèi)存;
③每個線程只能有一個活動棧幀,對應(yīng)著當(dāng)前正在執(zhí)行的那個方法。
總結(jié):
棧---》對應(yīng)著線程運行所需要的內(nèi)存空間。
棧幀---》對應(yīng)著每個方法運行時所需要的內(nèi)存空間。
我們可以通過下面一段代碼理解棧和棧幀,通過Debug模式
問題分析:
(1)垃圾回收是否涉及棧內(nèi)存?
答:不涉及,棧幀內(nèi)存每次方法結(jié)束后,都會彈出棧,自動釋放回收;我們知道垃圾回收機制只能回收堆內(nèi)存的無用對象。
(2)棧內(nèi)存分配越大越好嗎?
答:不是,棧內(nèi)存越大,會導(dǎo)致線程數(shù)變少,因為物理內(nèi)存的大小是一定的。例如:一個線程分配1M,物理內(nèi)存總共500M,理論上只能分配500個線程;若一個線程分配2M,理論上只能分配250個線程!
注:棧內(nèi)存劃分大了,只是能夠進行更多次的方法調(diào)用,并不會使運行效率提高!
注:在運行時,可以使用 -Xss size來指定分配的棧內(nèi)存大小;默認情況下:Linux、macOS分配的是1024KB,對于Windows是根據(jù)虛擬內(nèi)存大小進行分配
(3)方法內(nèi)的局部變量是否線程安全?
例1:分析多個線程調(diào)用是否會使變量x的值混亂?
我們知道一個線程一個棧,對于不同的線程進行調(diào)用,都會產(chǎn)生新的棧幀,每個線程都有自己私有的變量x。
例2:分析多個線程調(diào)用方法,能否保證線程安全?
①m1方法,局部變量,沒有逃離方法的作用范圍,方法結(jié)束變量釋放,線程安全的;
②m2方法,方法中的變量(前提是引用數(shù)據(jù)類型),其它線程可以通過這個方法進行調(diào)用,非線程安全的;
③m3方法,把這個局部變量(前提是引用數(shù)據(jù)類型)通過return返回,其它線程可以接收,然后執(zhí)行其它操作;
總結(jié):
①如果方法內(nèi)局部變量沒有逃離方法的作用范圍,它是線程安全的;
②如果是局部變量是一個引用類型(基本數(shù)據(jù)類型肯定還是線程安全的),并逃離方法的作用范圍,需要考慮線程安全;
(2)棧內(nèi)存溢出
情況一:棧幀過多導(dǎo)致棧內(nèi)存溢出
棧的大小是固定的,假如我們不斷的調(diào)用,使得棧幀不斷的壓棧,最終就會導(dǎo)致棧內(nèi)存溢出;例如:遞歸調(diào)用,沒有結(jié)束條件,最終拋出StackOverflowError異常!
情況二:棧幀過大導(dǎo)致棧內(nèi)存溢出
棧幀過大,一下子就超過了棧的大小;很少見!
(3)線程運行診斷
案例1: cpu 占用過多(有可能是死循環(huán))
定位 :在Linux環(huán)境下,運行Java代碼,nohub java 類 &
nohub:意思是不掛斷 。使用Xshell 等Linux 客戶端工具,遠程執(zhí)行 Linux 腳本時,有時候會由于網(wǎng)絡(luò)問題,導(dǎo)致客戶端失去連接,終端斷開,腳本運行一半就意外結(jié)束了。這種時候,就可以用nohup 指令來運行指令,即使客戶端與服務(wù)端斷開,服務(wù)端的腳本仍可繼續(xù)運行。
&:表示在后臺進行運行。
先使用top命令可以定位哪個進程對cpu的占用過高
ps H?-eo pid,tid,%cpu | grep 進程id (用ps命令進一步定位是哪個線程引起的cpu占用過高)
H:顯示樹狀結(jié)構(gòu),表示進程間的相互關(guān)系
-eo:規(guī)定輸出那些感興趣的內(nèi)容,例如:進程id(pid)、線程id(tid)、Cpu的占用情況(%cpu)
|:代表管道符,經(jīng)常與grep篩選命令一起使用
jstack 進程id:可以根據(jù)線程id 找到有問題的線程,進一步定位到問題代碼的源碼行號!
注意:上面顯示的32665線程號是十進制,而jstack顯示的十六進制對應(yīng)的就是7F99
案例2:程序運行很長時間沒有結(jié)果 (有可能是發(fā)生了線程的死鎖)
先執(zhí)行Java程序,nohub java 類 &,就會顯示進程id
?jstack 進程id:此時我們無法獲知線程id,看末尾執(zhí)行結(jié)果的提示
何時發(fā)生線程死鎖?
對于一個類,含有a、b屬性,對于t1線程先鎖a,在鎖b;對于t2線程先鎖b,在鎖a;這種情況程序就會僵持在哪里,也沒有拋出異常,這種情況下的排查錯誤是非常難的!
3. 本 地 方 法 棧(Native Method Stacks)
定義:JVM調(diào)用本地方法時,需要給這些本地方法提供的內(nèi)存空間!
解釋本地方法(Native Method):指那些不是由Java代碼編寫的方法,例如:利用C、C++編寫的本地方法來與操作系統(tǒng)打交道,Java代碼可以通過這些本地方法來調(diào)用到這些底層的功能;只寫本地方法使用的內(nèi)存就是本地方法棧!
例如:Object類的克隆方法
4. 堆(Heap)
前面學(xué)習(xí)的棧Stack是線程私有的、堆Heap是線程共享的!
堆(Heap):通過 new 關(guān)鍵字,創(chuàng)建對象都會使用堆內(nèi)存
特點:
①它是線程共享的,堆中對象都需要考慮線程安全的問題 ;
②有垃圾回收機制;
(1)堆 內(nèi) 存 溢 出
首先先看下面這段代碼:
首先創(chuàng)建一個ArrayList集合,寫一個死循環(huán),字符串不斷進行拼接,然后放到List集合當(dāng)中!
public class Demo1_5 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
執(zhí)行結(jié)果:內(nèi)存溢出,拋出OutOfMemoryError異常
可以使用 -Xmx size來指定分配的堆空間大小
(2)堆 內(nèi) 存 診 斷
①jps工具:查看當(dāng)前系統(tǒng)中有哪些java進程
②jmap工具:查看堆內(nèi)存占用情況,查看的是某一個時刻;?jmap -heap 進程id
③jconsole工具:圖形界面的,多功能的監(jiān)測工具,可以連續(xù)監(jiān)測
案例1:
首先先創(chuàng)建一個byte數(shù)組,會在堆內(nèi)存中開辟10M的空間;然后把數(shù)組的引用arr置為null,開啟垃圾回收機制進行回收;中間的sleep睡眠是為了方便執(zhí)行指令進行監(jiān)控。
public class Demo1_4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
使用IDEA運行此程序,打開自帶的dos窗口,輸入命令
①先輸入jps命令,查看有哪些Java進程
②使用jmap進行檢測
第一步:在控制臺打印輸出1,也就是未創(chuàng)建10M內(nèi)存空間時,使用jmap -heap 18756進行檢測
第二步:在控制臺上打印出2,使用jmap -heap 18756進行檢測(此時創(chuàng)建10M的空間)
第三步:?在控制臺上打印出3,使用jmap -heap 18756再次進行檢測(此時引用置為null),并啟用了垃圾回收機制進行回收
?③使用jconsole進行檢測(圖形化界面進行顯示)
步驟:直接輸入jconsole--->顯示圖形化界面,找到要檢測的類---》選擇不安全連接;就會動態(tài)顯示每一個時刻的檢測效果!
案例2:調(diào)用垃圾回收后,內(nèi)存占用仍然很高
這里先把這段代碼給出來,假如我們不知道代碼的具體實現(xiàn),怎么去一步排查
import java.util.ArrayList;
import java.util.List;
public class ClazzMapperTest {
public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024*1024];
}
第一步:使用jps查看進程的id
第二步:使用jmap -head 進程id查看內(nèi)存使用情況;分為兩部分:
Eden區(qū):?
Old Generation區(qū):?
第三步:使用jconsole工具,執(zhí)行垃圾回收機制GC;發(fā)現(xiàn)相對于最初的狀態(tài)確實回收了一部分內(nèi)存,但是還有200多M沒有被回收!
第四步:實際上200多M,Eden區(qū)確實被回收了不少,但是Old Generation區(qū)卻沒有被回收;使用更加好用的檢測工具jvisualvm(JDK9以后就沒有了,需要下載插件)進行檢測
①找到堆 Dump表示抓取當(dāng)前堆的快照
②?查找前20個占用堆內(nèi)存最大的對象
③可以找到占用堆內(nèi)存最大的對象是一個ArrayList對象
④點擊去ArrayList,查看它的屬性都是Student對象?;總共有244個項,其中Student項有200個,其它的都是Object對象(已經(jīng)被釋放掉了);一個Student對象占用1M左右,200個就是占用200多兆,這樣就能排查出來。
⑤再結(jié)合源碼分析,在主方法main執(zhí)行結(jié)束之前(調(diào)用了sleep方法睡眠),ArrrayList集合中存儲了大量的Student對象,無法釋放;最終使得垃圾回收后,內(nèi)存占用仍然很高!
5. 方 法 區(qū)(Method Area)
(1)定義
(1)方法區(qū)域類似于用于傳統(tǒng)語言的編譯代碼的存儲區(qū)域,或者類似于操作系統(tǒng)進程中的“文本”段。它存儲每個類的結(jié)構(gòu),例如運行時常量池、字段和方法數(shù)據(jù),以及方法和構(gòu)造函數(shù)的代碼,包括類和實例初始化以及接口初始化中使用的特殊方法。方法區(qū)域是在虛擬機啟動時創(chuàng)建的、方法區(qū)域在邏輯上是堆的一部分、方法區(qū)域中的內(nèi)存無法滿足分配請求,Java虛擬機將拋出OutOfMemoryError。
(2)特點:
①方法區(qū)是線程共享的,如果多個線程用到同一個類的時候,若這個類還未被加載,此時只能有一個線程去加載類,其他線程需要等待;
②方法區(qū)的大小可以是非固定的,jvm可以根據(jù)應(yīng)用需要動態(tài)調(diào)整,jvm也支持用戶和程序指定方法區(qū)的初始大??;
③方法區(qū)有垃圾回收機制,一些類不再被使用則變?yōu)槔?,需要進行垃圾清理。
(2)組成
JVM1.6版本內(nèi)存結(jié)構(gòu):
使用一個PermGen永久代作為方法區(qū)的實現(xiàn),這個永久代包括以下信息:Class類的信息、ClassLoader類加載器信息、StringTable(字符串表)運行時常量池;
JVM1.8版本內(nèi)存結(jié)構(gòu):
使用一個Metaspace元空間作為方法區(qū)的實現(xiàn),存儲以下信息:Class類的信息、ClassLoader類加載器信息、常量池(和上面不同的地方);已經(jīng)不占用堆內(nèi)存了,換句話說不是由JVM來管理它的內(nèi)存結(jié)構(gòu)了;移到本地內(nèi)存(操作系統(tǒng)內(nèi)存)
?(3)方法區(qū)內(nèi)存溢出
①JDK1.8以前會導(dǎo)致永久代內(nèi)存溢出
我們沒有設(shè)置內(nèi)存的上限,它會把10000個類全都加載到內(nèi)存當(dāng)中,可以使用參數(shù)進行設(shè)置,指定源空間內(nèi)存的大?。?span style="color:#0d0016;">-XX:MaxPermSize=8m
package cn.itcast.jvm.t1.metaspace;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
// 把下面10000個類加載到內(nèi)存當(dāng)中
public class Demo1_8 extends ClassLoader { // 可以用來加載類的二進制字節(jié)碼
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 20000; i++, j++) {
// ClassWriter 作用是生成類的二進制字節(jié)碼
ClassWriter cw = new ClassWriter(0);
// 版本號, public, 類名, 包名, 父類, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 執(zhí)行了類的加載
test.defineClass("Class" + i, code, 0, code.length); // Class 對象
}
} finally {
System.out.println(j);
}
}
}
加上-XX:MaxPermSize=8m 只循環(huán)了19314次,就拋出了永久代溢出異常
②JDK1.8之后會導(dǎo)致元空間內(nèi)存溢出
相同的代碼,使用參數(shù)進行設(shè)置,指定源空間內(nèi)存的大?。?span style="color:#0d0016;">-XX:MaxMetaspaceSize=8m
加上-XX:MaxMetaspaceSize=8m 只循環(huán)了5411次,就拋出了元空間內(nèi)存溢出
(4)運行時常量池
①先理解常量池
對于二進制字節(jié)碼,包括:類基本信息,常量池,類方法定義,包含了虛擬機指令;先看以下代碼,編譯生成HelloWorld.class文件,使用:javap -v HelloWorld.class進行反編譯
package cn.itcast.jvm.t5;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
常量池,就是一張表,虛擬機指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量等信息,例如:??
②運行時常量池
常量池是 *.class 文件中的;當(dāng)該類被加載,它的常量池信息就會放入運行時常量池,并把里面的符號地址變?yōu)檎鎸嵉刂?。
(5)StringTable
StringTable特性:
①常量池中的字符串僅是符號,第一次用到時才變?yōu)閷ο?/span>;
② 利用串池的機制,來避免重復(fù)創(chuàng)建字符串對象;
③ 字符串變量拼接的原理是 StringBuilder (1.8);
④ 字符串常量拼接的原理是編譯期優(yōu)化 ;
⑤可以使用 intern方法,主動將串池中還沒有的字符串對象放入串池;
對于1.8 :將這個字符串對象嘗試放入串池,如果有則并不會放入;如果沒有則放入串池, 會把串池中的對象返回。
對于1.6: 將這個字符串對象嘗試放入串池,如果有則并不會放入;如果沒有會把此對象復(fù)制一份, 放入串池, 會把串池中的對象返回。
驗證上面的特性:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
進行反編譯:
#2就對應(yīng)著String a,#3就對應(yīng)著String b,#4就對應(yīng)著String ab
、
?astore_1就把加載好的字符串對象存入1號局部變量s1,其它依次類推
常量池是存在字節(jié)碼文件.class里,當(dāng)運行的時候會放到運行時常量池當(dāng)中;但是加載到運行時常量池當(dāng)中時,還沒有成為java字符串對象,直到具體執(zhí)行到引用它的那一行代碼;例如:執(zhí)行到String s1 = "a",會把ldc #2 會把a符號變?yōu)椤癮”字符串對象;此時還會準(zhǔn)備一塊空間StringTable,把“a”字符串對象放進去(如果里面沒有的話),這實際上是一個延遲加載(懶惰的)行為;如果串池中有的話,就會直接使用,總而言之,只會存在一份!
所以:會把s1、s2、s3引用指向的“a”、“b”、“ab”放到字符串常量池StringTable當(dāng)中,StringTable [ "a", "b" ,"ab" ] 底層是hashtable結(jié)構(gòu),不能擴容。
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
s4=s1+s2,變量拼接,s4引用首先會會創(chuàng)建一個StringBuilder對象,然后調(diào)用append方法,把“a”和“b”拼接進去,然后調(diào)用toString方法;我們通過查看StringBuilder的toString方法底層原碼發(fā)現(xiàn)是創(chuàng)建一個新的字符串對象:new String("ab")。
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
// 問
System.out.println(s3 == s4);
s3 == s4的結(jié)果?
s3對應(yīng)的"ab"是在字符串常量池中的對象,但s4是一個新創(chuàng)建的字符串對象,雖然值是相同的,但是s3是在串池當(dāng)中的,s4是先創(chuàng)建出來的,== 對比的就是地址,肯定是不一樣的,結(jié)果是false。
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // 變量拼接
String s5 = "a" + "b"; // 常量拼接
// 問
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
s5 = "a" + "b" 直接找的就是已經(jīng)拼接好的“ab”對象;這是javac在編譯器的優(yōu)化,在編譯期間我們就能確定肯定就是“ab”對象,此時常量池中已經(jīng)存在這個對象,所以 s3 == s5的結(jié)果就是true。
注:s4=s1+s2是在運行期間才能確定,去動態(tài)拼接!
JDK1.8:會將這個字符串對象嘗試放入串池,如果有則并不會放入,如果沒有則放入串池, 會把串池中的對象返回!
String s = new String("a") + new String("b");
// 使用JDK1.8,會將這個字符串對象嘗試放入串池,
// 如果有則不會放入,如果沒有則放入串池,并把串池中的對象返回
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // true
s = new String("a") + new String("b"); 首先會把“a”和“b”放到常量池當(dāng)中,但是s="ab"不會放進去,因為是變量拼接,會創(chuàng)建一個字符串對象,是存放在堆當(dāng)中,要想把"ab"放到常量池當(dāng)中,可以調(diào)用intern方法,這樣就可以把"ab"放入字符串常量池當(dāng)中。此時就把s的對象放入常量池當(dāng)中,并且s2是串池中對象的返回值;所以兩個都為true。
String s = new String("a") + new String("b");
String x = "ab";
String s2 = s.intern();
System.out.println(s2 == x); // true
System.out.println(s == x); // false
此時String x = "ab',會把“ab”放到串池當(dāng)中;此時串池當(dāng)中已經(jīng)存在“ab”對象,此時s.intern(),就不會把“ab”對象放入串池當(dāng)中;而s2 = s.intern返回的一定是一個串池當(dāng)中的對象;所以此時s2 == x是true,s == x是false。
JDK1.6:將這個字符串對象嘗試放入串池,如果有則并不會放入,如果沒有會把此對象復(fù)制一份, 放入串池, 會把串池中的對象返回!
String s = new String("a") + new String("b");
// 使用JDK1.6,會將這個字符串對象嘗試放入串池,
// 如果有則并不會放入,如果沒有會把此對象復(fù)制一份,放入串池, 會把串池中的對象返回
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // false
使用JDK1.6最主要的區(qū)別就是s.intern,此時是拷貝一份放入串池當(dāng)中,而不是把s本身的對象放入串池,s還是堆中的對象,此時s == "ab"就是false。
經(jīng)典面試題:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 問
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
String x1 = "cd"; // "cd"
x2.intern();
System.out.println(x1 == x2); // false
// 問,如果調(diào)換了x1,x2的位置呢?如果是jdk1.6呢?
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern(); //先把“ab”入常量池
String x1 = "cd";
System.out.println(x1 == x2);
// 此時對于JDK1.8-true,對于JDK1.6-false
(6)StringTable的位置
對于JDK1.6
對于JDK1.8
?那么能通過代碼直觀上體現(xiàn)出StringTable的位置嗎?
import java.util.ArrayList;
import java.util.List;
public class Demo1_6 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
對于JDK1.6把永久代的內(nèi)存設(shè)置小一點:-XX:MaxPermSize=10m
對于JDK1.8把堆的內(nèi)存設(shè)置小一點:-Xmx10m,此時并沒有提示堆內(nèi)存不足錯誤;下面的提示表示使用98%的精力去回收,但是值回收了2%,就會報這個錯誤提示。
此時需要在加上一個參數(shù),關(guān)閉這個提示?-Xmx10m -XX:-UseGCOverheadLimit
(7)StringTable的垃圾回收機制
StringTable也是受到垃圾回收機制的管理的,當(dāng)內(nèi)存空間不足時,StringTable中那些還沒有被引用的字符串常量就會被垃圾回收器回收!
設(shè)置參數(shù):-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
-Xmx10m:設(shè)置虛擬機堆內(nèi)存的最大值;
-XX:+PrintStringTableStatistics:打印字符串表的統(tǒng)計信息;
-XX:+PrintGCDetails -verbose:gc:打印垃圾回收的一些信息(若發(fā)生了垃圾回收的話);
package cn.itcast.jvm.t1.stringtable;
import java.util.ArrayList;
import java.util.List;
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern(); // 入池
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
①先不添加循環(huán)代碼,此時查看StringTable的存儲情況
②若循環(huán)100次,此時還沒有超過堆內(nèi)存的大小,不會觸發(fā)垃圾回收機制
③若循環(huán)10000次,此時已經(jīng)超過堆內(nèi)存的大小,會觸發(fā)垃圾回收機制進行回收
?啟動了垃圾回收機制的打印信息:
(8)StringTable性能調(diào)優(yōu)
方法1:調(diào)整 -XX:StringTableSize = 桶個數(shù)
StringTable的底層是一個哈希表(數(shù)組+鏈表),哈希表的性能是和它的大小密切相關(guān)的:如果哈希表桶的個數(shù)比較多,元素就會比較分散,哈希碰撞的幾率就會減少,查找的速率也會變快。如果桶的個數(shù)較少,哈希碰撞的幾率就會增高,導(dǎo)致鏈表比較長,查找的速度就會受到影響!
package cn.itcast.jvm.t1.stringtable;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 演示串池大小對性能的影響
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
*/
public class Demo1_24 {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern(); // 入串池
}
System.out.println("cost:" + (System.nanoTime() - start) / 1000000); // 毫秒
}
}
}
所謂的調(diào)優(yōu),最主要的就是調(diào)整桶的個數(shù): -XX:StringTableSize = 桶?數(shù),不設(shè)置虛擬機的內(nèi)存的最大值,對于四萬多個數(shù)據(jù)可以輕松入池!
①-XX:StringTableSize = 200000,把桶的個數(shù)調(diào)整為200000
②不加-XX:StringTableSize這個參數(shù),使用的默認桶大小60013
結(jié)論:桶的個數(shù)越小,耗費的時間最多;并且最小的桶個數(shù)是1009!
方法二:考慮將字符串對象是否入池
假設(shè)現(xiàn)在有大量的字符串對象被創(chuàng)建,例如:linux.words文件中有4.8萬個串,循壞10次,對比入池與不如池的內(nèi)存使用情況。
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class Demo1_25 {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if(line == null) {
break;
}
// 不入池
address.add(line);
// 入池
address.add(line.intern());
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
①address.add(line)不入池,此時相當(dāng)于有48萬個數(shù)據(jù)被添加到List集合當(dāng)中。
使用jvisualvm工具,選擇抽樣器:
對內(nèi)存的占用進行圖形化的展示:
讀取之前,此時字符串占用的內(nèi)存大概是1M左右:
?讀取之后,此時字符串占用的內(nèi)存大概是110M左右:
②address.add(line.intern())入池,入池以后,后面循環(huán)9次的數(shù)據(jù)都是重復(fù)的,都是直接使用只有第一次入池的數(shù)據(jù)即可,此時讀取之后,String+創(chuàng)建的char數(shù)組也才40M不到,大大節(jié)省了堆內(nèi)存空間!
三:直接內(nèi)存
(1)定義
直接內(nèi)存不屬于Java虛擬機里面的內(nèi)存,是操作系統(tǒng)的內(nèi)存!
直接內(nèi)存:DirectMemory
①常見于NIO操作時 , 用于數(shù)據(jù)緩沖區(qū);?
②分配回收成本較高 , 但讀寫性能高;?
③不受JVM內(nèi)存回收管理;
?案例:使用傳統(tǒng)的IO流和直接內(nèi)存進行比較
package cn.itcast.jvm.t1.direct;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM = "E:\\編程資料\\text.txt";
static final String TO = "E:\\a.txt";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用時:3秒左右
directBuffer(); // directBuffer 用時:1秒左右
}
// 使用直接內(nèi)存的方式
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用時:" + (end - start) / 1000_000.0);
}
// 傳統(tǒng)的IO流
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用時:" + (end - start) / 1000_000.0);
}
}
我們會發(fā)現(xiàn),使用直接內(nèi)存ByteBuffer比傳統(tǒng)的IO流拷貝文件(特別是大文件)的速度明顯快很多,就從文件的讀寫過程進行分析!
對于傳統(tǒng)的IO流:
Java本身并不具備讀寫磁盤的能力,必須調(diào)用操作系統(tǒng)提供的函數(shù);就是從Java的方法調(diào)用到本地的方法;此時CPU會從用戶態(tài)切換到內(nèi)核態(tài);此時就可以讀取磁盤文件的內(nèi)容,此時會在操作系統(tǒng)中劃出來一層緩沖區(qū)(系統(tǒng)緩沖區(qū)),磁盤的內(nèi)容就會先讀取到這個系統(tǒng)緩沖區(qū)(分次讀取,并且Java的代碼是不能讀取系統(tǒng)緩沖區(qū)的內(nèi)容的),此時Java也會在堆內(nèi)存中分配一塊Java的緩沖區(qū);Java要想讀取到數(shù)據(jù),必須先從系統(tǒng)緩存數(shù)據(jù)讀入到Java緩沖區(qū);兩塊緩沖區(qū),相當(dāng)于讀取的時候必須存兩份(造成不必要的數(shù)據(jù)復(fù)制),效率較低!
對于直接內(nèi)存:
當(dāng)ByteBuffer調(diào)用allocateDirect方法時,會在操作系統(tǒng)間劃出一塊緩沖區(qū)(direct memory),這塊區(qū)域Java代碼是可以直接訪問的;這塊內(nèi)存無論是操作系統(tǒng)還是Java代碼都是可以直接訪問的,共享的一塊內(nèi)存區(qū)域;只有一次緩沖區(qū)的讀入,效率較高!
不受JVM內(nèi)存回收管理,所以直接內(nèi)存也會導(dǎo)致內(nèi)存溢出,例:
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
// 演示直接內(nèi)存溢出
public class Demo1_10 {
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法區(qū)是jvm規(guī)范, jdk6 中對方法區(qū)的實現(xiàn)稱為永久代
// jdk8 對方法區(qū)的實現(xiàn)稱為元空間
}
}
直接內(nèi)存使用操作不當(dāng),也會導(dǎo)致內(nèi)存溢出:
(2)分配和回收原理
例:
public class Test {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb); // 分配1G的空間
System.out.println("分配完畢...");
System.in.read();
System.out.println("開始釋放...");
byteBuffer = null; // 空引用
System.gc(); // 啟動垃圾回收
System.in.read();
}
}
查看任務(wù)管理器,分配1G:
查看任務(wù)管理器,置為null,啟動垃圾回收機制,發(fā)現(xiàn)竟然被回收了!前面不是說直接內(nèi)存不受JVM內(nèi)存回收管理嗎?為什么垃圾回收之后,直接內(nèi)存就被回收釋放了?
這就需要解釋一下直接內(nèi)存的釋放原理:首先通過某種方式獲取unsafe對象,通過unsafe對象就可以完成直接內(nèi)存的分配和回收!
注:對于直接內(nèi)存的監(jiān)控,就不能使用IDEA中的那些監(jiān)控工具,需要看任務(wù)管理器中的進程
分配內(nèi)存:
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
釋放內(nèi)存:
unsafe.freeMemory(base);
ByteBuffer.allocateDirect方法底層是使用了ByteBu?er的實現(xiàn)類DirectByteBuffer
DirectByteBuffer的構(gòu)造器中就調(diào)用了unsafe的allocateMemory方法對直接內(nèi)存進行分配
DirectByteBuffer的構(gòu)造器中內(nèi)部還使用了 Cleaner (虛引用)來監(jiān)測 ByteBu?er 對象,一旦 ByteBu?er對象被垃圾回收,那么就會由 ReferenceHandler 線程通過Cleaner的clean方法去執(zhí)行任務(wù)對象Deallocator,任務(wù)對象在調(diào)用unsafe對象的freeMemory 來釋放直接內(nèi)存
文章來源:http://www.zghlxwxcb.cn/news/detail-451808.html
-XX:+DisableExplicitGC:禁用顯式回收對直接內(nèi)存的影響,就是讓System.gc()無效,但是此時就會影響直接內(nèi)存的釋放,我們就可以使用unsafe對象手動釋放直接內(nèi)存!文章來源地址http://www.zghlxwxcb.cn/news/detail-451808.html
到了這里,關(guān)于Java虛擬機快速入門 | JVM引言、JVM內(nèi)存結(jié)構(gòu)、直接內(nèi)存的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!