JVM的內存結構一般指Java的運行時數據區:
由方法區,堆區,虛擬機棧,程序計數器和本地方法棧組成。下面我們依次介紹這5部分。
1.程序計數器(Program Counter Register)
程序計數器:記錄下一條要執行的JVM指令的執行地址,字節碼解釋器工作時就是通過改變程序計數器中的值來獲取下一條要執行的指令,分支,循環,跳轉,異常處理線程恢復等功能都是依賴程序計數器來完成。
程序計數器存放在cpu的寄存器中。每條線程都有自己獨立的程序計數器,各個程序計數器互不影響,故程序計數器為線程隔離的數據。
如果當前執行的是Java的非native方法,那么程序計數器存儲下一條要執行的指令。如果是native方法那么程序計數器中為null。
特點:
- 線程私有
- 無內存溢出
2.Java虛擬機棧(VM Stack)
Java虛擬機棧和程序計數器一樣,都是線程私有的,它的生命周期與線程相同。
- 每個Java線程運行需要的內存,稱為虛擬機棧
- 每個棧由多個棧幀(Frame)組成,一個棧幀對應一個對應線程調用的方法時所占用的內存。
- 每個虛擬機棧只有一個活動棧幀,對應當前正在執行的方法。
Java虛擬機棧的內存模型:
有關Java虛擬機棧的問題
1.Java虛擬機棧內存是否受到內存回收機制的管理?
不會,因為在每次執行完方法后,內存會自動釋放。當然線程執行完成后內存也是自動釋放。
2.Java虛擬機棧內存分配越大越好?
不是,合適即可,如果棧內存分配過小容易造成內存溢出,而如果內存過大,那么Java開啟的線程數就會變小,降低了Java代碼的運行效率。
設置虛擬機棧的大小的虛擬機指令:
3.方法內局部變量是否線程安全?
- 如果方法內局部變量的只作用在方法內部,那么是線程安全的
- 如果局部變量引用了對象,或作為返回值返回那么就需要考慮線程安全問題。
虛擬機棧的內存溢出
- 棧幀過多會引發棧溢出(java.lang.StackOverflowError)。如程序中出現死循環時,造成虛擬機棧中不斷創建棧幀導致內存溢出。
- 棧幀過大時,也會造成內存溢出(OutofMemoryError)。如果虛擬機棧可以動態擴展,如果擴展到大小超過Java虛擬機規范中的規定的大小時也會造成溢出。
3.本地方法棧(Native Method Stack)
本地方法棧與Java虛擬機棧的作用非常相似,不過Java虛擬機棧調用的是Java方法(即二進制字節碼),而本地方法棧調用虛擬機使用到的Native方法。虛擬機規范並未強制規定,該方法的具體實現的語言。
與虛擬機棧一樣本地方法棧也會拋出StackOverflowError和OutofMemoryError異常。
4.堆(heap)
定義
堆是Java虛擬機管理中內存最大的一塊,是一塊線程共享的區域。在虛擬機啟動時創建堆區。堆區的唯一目的是存放對象實例(即new出來的對象)。
Java堆是垃圾回收器管理的最主要區域,因此也被稱為GC堆
堆內存溢出
Java堆內存溢出是堆中最常見的異常,出現的原因是當進程中不斷有新的對象創建而老對象有在使用導致不能被垃圾回收機制回收,就會出現堆內存溢出情況
可設置堆內存的大小來觀測堆內存溢出情況:
設置堆內存大小的參數是-Xmx size
-Xmx8m
java.lang.OutOfMemoryError: Java heap space
內存溢出異常
示例:
/**
* java.lang.OutOfMemoryError:內存溢出問題
* 設置堆的大小:
* -Xmx8m
* 設置為8m
*/
public class heapdemo {
public static void main(String[] args) {
int count = 0;
List<String> list = new ArrayList<>();
String str = "hello";
try {
while (true){
list.add(str);
str+=str;
count++;
}
}catch (Throwable throwable){
throwable.printStackTrace();
System.out.println(count);
System.out.println(str.length());
}
}
}
輸出結果:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at wf.memo.heapdemo.main(heapdemo.java:21)
26
335544320
5.方法區(Method Area)
方法區( Method Area )與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。
組成
方法區在1.6版本以后發生了很大的改變,1.6以前方法區在一個叫永久代,此時方法區的數據被存儲在堆中,有虛擬機進行管理。而在1.7后把方法區中的stringTable(字符串常量池)移出永久代還是存放在堆中。1.8以后把剩下的部分稱為元空間(MetaSpace)並把元空間移出堆,存入內存中。
方法區的內存溢出
1.8以前會導致永久代內存溢出
演示永久代內存溢出java. lang . OutOfMemoryError: PermGen space
-XX:MaxPernSize=8m
1.8以后會導致元空間內存溢出
演示元空間內存溢出java . lang . OutOfMemoryError: Metaspace
- -XX:MaxMetaspaceSize=8m
因為方法區主要存儲的是被加載的Java的類文件信息,故如果使用cglib等代理方式在代碼運行的過程中,不斷加載新的lei,那么就可能引起內存溢出。
方法區內存溢出實例
/**
* -XX:MaxMetaspaceSize=size
*/
public class Mareademo extends ClassLoader {
public static void main(String[] args) {
int count=0;
try {
Mareademo mareademo = new Mareademo();
for (int i = 0; i < 100000; i++) {
//ClassWriter作用是生成類的二進制字節碼
ClassWriter classWriter = new ClassWriter(0);
//visit()方法各個參數的意義:1.Java的版本號,2.類修飾符(public);3.類名稱;4.包名;4.父類;5.實現的接口
classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"class"+count,null,"java/lang/Object",null);
//返回byte數組及二進制字節碼文件
byte[] bytes = classWriter.toByteArray();
//加載二進制文件
mareademo.defineClass("class"+count,bytes,0,bytes.length);
count++;
}
}finally {
System.out.println(count);
}
}
}
在執行代碼時因為方法區存在於本地內存所以可以放下的我們申請的類,所以我們在運行前需要設置參數減小方法區的大小,才能觀察到內存溢出現象。
輸出:
5411
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at wf.memo.Mareademo.main(Mareademo.java:23)
6.運行時常量池
運行時常量池( Runtime Constant Pool )是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池( Constant Pool Table ),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
即常量池,就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等信息
運行時常量池,常量池是*.class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,並把里面的符號地址變為真實地址
我們來了解一下一個具體實例中常量池的信息有哪些:
二進制字節碼由:類基本信息,常量池,類方法定義及虛擬機指令
//類基本信息
Classfile /E:/java/idea/ym/jvm/out/production/jvm/wf/memo/Demo.class
Last modified 2020-2-11; size 531 bytes
MD5 checksum c8ad1fc04006e932e4edcd9c9cfcebba
Compiled from "Demo.java"
public class wf.memo.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // wf/memo/Demo
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lwf/memo/Demo;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Demo.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 wf/memo/Demo
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V、
//類方法定義
{
//默認的構造方法
public wf.memo.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lwf/memo/Demo;
//main方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
在執行nain方法是主要是執行
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
指令,getstatic 后面接 #2所以執行到getstatic時會到常量池中查詢#2對應的值,#2的Fieldref表示是一個成員變量,而#2又對應#20和#21繼續查找最終可以確定,getstatic是一個java/io/PrintStream所調用的 java /lang / System的out。
7.StringTables
存放在常量池中的類信息,都會被加載到運行時常量池中,但是如果類中存在字符串,那么字符串在剛載入運行時常量池時不會立即變為對象,只是常量池中的符號,只有執行的代碼調用相應的字符串時,才會把字符串變為對象。這是一種懶加載。
特性:
- 常量池中的字符串僅是符號,第一次用到時才變為對象
- 利用串池的機制,來避免重復創建字符串對象
- 字符串變量拼接的原理:是StringBuilder (1.8)
- 字符串常量拼接的原理是編譯期優化
String的intern()方法:
可以使用intern方法,主動將串池中還沒有的字符串對象放入串池
- 1.8 版本以后將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池,會把串池中的對象返回
- 1.6將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有會把此對象復制一份,則放入串池,會把串池中的對象返回
面試題:
public class StringTableDemo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
String s6 = s4.intern();
String s7 = "a" + s2;
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
System.out.println(s3 == s7);//false
String x1 = new String("c") + new String("d");
String x2 = x1.intern();
String x3 = "cd";
System.out.println(x1 == x2);//true
System.out.println(x3 == x1);//true
}
}
分析:
- s3 == s4為false因為s3是字符串常量存在有StringTable(以下稱:串表)中,而s4是由s1和s2兩個字符串對象拼接而成,對象是可能變化的,所以s4對象是拼接過程中新new出來的(其實兩個字符串對象拼接的過程就是new一個StringBuild然后往StringBuild中append添加函數的過程。),而且s4使用intern()函數前字符串"ab"在串表中已經存在,不會把s4的對象插入串表,所以為false
- s3 == s5為true。因為s5是"a"和"b"兩個字符串常量拼接而成,常量不會變化所以jvm會在編譯其對常量字符串的拼接進行優化,直接把拼接后的結果存入串表中。
- s3 == s6為true。因為intern函數無論調用該函數的字符串是否在串表中存在,函數都會把串表中的對應字符串返回,故s6和s3的地址相同為false
- s3 == s7為false。因為s7拼接中也涉及到了變量的操作,而字符串拼接過程中一旦變量參與拼接,都會new一個StringBuild。
- x1 == x2為true。因為x1調用intern函數時串表中不存在"cd"字符串,所以會把x1的地址添加到串表中,而該地址給x2返回故二者相等。
- x3 == x1為true。因為Java字符串只有在調用時才會為其生成對象,在調用前這是常量池中的符號。而常量對象會從串表中取地址,如果不存在會創建新的對象返回地址。故在調用 String x3 = "cd";時才會檢測串表中是否存在 "cd",因為已經存在故會把串表中的字符串地址返回給x3.
StringTable也會發生垃圾回收。
StringTable的性能調優:在使用String對象時,可以先調用一下intern函數把字符串入串表,來減少對象的空間占用。
8.直接內存
直接內存並不屬於JVM內存結構,即直接內存被不受JVM的管理。直接內存( Direct Memory )並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現,所以我們放到這里一起講解。
在JDK 1.4中新加入了NIO ( New Input/Output )類,引入了一種基於通道( Channel )與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制數據。
顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現OutCfMemoryError異常。
引入直接內存的原因
這是不使用直接內存時,Java操作磁盤文件的過程。我們可以看到,如果Java想使用磁盤文件的內容,那么需要先把數據讀入系統緩存區,然后從系統緩存區復制一份,給Java的緩沖區,才能使用。
而如果我們使用直接內存:
那么表示Java可以直接使用內存的一部分,所以Java操控磁盤文件時,當文件寫入系統的內存后,Java可以直接使用,不在進行復制到Java自己緩存區的操作,大大的提供了程序運行的效率。
我們可以使用代碼來演示使用與不使用直接內存的情況,來觀察二者的效率:
public class DirectDemo {
private static String from = "F:\\game\\view.mp4";
private static String to = "F:\\h.mp4";
private static int SIZE = 1024*1024;
public static void main(String[] args) {
io();
directBuffer();
}
private static void io() {
long start = System.currentTimeMillis();
try {
FileOutputStream outputStream = new FileOutputStream(to);
FileInputStream inputStream = new FileInputStream(from);
byte[] bytes = new byte[SIZE];
while (true){
int read = inputStream.read(bytes);
if (read == -1){
break;
}
outputStream.write(bytes,0,read);
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println("no direct use time:" + (System.currentTimeMillis()-start));
}
private static void directBuffer() {
long start = System.currentTimeMillis();
try {
FileChannel outputStream = new FileOutputStream(to).getChannel();
FileChannel inputStream = new FileInputStream(from).getChannel();
ByteBuffer bb = ByteBuffer.allocateDirect(SIZE);
while (true){
int read = inputStream.read(bb);
if (read == -1){
break;
}
bb.flip();
outputStream.write(bb);
bb. clear();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println("direct use time:" + (System.currentTimeMillis()-start));
}
}
輸出結果:
no direct use time:423
direct use time:169
no direct use time:428
direct use time:169
no direct use time:425
direct use time:196
執行三次可以看到,使用直接內存的效率總是優於不使用直接內存。
直接內存的內存溢出
雖然直接內存不由JVM管理,但是它仍然會存在內存溢出問題。
案例:
public class StringTableDemo2 {
public static void main(String[] args) {
List list = new ArrayList();
int count = 0;
try {
while (true){
//ByteBuffer.allocateDirect方法可以分配在直接內存分配空間
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024*1024*100);//100m
list.add(byteBuffer);
count++;
}
}catch (Throwable throwable){
System.out.println(count);
throwable.printStackTrace();
}
}
}
輸出結果:
36
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at wf.memo.StringTableDemo2.main(StringTableDemo2.java:19)
可以看到循環了36次才釋放內存(3.6g左右),即直接內存由上限,如果超過就會導致內存溢出。
直接內存的內存回收
雖然直接內存不受JVM管理,但是Java可以通過程序來控制直接內存的釋放和申請。Java中的Unsafe就提供了對直接內存的回收和釋放。而Unsafe類在具體的直接內存的申請和釋放是native修飾即為本地方法。故就如之前所說Java的確不能控制直接內存的申請和釋放。但Java可以通過其他語言對內存的操作來實現內存的使用。從而變相的實現JVM對直接內存的管理。
示例;
public class UnsafeDemo {
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
int size = 1024*1024*1024;
System.out.println("等待開始。。");
System.in.read();
System.out.println("資源開始寫入。。。");
long l = unsafe.allocateMemory(size);
unsafe.setMemory(l,size,(byte)0);
System.in.read();
System.out.println("資源釋放");
unsafe.freeMemory(l);
System.in.read();
}
//我們不能直接獲取Unsafe對象,故需要通過反射實現
private static Unsafe getUnsafe(){
Field theUnsafe;
Unsafe unsafe = null;
try {
theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return unsafe;
}
}
該類實現了對內存的操作,在運行該類后,可以查看任務管理器,可以看到在確定該類后,會增加Java程序對內存的使用量。
直接內存的分配與釋放原理:
- 使用了Unsafe對象完成直接內存的分配回收,並且回收需要主動調用freeMemory方法
- ByteBuffer的實現類內部,使用了Cleaner (虛引用)來監測ByteBuffer對象,一旦ByteBuffer對象被垃圾回收,那么就會由ReferenceHandler線程通過Cleaner的clean方法調用freeMemory來釋放直接內存。
禁用顯示回收對直接內存的影響:
-XX:+DisableExplicitGC
命令可以關閉顯示的調用System.gc(),即在代碼中使用該gc方法不會起任何作用。而有時我們要可能因為某些原因關閉顯示調用gc方法,此時我們申請的直接內存得不到釋放。所以如果我們想在程序中直接使用直接內存,那么最好在使用完成后調用Unsafe.freeMemory()方法對內存進行回收。和c++程序一樣。