目錄
?
一、JVM內(nèi)存結(jié)構(gòu)
1. 虛擬機(jī)棧(JVM Stacks)
1)定義
2)棧內(nèi)存溢出
3) 線程運(yùn)行診斷
案例1:CPU占用過高
案例2:程序運(yùn)行很長時(shí)間沒有結(jié)果?編輯
2. 本地方法棧(Native Method Stacks)
3.? 堆(Heap)
1)定義
2)特點(diǎn)
3)堆內(nèi)存溢出
4)堆內(nèi)存診斷
5)案例:垃圾回收后,內(nèi)存占用仍然很高
4. 方法區(qū)
1)定義
2)組成?
3)方法區(qū)內(nèi)存溢出
4)運(yùn)行時(shí)常量池
5)StringTable
5.1 常量池和串池(StringTable)的關(guān)系
5.2?StringTable 特性
例1:第14行代碼本質(zhì)分析
例2:常量字符串拼接的底層原理
例3:intern() 方法
例4:面試題解答
5.3?StringTable 位置
5.4?StringTable 垃圾回收
5.5?StringTable 性能調(diào)優(yōu)
5. 直接內(nèi)存(Direct Memory)
1)定義
2)使用直接內(nèi)存的好處?
3)演示直接內(nèi)存溢出?
4)直接內(nèi)存回收原理
?一些文章:
- JVM參數(shù)設(shè)置_徒步?jīng)龀?Jasper的博客-CSDN博客
- JVM常用內(nèi)存參數(shù)配置_如何設(shè)置jvm啟動配置參數(shù)在那文件夾中_mars-kobe的博客-CSDN博客
- jvm參數(shù)設(shè)置方法(win10)_-xmx2048m -dfile.encoding=gb2312_還好,還好的博客-CSDN博客
- Java8-jvm運(yùn)行時(shí)數(shù)據(jù)區(qū)(權(quán)威解析) - 知乎
- 方法區(qū)和永久區(qū)/元空間之間的關(guān)系 - 簡書
- https://www.cnblogs.com/lanqingzhou/p/12374544.html
學(xué)習(xí)黑馬視頻:01_什么是jvm_嗶哩嗶哩_bilibili
筆記參考文章:
- JVM 學(xué)習(xí)筆記(一)內(nèi)存結(jié)構(gòu)_codeali csdn jvm內(nèi)存結(jié)構(gòu)_CodeAli的博客-CSDN博客
- 【JVM】內(nèi)存結(jié)構(gòu)_jvm結(jié)構(gòu)圖_ΘLLΘ的博客-CSDN博客
一、JVM內(nèi)存結(jié)構(gòu)
程序計(jì)數(shù)器
虛擬機(jī)棧
本地方法棧
堆
方法區(qū)
程序計(jì)數(shù)器、棧、本地方法棧,都是線程私有的。堆、方法區(qū)是線程共享的區(qū)域。
下圖來源:https://www.cnblogs.com/lanqingzhou/p/12374544.html?
1. 虛擬機(jī)棧(JVM Stacks)
1)定義
- 每個(gè)線程運(yùn)行時(shí)所需要的內(nèi)存,稱為虛擬機(jī)棧
- 每個(gè)棧由多個(gè)棧幀(Frame)組成,對應(yīng)著每次方法調(diào)用時(shí)所占用的內(nèi)存
- 每個(gè)線程只能有一個(gè)活動棧幀,對應(yīng)著當(dāng)前正在執(zhí)行的那個(gè)方法
問題辨析
1. 垃圾回收是否涉及棧內(nèi)存?
????????不會。棧內(nèi)存是方法調(diào)用產(chǎn)生的,方法調(diào)用結(jié)束后會彈出棧。
2. 棧內(nèi)存分配越大越好嗎?
????????不是。因?yàn)槲锢韮?nèi)存是一定的,棧內(nèi)存越大,可以支持更多的遞歸調(diào)用,但是可執(zhí)行的線程數(shù)就會越少。
3. 方法內(nèi)的局部變量是否線程安全
- 如果方法內(nèi)部的變量沒有逃離方法的作用訪問,它是線程安全的;(如果是對象,才需要考慮此問題;如果是基本類型變量,是可以保證它是線程安全的)
- 如果是局部變量引用了對象,并逃離了方法的訪問,那就要考慮線程安全問題。
總之,如果變量是線程私有的,就不用考慮線程安全問題;如果是共享的,如加了static之后,就需要考慮線程安全問題。
public class main1 {
public static void main(String[] args) {
}
//下面各個(gè)方法會不會造成線程安全問題?
//不會
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//會,可能會有其他線程使用這個(gè)對象
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//會,其他線程可能會拿到這個(gè)線程的引用
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2)棧內(nèi)存溢出
Java.lang.stackOverflowError 棧內(nèi)存溢出
導(dǎo)致棧內(nèi)存溢出的情況:棧幀過大、過多、或者第三方類庫操作,都有可能造成棧內(nèi)存溢出
設(shè)置虛擬機(jī)棧內(nèi)存大?。?/p>
package cn.itcast.jvm.t1.stack;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
/**
* json 數(shù)據(jù)轉(zhuǎn)換
*/
public class Demo1_19 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
//使用該注解避免循環(huán)調(diào)用問題,轉(zhuǎn)換時(shí)忽略這個(gè)屬性————只通過部門去關(guān)聯(lián)員工,員工不再關(guān)聯(lián)部門了
@JsonIgnore
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
3) 線程運(yùn)行診斷
案例1:CPU占用過高
Linux環(huán)境下運(yùn)行某些程序的時(shí)候,可能導(dǎo)致CPU的占用過高,這時(shí)需要定位占用CPU過高的線程。
第一步:用top命令定位哪個(gè)進(jìn)程對cpu的占用過高;
通過top命令可以看到,PID為32655的進(jìn)程編號占了CPU的99.7%,那如何進(jìn)一步定位問題呢?
第二步:用ps命令進(jìn)一步定位是哪個(gè)線程引起的cpu占用過高;(如下圖,線程32665有問題)
ps H -eo pid,tid,%cpu
ps H -eo pid,tid,%cpu | grep 進(jìn)程id ,剛才通過top查到的進(jìn)程號
通過下面這個(gè)命令發(fā)現(xiàn)占用CPU過高的線程編號為32665,該線程編號為十進(jìn)制的,換算為十六進(jìn)制為7f99;
第三步:jstack 進(jìn)程id
,可以根據(jù)線程id找到有問題的線程,進(jìn)一步定位問題代碼的源碼行號;?
案例2:程序運(yùn)行很長時(shí)間沒有結(jié)果
2. 本地方法棧(Native Method Stacks)
定義:實(shí)際上就是在java虛擬機(jī)調(diào)用一些本地方法時(shí),需要給這些本地方法提供的內(nèi)存空間。本地方法運(yùn)行時(shí),使用的內(nèi)存就叫本地方法棧。
作用:給本地方法的運(yùn)行提供內(nèi)存空間。
本地方法:指那些不是由java代碼編寫的方法,因?yàn)閖ava代碼是有一定限制的,有的時(shí)候它不能直接與操作系統(tǒng)底層打交道,所以就需要用C或者C++語言編寫的本地方法來真正與操作系統(tǒng)打交道。java代碼可以間接的通過本地方法來調(diào)用到底層的一些功能。
這樣的方法多嗎?當(dāng)然!不管是在一些java類庫,還是在執(zhí)行引擎,它們都會去調(diào)用這些本地方法。比如,在Object類的方法中,clone()方法是帶有native的,這種native方法它是沒有方法實(shí)現(xiàn)的,方法實(shí)現(xiàn)都是通過c或者c++語言編寫的,java代碼通過間接的去調(diào)用c或者c++的方法實(shí)現(xiàn)。
3.? 堆(Heap)
前面講的程序計(jì)數(shù)器、棧、本地方法棧,都是線程私有的。堆、方法區(qū)是線程共享的區(qū)域。
1)定義
? ? ? ? 通過new關(guān)鍵字創(chuàng)建的對象,都會使用堆內(nèi)存。
2)特點(diǎn)
- 堆中的對象是線程共享的,堆中的對象一般都需要考慮線程安全問題(有例外);(前面說的虛擬機(jī)棧中的局部變量只要不逃逸出方法的作用范圍,都是線程私有的,都是線程安全的);
- 垃圾回收機(jī)制;(Heap中不再被引用的對象,就會被當(dāng)作垃圾進(jìn)行回收,以釋放堆內(nèi)存)。
3)堆內(nèi)存溢出
堆空間大小設(shè)置:使用-Xmx參數(shù)
?????????所以,這里需要注意,當(dāng)內(nèi)存足夠大時(shí)不太容易暴露內(nèi)存溢出的問題,隨著時(shí)間的累積有可能會導(dǎo)致內(nèi)存溢出。但是,有可能你運(yùn)行了很短的一段時(shí)間發(fā)現(xiàn)它沒問題。所以,排查這種堆內(nèi)存問題,最好把運(yùn)行內(nèi)存設(shè)的稍微小一些,這樣會比較早的暴露堆內(nèi)存溢出的問題。
4)堆內(nèi)存診斷
1. jps 工具
????????查看當(dāng)前系統(tǒng)中有哪些 java 進(jìn)程
2. jmap 工具
????????查看堆內(nèi)存占用情況 jmap - heap 進(jìn)程id
?注意:它只能查詢某個(gè)時(shí)刻堆內(nèi)存的使用情況。如果想連續(xù)監(jiān)測,需要使用下面這個(gè)工具。
?3. jconsole 工具
????????圖形界面的,多功能的監(jiān)測工具,可以連續(xù)監(jiān)測
除了監(jiān)測堆內(nèi)存,還可以監(jiān)測CPU,線程。?
演示上面幾個(gè)工具的使用:
3.1?演示jmap工具的使用
jmap -heap 進(jìn)程id? ? ? ?——檢查該進(jìn)程堆內(nèi)存的占用情況;
?
?3.2?jconsole 工具的使用
5)案例:垃圾回收后,內(nèi)存占用仍然很高
jvisualvm可視化工具 (視頻21)
4. 方法區(qū)
官網(wǎng)地址:Chapter?2.?The Structure of the Java Virtual Machine
1)定義
????????對于 HotSpotJVM 而言,方法區(qū)還有一個(gè)別名叫做Non-Heap(非堆),目的就是要和堆分開。
????????Java 虛擬機(jī)有一個(gè)在所有 Java 虛擬機(jī)線程之間共享的方法區(qū)域。方法區(qū)域類似于用于傳統(tǒng)語言的編譯代碼的存儲區(qū)域,或者類似于操作系統(tǒng)進(jìn)程中的“文本”段。它存儲每個(gè)類的結(jié)構(gòu)信息,例如運(yùn)行時(shí)常量池、成員變量和方法數(shù)據(jù),以及方法和構(gòu)造函數(shù)的代碼,包括特殊方法,用于類和實(shí)例初始化以及接口初始化。
????????方法區(qū)域是在虛擬機(jī)啟動時(shí)創(chuàng)建的。盡管方法區(qū)域在邏輯上是堆的一部分,但簡單的實(shí)現(xiàn)可能會選擇不進(jìn)行垃圾收集或壓縮。此規(guī)范不強(qiáng)制指定方法區(qū)的位置或用于管理已編譯代碼的策略。方法區(qū)域可以具有固定的大小,或者可以根據(jù)計(jì)算的需要進(jìn)行擴(kuò)展,并且如果不需要更大的方法區(qū)域,則可以收縮。方法區(qū)域的內(nèi)存不需要是連續(xù)的。
Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code.
可以這樣理解:“方法區(qū)”在邏輯上是堆的一部分。雖然在概念上定義了“方法區(qū)”,但是不同的JVM廠商去實(shí)現(xiàn)時(shí),不一定會去遵從JVM邏輯上的定義。規(guī)范不強(qiáng)制方法區(qū)的位置,比如Oracle的HotSpot虛擬機(jī)在JDK1.8以前,它的實(shí)現(xiàn)叫做永久代,永久代就是使用了堆的一部分作為方法區(qū);但JDK1.8以后,它把永久代移除了,換了一種實(shí)現(xiàn),這種實(shí)現(xiàn)就元空間。元空間用的就不是堆內(nèi)存,而是本地內(nèi)存,即操作系統(tǒng)的內(nèi)存。所以,不同的實(shí)現(xiàn)對于方法區(qū)所在位置的選擇就會有所不同。如果網(wǎng)絡(luò)上有人提到永久代,它只是HotSpot JDK1.8以前的一個(gè)實(shí)現(xiàn)而已,方法區(qū)是規(guī)范,永久代和元空間都只是一種實(shí)現(xiàn)而已。
永久區(qū)和元空間的區(qū)別,參考:https://blog.csdn.net/m0_53284765/article/details/131693910?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22131693910%22%2C%22source%22%3A%22m0_53284765%22%7Dhttps://blog.csdn.net/m0_53284765/article/details/131693910?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22131693910%22%2C%22source%22%3A%22m0_53284765%22%7D
2)組成?
3)方法區(qū)內(nèi)存溢出
- 1.8 之前會導(dǎo)致永久代內(nèi)存溢出
- 使用 -XX:MaxPermSize=8m 指定永久代內(nèi)存大小
- 1.8 之后會導(dǎo)致元空間內(nèi)存溢出
- 使用 -XX:MaxMetaspaceSize=8m 指定元空間大小
?場景:Spring、MyBatis等。動態(tài)字節(jié)碼技術(shù)。
4)運(yùn)行時(shí)常量池
常量池:就是一張表,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量
等信息;
運(yùn)行時(shí)常量池:常量池是 *.class 文件中的,當(dāng)該類被加載,它的常量池信息就會放入運(yùn)行時(shí)常量
池,并把里面的符號地址變?yōu)檎鎸?shí)地址。(若不理解再看一遍視頻25、26就明白了)
5)StringTable
StringTable是運(yùn)行時(shí)常量池中重要的一個(gè)組成部分,即俗稱的串池。先看幾道面試題:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 問
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 問,如果調(diào)換了【最后兩行代碼】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
回答上面問題之前,必須知道的常識點(diǎn):
- ==:對于基本類型,比較的是值是否相等;對于引用類型,比較的是地址是否相等
- equals:比較的是對象是否相等(不能比較基本類型,因?yàn)閑quals是Object超類中的方法,而Object是所有類的父類)
- String是引用類型
5.1 常量池和串池(StringTable)的關(guān)系
常量池,最初存在于字節(jié)碼文件中,當(dāng)它運(yùn)行的時(shí)候就會被加載到運(yùn)行時(shí)常量池,但這時(shí)a b ab 僅是常量池中的符號,還沒有成為java字符串對象;只有當(dāng)程序執(zhí)行到第11行的時(shí)候,才會把a(bǔ)符號變?yōu)椤癮”字符串對象,以此類推。(視頻28)
5.2?StringTable 特性
- 常量池中的字符串僅是符號,第一次用到時(shí)才變?yōu)閷ο?/strong>
- 利用串池的機(jī)制,來避免重復(fù)創(chuàng)建字符串對象
- 字符串變量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是編譯期優(yōu)化
- 可以使用 intern 方法,主動將串池中還沒有的字符串對象放入串池
? ? ? ? ★ 1.8 將這個(gè)字符串對象嘗試放入串池,如果有則并不會放入,如果沒有則放入串池。這兩種情況都會把串池中的對象返回;
????????★ 1.6 將這個(gè)字符串對象嘗試放入串池,如果有則并不會放入,如果沒有會把此對象復(fù)制一份,放入串池,會把串池中的對象返回;
例1:第14行代碼本質(zhì)分析
例2:常量字符串拼接的底層原理
編譯期會進(jìn)行優(yōu)化,因?yàn)榻Y(jié)果是確定的。
例3:intern() 方法
intern方法 1.8:
調(diào)用字符串對象的intern方法,會將該字符串對象嘗試放入到串池中
- 如果串池中沒有該字符串對象,則放入成功
- 如果有該字符串對象,則放入失敗
無論放入是否成功,都會返回串池中的字符串對象
(下圖是針對1.8的情況)?
intern方法 1.6:
調(diào)用字符串對象的intern方法,會將該字符串對象嘗試放入到串池中
- 如果串池中沒有該字符串對象,會將該字符串對象復(fù)制一份,再放入到串池中
- 如果有該字符串對象,則放入失敗
無論放入是否成功,都會返回串池中的字符串對象
(下圖是針對1.6的情況)?
例4:面試題解答
5.3?StringTable 位置
- JDK1.6 時(shí),StringTable是屬于常量池的一部分,它隨常量池存儲在永久代中。
- JDK1.8 以后,StringTable是放在堆中的。
- 下面的案例證明了1.6中是存在于永久代,1.8中是存在于堆空間。
為什么要做這個(gè)更改呢?
? ? ? ? 因?yàn)橛谰么膬?nèi)存恢復(fù)效率很低,永久代是在full GC的時(shí)候才會觸發(fā)永久代的垃圾回收,而full GC要等到老年代的空間不足才會觸發(fā),觸發(fā)的時(shí)機(jī)有點(diǎn)晚,這就間接導(dǎo)致StringTable的垃圾回收效率并不高。其實(shí)StringTable用的非常頻繁,一個(gè)java應(yīng)用程序中大量的字符串常量對象都會分配到StringTable里,如果它的回收效率不高的話,就會占用大量的內(nèi)存,會產(chǎn)生永久代的內(nèi)存不足?;谶@個(gè)缺點(diǎn),從JDK1.7開始,就將StringTable轉(zhuǎn)移到了堆里。
5.4?StringTable 垃圾回收
-Xmx10m 指定堆內(nèi)存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次數(shù),耗費(fèi)時(shí)間等信息
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Code_05_StringTableTest {
public static void main(String[] args) {
int i = 0;
try {
for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
String.valueOf(j).intern();
i++;
}
}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
5.5?StringTable 性能調(diào)優(yōu)
StringTable底層是哈希表,所以它的性能跟哈希表大小是密切相關(guān)的。如果哈希表桶的個(gè)數(shù)比較多,那么元素相對就分散,哈希碰撞的幾乎就會減小,查找的速度也會變快;反之,如果桶的個(gè)數(shù)較少,那么哈希碰撞的幾乎就會增高,導(dǎo)致鏈表較長,查找的速度也會受到影響。
- StringTable是由HashTable實(shí)現(xiàn)的,所以可以適當(dāng)增加HashTable桶的個(gè)數(shù),來減少字符串放入串池所需要的時(shí)間
-XX:StringTableSize=xxxx
//最低為1009
- 考慮是否將字符串對象入池
????????可以通過intern方法減少重復(fù)入池,保證相同的地址在StringTable中只存儲一份
5. 直接內(nèi)存(Direct Memory)
1)定義
直接內(nèi)存,并不屬于Java虛擬機(jī)的內(nèi)存管理,而是屬于操作系統(tǒng)內(nèi)存。?
- 常見于 NIO 操作時(shí),用于數(shù)據(jù)緩沖區(qū)
- 分配回收成本較高,但讀寫性能高
- 不受 JVM 內(nèi)存回收管理
2)使用直接內(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:\\編程資料\\第三方教學(xué)視頻\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用時(shí):1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用時(shí):479.295165 702.291454 562.56592
}
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 用時(shí):" + (end - start) / 1000_000.0);
}
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 用時(shí):" + (end - start) / 1000_000.0);
}
}
????????因?yàn)?java 不能直接操作文件管理,需要切換到內(nèi)核態(tài),使用本地方法進(jìn)行操作,然后讀取磁盤文件,會在系統(tǒng)內(nèi)存中創(chuàng)建一個(gè)緩沖區(qū),將數(shù)據(jù)讀到系統(tǒng)緩沖區(qū), 然后在將系統(tǒng)緩沖區(qū)數(shù)據(jù),復(fù)制到 java 堆內(nèi)存中。缺點(diǎn)是數(shù)據(jù)存儲了兩份,在系統(tǒng)內(nèi)存中有一份,java 堆中有一份,造成了不必要的復(fù)制。(原理流程如上面左圖)
????????使用了 DirectBuffer 后文件讀取流程,如上面右圖。直接內(nèi)存是操作系統(tǒng)和 Java 代碼都可以訪問的一塊區(qū)域,無需將代碼從系統(tǒng)內(nèi)存復(fù)制到 Java 堆內(nèi)存,從而提高了效率。
3)演示直接內(nèi)存溢出?
4)直接內(nèi)存回收原理
直接內(nèi)存不受 JVM 內(nèi)存回收管理,那么它所分配內(nèi)存會不會被正確回收?底層又是怎么實(shí)現(xiàn)的?
public class Code_06_DirectMemoryTest {
public static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
// method();
method1();
}
// 演示 直接內(nèi)存 是被 unsafe 創(chuàng)建與回收
private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB, (byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
}
// 演示 直接內(nèi)存被 釋放
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完畢");
System.in.read();
System.out.println("開始釋放");
byteBuffer = null;
System.gc(); // 手動 gc
System.in.read();
}
}
直接內(nèi)存的回收不是通過 JVM 的垃圾回收來釋放的,而是通過unsafe.freeMemory 來手動釋放。
第一步:allocateDirect 的實(shí)現(xiàn)
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
底層是創(chuàng)建了一個(gè) DirectByteBuffer 對象。
第二步: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); // 申請內(nèi)存
} 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;
}
// 通過虛引用,來實(shí)現(xiàn)直接內(nèi)存的釋放,this為虛引用的實(shí)際對象, 第二個(gè)參數(shù)是一個(gè)回調(diào),
//實(shí)現(xiàn)了 runnable 接口,run 方法中通過 unsafe 釋放內(nèi)存。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
這里調(diào)用了一個(gè) Cleaner 的 create 方法,且后臺線程還會對虛引用的對象監(jiān)測,如果虛引用的實(shí)際對象(這里是 DirectByteBuffer )被回收以后,就會調(diào)用 Cleaner 的 clean 方法,來清除直接內(nèi)存中占用的內(nèi)存。?
public void clean() {
if (remove(this)) {
try {
// 都用函數(shù)的 run 方法, 釋放內(nèi)存
this.thunk.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;
}
});
}
}
}
可以看到關(guān)鍵的一行代碼, this.thunk.run(),thunk 是 Runnable 對象。run 方法就是回調(diào) Deallocator 中的 run 方法,
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 釋放內(nèi)存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
直接內(nèi)存的回收機(jī)制總結(jié)
- 使用了 Unsafe 類來完成直接內(nèi)存的分配回收,回收需要主動調(diào)用freeMemory 方法
- ByteBuffer 的實(shí)現(xiàn)內(nèi)部使用了 Cleaner(虛引用)來檢測 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么會由 ReferenceHandler(守護(hù)線程) 來調(diào)用 Cleaner 的 clean 方法調(diào)用 freeMemory 來釋放內(nèi)存
注意:
/**
* -XX:+DisableExplicitGC 顯示的
*/
private static void method() throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完畢");
System.in.read();
System.out.println("開始釋放");
byteBuffer = null;
System.gc(); // 手動 gc 失效
System.in.read();
}
一般用 jvm 調(diào)優(yōu)時(shí),會加上下面的參數(shù):
-XX:+DisableExplicitGC ?// 靜止顯示的 GC文章來源:http://www.zghlxwxcb.cn/news/detail-563212.html
意思就是禁止我們手動的 GC,比如手動 System.gc() 無效,它是一種 full gc,會回收新生代、老年代,會造成程序執(zhí)行的時(shí)間比較長。所以我們就通過 unsafe 對象調(diào)用 freeMemory 的方式釋放內(nèi)存。文章來源地址http://www.zghlxwxcb.cn/news/detail-563212.html
到了這里,關(guān)于JVM學(xué)習(xí)筆記(二)內(nèi)存結(jié)構(gòu)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!