一、什么是JVM
定義
Java Virtual Machine刻剥,JAVA程序的運行環(huán)境(JAVA二進制字節(jié)碼的運行環(huán)境)
好處
- 一次編寫造虏,到處運行
- 自動內存管理,垃圾回收機制
- 數(shù)組下標越界檢查
- 多態(tài) (虛方法表機制實現(xiàn))
比較
JVM JRE JDK的區(qū)別
二、內存結構
整體架構
分成3大塊:類加載器揍诽、JVM內存結構暑脆、執(zhí)行引擎添吗。
- 從java源代碼編譯成java class份名,通過類加載器加載到JVM內存中去運行同窘;
- 類放在方法區(qū)想邦,創(chuàng)建的實例對象放在Heap(堆)中丧没,堆里的對象在調用方法時,又會用到虛擬機棧(Stack)漆际、程序計數(shù)器(PC)奸汇、本地方法棧(Native)擂找;
- 方法執(zhí)行時浩销,每行代碼是由執(zhí)行引擎中的解釋器逐行執(zhí)行慢洋,一些熱點代碼會由JIT即時編譯器編譯優(yōu)化,GC會對不再引用的對象進行回收礁遣,一些與操作系統(tǒng)打交道的就需要調用操作系統(tǒng)提供的本地方法接口祟霍。
1盈包、程序計數(shù)器
作用
用于保存JVM中下一條所要執(zhí)行的指令的地址
ps: 二進制字節(jié)碼(jvm指令)----》解釋器 來解釋成----》機器碼 ----》交給cpu來運行(cpu只認識機器碼)
特點
- 線程私有
-- CPU會為每個線程分配時間片呢燥,當當前線程的時間片使用完以后叛氨,CPU就會去執(zhí)行另一個線程中的代碼
-- 程序計數(shù)器是每個線程所私有的寞埠,當另一個線程的時間片用完仁连,又返回來執(zhí)行當前線程的代碼時,通過程序計數(shù)器可以知道應該執(zhí)行哪一句指令 - 不會內存溢出
2使鹅、虛擬機棧
定義
- 每個線程運行需要的內存空間患朱,稱為虛擬機棧
- 每個棧由多個棧幀(Frame)組成裁厅,對應著每次調用方法時所占用的內存
- 每個線程只能有一個活動棧幀姐直,對應著當前正在執(zhí)行的方法
演示
public class Main {
public static void main(String[] args) {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
在控制臺中可以看到,主類中的方法在進入虛擬機棧的時候姻成,符合棧的特點
問題辨析
- 垃圾回收是否涉及棧內存科展?
-- 不需要才睹。因為虛擬機棧中是由一個個棧幀組成的琅攘,在方法執(zhí)行完畢后坞琴,對應的棧幀就會被彈出棧。所以無需通過垃圾回收機制去回收內存寒亥。 - 棧內存的分配越大越好嗎溉奕?
-- 不是腐宋。因為物理內存是一定的胸竞,棧內存越大卫枝,可以支持更多的遞歸調用讹挎,但是可執(zhí)行的線程數(shù)就會越少筒溃。 - 方法內的局部變量是否是線程安全的怜奖?
-- 如果方法內局部變量沒有逃離方法的作用范圍,則是線程安全的
-- 如果如果局部變量引用了對象掷匠,并逃離了方法的作用范圍讹语,則需要考慮線程安全問題
棧內存溢出
Java.lang.stackOverflowError 棧內存溢出
使用 -Xss256k 指定棧內存大小顽决,配置在vm參數(shù)里才菠。
發(fā)生原因
- 虛擬機棧中鸠儿,棧幀過多
a.無限遞歸
b.兩個對象的屬性互相依賴厕氨,當json解析這類對象時就會一直循環(huán)解析命斧。(對某屬性@JsonIgnore) - 每個棧幀所占用過大
線程運行診斷
案例一:cpu 占用過多
- Linux環(huán)境下運行某些程序的時候国葬,可能導致CPU的占用過高汇四,這時需要定位占用CPU過高的線程
1通孽、確定哪個進程占用CPU過高:
?命令:top
2背苦、確定哪個線程占用CPU過高:
?命令:ps H -eo pid, tid, %cpu | grep 進程ID
3行剂、定位具體代碼:
?命令:jstack 進程id
?通過查看進程中的線程的nid厚宰,剛才ps命令看到的tid來對比定位,注意jstack查找出的線程id是16進制的城菊,需要轉換。
4漏麦、jstack 進程id 工具還可以定位死鎖信息撕贞。
3测垛、本地方法棧
一些帶有native關鍵字的方法就是需要JAVA去調用本地的C或者C++方法号涯,因為JAVA有時候沒法直接和操作系統(tǒng)底層交互锯七,所以需要用到本地方法
4眉尸、堆
定義
通過new關鍵字創(chuàng)建的對象都會被放在堆內存
特點
- 所有線程共享噪猾,堆內存中的對象都需要考慮線程安全問題
- 有垃圾回收機制
堆內存溢出
java.lang.OutofMemoryError :java heap space. 堆內存溢出
可以使用 -Xmx8m 來指定堆內存大小袱蜡。
調試程序時懷疑是堆內存溢出半夷,可以嘗試把堆內存調小一點試試迅细。
堆內存診斷
- jps 工具
-- 查看當前系統(tǒng)中有哪些 java 進程 - jmap 工具
-- 查看堆內存占用情況: jmap - heap 進程id
System.out.println("1");
Thread.sleep(30000);
System.out.println("2");
byte[] bytes = new byte[1024 * 1024 *10];
Thread.sleep(10000);
System.out.println(3);
bytes = null;
System.gc();
Thread.sleep(10000);
-- java11環(huán)境:jhsdb jmap --heap --pid 922
- jconsole 工具
-- 圖形界面的湘换,多功能的監(jiān)測工具彩倚,可以連續(xù)監(jiān)測 - jvirsualvm 工具
--監(jiān)視--》堆dump (可dump下來當前時刻的堆信息)
----〉檢查--查找(指定大小堆)---》定位具體哪個對象占用空間比較大
5帆离、方法區(qū)
定義
JVM線程之間共享的方法區(qū)域哥谷。它存儲每個類的結構數(shù)據(jù)
们妥,例如運行時常量池
监婶、字段
和方法
數(shù)據(jù),以及方法和構造函數(shù)
的代碼,包括特殊方法孕似,用于類和實例初始化以及接口初始化方法區(qū)域是在虛擬機啟動時創(chuàng)建的喉祭。
- 實現(xiàn):
永久代(1.8之前):存儲包括類信息泛烙、常量蔽氨、字符串常量鹉究、類靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)踪宠。
元空間(1.8以后):使用的是物理內存自赔,元空間是方法區(qū)的在 HotSpot JVM 中的實現(xiàn),方法區(qū)主要用于存儲類信息
柳琢、常量池
绍妨、方法數(shù)據(jù)
润脸、方法代碼
、符號引用
等毙驯。元空間的本質和永久代類似,都是對 JVM 規(guī)范中方法區(qū)的實現(xiàn)灾测。
方法區(qū)內存溢出
- 1.8 之前會導致永久代內存溢出
-- 使用 -XX:MaxPermSize=8m 指定永久代內存大小 - 1.8 之后會導致元空間內存溢出
-- 使用 -XX:MaxMetaspaceSize=8m 指定元空間大小
常量池
二進制字節(jié)碼的組成:類的基本信息爆价、常量池、類的方法定義(包含了虛擬機指令)
public class Hello {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
使用 javap -v Hello.class 命令反編譯查看結果:
運行時常量池
- 常量池
-- 就是一張表(如上圖中的constant pool)行施,虛擬機指令根據(jù)這張常量表找到要執(zhí)行的類名允坚、方法名魂那、參數(shù)類型蛾号、字面量信息 - 運行時常量池
-- 常量池是.class文件中的,當該類被加載以后涯雅,它的常量池信息就會放入運行時常量池鲜结,并把里面的符號地址變?yōu)檎鎸嵉刂?/em>*
串池StringTable
- 常量池中的字符串僅是符號,只有在被用到時才會轉化為對象
- 利用串池的機制活逆,來避免重復創(chuàng)建字符串對象
- 字符串變量拼接的原理是StringBuilder
- 字符串常量拼接的原理是編譯器優(yōu)化
- 可以使用intern方法精刷,主動將串池中還沒有的字符串對象放入串池中
- 注意:無論是串池還是堆里面的字符串,都是對象
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
}
}
編譯后的二進制中蔗候,常量池中的信息怒允,都會被加載到運行時常量池中,但這是a b ab 僅是常量池中的符號锈遥,還沒有成為java字符串
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
當執(zhí)行到 ldc #2 時纫事,會把符號 a 變?yōu)?“a” 字符串對象,并放入串池中(hashtable結構 不可擴容)
當執(zhí)行到 ldc #3 時所灸,會把符號 b 變?yōu)?“b” 字符串對象丽惶,并放入串池中
當執(zhí)行到 ldc #4 時,會把符號 ab 變?yōu)?“ab” 字符串對象爬立,并放入串池中
最終StringTable [“a”, “b”, “ab”]
注意:字符串對象的創(chuàng)建都是懶惰的钾唬,只有當運行到那一行字符串且在串池中不存在的時候(如 ldc #2)時,該字符串才會被創(chuàng)建并放入串池中侠驯。
- 使用拼接字符串變量對象創(chuàng)建字符串的過程:
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
//拼接字符串對象來創(chuàng)建新的字符串
String ab2 = a+b;
}
}
反編譯后的結果:
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
27: astore 4
29: return
通過變量拼接的方式來創(chuàng)建字符串的過程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一個新的字符串抡秆,但字符串的值和拼接的字符串一致,但是兩個不同的字符串吟策,一個存在于串池之中儒士,一個存在于堆內存之中
String ab = "ab";
String ab2 = a+b;
//結果為false,因為ab是存在于串池之中,ab2是由StringBuffer的toString方法所返回的一個對象踊挠,存在于堆內存之中
System.out.println(ab == ab2);
- 使用拼接字符串常量對象的方法創(chuàng)建字符串:
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
String ab2 = a+b;
//使用拼接字符串的方法創(chuàng)建字符串
String ab3 = "a" + "b";
}
}
反編譯后的結果:
Code:
stack=2, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
27: astore 4
//ab3初始化時直接從串池中獲取字符串
29: ldc #4 // String ab
31: astore 5
33: return
- 使用拼接字符串常量的方法來創(chuàng)建新的字符串時乍桂,因為內容是常量冲杀,javac在編譯期會進行優(yōu)化,結果已在編譯期確定為ab睹酌,而創(chuàng)建ab的時候已經(jīng)在串池中放入了“ab”权谁,所以ab3直接從串池中獲取值,所以進行的操作和 ab = “ab” 一致憋沿。
- 使用拼接字符串變量的方法來創(chuàng)建新的字符串時旺芽,因為內容是變量,只能在運行期確定它的值辐啄,所以需要使用StringBuilder來創(chuàng)建
intern方法 (jkd1.8)
一般采章,只有字面量才能放入串池中。
但壶辜,調用字符串對象的intern方法悯舟,會將該字符串對象嘗試放入到串池中
- 如果串池中沒有該字符串對象,則放入成功
- 如果有該字符串對象砸民,則放入失敗
無論放入是否成功抵怎,都會返回串池中的字符串對象
注意:此時如果調用intern方法成功(放入串池成功),堆內存與串池中的字符串對象是同一個對象岭参;如果失敺刺琛(放入串池失敗,也就是串池中已經(jīng)存在)演侯,則不是同一個對象
例1
public class Main {
public static void main(String[] args) {
//"a" "b" 被放入串池中姿染,str則存在于堆內存之中
String str = new String("a") + new String("b");
//調用str的intern方法,這時串池中沒有"ab"秒际,則會將該字符串對象放入到串池中悬赏,此時堆內存與串池中的"ab"是同一個對象
String st2 = str.intern();
//給str3賦值,因為此時串池中已有"ab"程癌,則直接將串池中的內容返回
String str3 = "ab";
//因為堆內存與串池中的"ab"是同一個對象舷嗡,所以以下兩條語句打印的都為true
System.out.println(str == st2);
System.out.println(str == str3);
}
}
例2
public class Main {
public static void main(String[] args) {
//此處創(chuàng)建字符串對象"ab",因為串池中還沒有"ab"嵌莉,所以將其放入串池中
String str3 = "ab";
//"a" "b" 被放入串池中进萄,str則存在于堆內存之中
String str = new String("a") + new String("b");
//此時因為在創(chuàng)建str3時,"ab"已存在與串池中锐峭,所以放入失敗中鼠,但是會返回串池中的"ab"
String str2 = str.intern();
//false
System.out.println(str == str2);
//false
System.out.println(str == str3);
//true
System.out.println(str2 == str3);
}
}
intern方法 (jkd1.6)
調用字符串對象的intern方法,會將該字符串對象嘗試放入到串池中
- 如果串池中沒有該字符串對象沿癞,會將該字符串對象復制一份援雇,再放入到串池中
- 如果有該字符串對象,則放入失敗
無論放入是否成功椎扬,都會返回串池中的字符串對象
注意:此時無論調用intern方法成功與否惫搏,串池中的字符串對象和堆內存中的字符串對象都不是同一個對象
StringTable 的位置
jdk1.6 StringTable 位置是在永久代中具温,1.8 StringTable 位置是在堆中。
永久代中gc是full gc筐赔,堆內存中gc是minorr gc铣猩。
/**
* 演示 StringTable 位置
* 在jdk8下設置 -Xmx10m -XX:-UseGCOverheadLimit
* 在jdk6下設置 -XX:MaxPermSize=10m
*/
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);
}
}
}
通過不同版本jdk的StringTable內存不足時報錯信息:
jdk1.6:java.lang.OutOfMemoryError:PermGen space
jdk1.8:java.lang.OutOfMemoryError:GC overhead limit exceeded
或者:java.lang.OutOfMemoryError:Java heap space
StringTable 垃圾回收
StringTable在內存緊張時,會發(fā)生垃圾回收
-Xmx10m 指定堆內存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次數(shù)茴丰,耗費時間等信息
【代碼演示】
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
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);
}
}
}
分別看往StringTable里添加100和10000個字符串常量看串池常量數(shù)增加和gc現(xiàn)象达皿。
StringTable調優(yōu)
- 因為StringTable是由HashTable實現(xiàn)的,所以可以適當增加HashTable桶的個數(shù)贿肩,來減少字符串放入串池所需要的時間
-XX:StringTableSize=桶個數(shù)(最少設置為 1009 以上)
/**
* 演示串池大小對性能的影響
* -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);
}
}
}
- 考慮是否需要將字符串對象入池
可以通過 intern 方法減少重復入池
/**
* 演示 intern 減少內存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
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.intern());
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
6峦椰、直接內存
- 屬于操作系統(tǒng),常見于NIO操作時汰规,用于數(shù)據(jù)緩沖區(qū) --ByteBuffer
- 分配回收成本較高汤功,但讀寫性能高
- 不受JVM內存回收管理
文件讀寫流程:
使用了DirectBuffer:
直接內存是操作系統(tǒng)和Java代碼都可以訪問的一塊區(qū)域,無需將代碼從系統(tǒng)內存復制到Java堆內存控轿,從而提高了效率
釋放原理
直接內存的回收不是通過JVM的垃圾回收來釋放的冤竹,而是通過unsafe.freeMemory來手動釋放
通過
//通過ByteBuffer申請1M的直接內存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
申請直接內存,但JVM并不能回收直接內存中的內容茬射,它是如何實現(xiàn)回收的呢?
allocateDirect的實現(xiàn)
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer類
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size); //申請內存
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通過虛引用冒签,來實現(xiàn)直接內存的釋放在抛,this為虛引用的實際對象
att = null;
}
這里調用了一個Cleaner的create方法,且后臺線程還會對虛引用的對象監(jiān)測萧恕,如果虛引用的實際對象(這里是DirectByteBuffer)被回收以后刚梭,就會調用Cleaner的clean方法,來清除直接內存中占用的內存
public void clean() {
if (remove(this)) {
try {
this.thunk.run(); //調用run方法
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
對應對象的run方法
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address); //釋放直接內存中占用的內存
address = 0;
Bits.unreserveMemory(size, capacity);
}
直接內存的回收機制總結
- 使用了Unsafe類來完成直接內存的分配回收票唆,回收需要主動調用freeMemory方法
- ByteBuffer的實現(xiàn)內部,使用了Cleaner(虛引用)來監(jiān)測ByteBuffer對象。一旦ByteBuffer被垃圾回收墓臭,那么會由ReferenceHandler來調用Cleaner的clean方法調用freeMemory來釋放內存森逮。