了解Java虛擬機(jī)JVM的基本結(jié)構(gòu)及JVM的內(nèi)存溢出方式
JVM內(nèi)部結(jié)構(gòu)圖

Java虛擬機(jī)主要分為五個區(qū)域:方法區(qū)、堆、Java棧、PC寄存器、本地方法棧。下面
來看一些關(guān)于JVM結(jié)構(gòu)的重要問題。
1.哪些區(qū)域是共享的?哪些是私有的?
Java棧、本地方法棧、程序計數(shù)器是隨用戶線程的啟動和結(jié)束而建立和銷毀的,
每個線程都有獨立的這些區(qū)域。而方法區(qū)、堆是被整個JVM進(jìn)程中的所有線程共享的。

2.方法區(qū)保存什么?會被回收嗎?
方法區(qū)不是只保存的方法信息和代碼,同時在一塊叫做運(yùn)行時常量池的子區(qū)域還
保存了Class文件中常量表中的各種符號引用,以及翻譯出來的直接引用。通過堆中
的一個Class對象作為接口來訪問這些信息。
雖然方法區(qū)中保存的是類型信息,但是也是會被回收的,只不過回收的條件比較苛刻:
(1)該類的所有實例都已經(jīng)被回收
(2)加載該類的ClassLoader已經(jīng)被回收
(3)該類的Class對象沒有在任何地方被引用(包括Class.forName反射訪問)
3.方法區(qū)中常量池的內(nèi)容不變嗎?
方法區(qū)中的運(yùn)行時常量池保存了Class文件中靜態(tài)常量池中的數(shù)據(jù)。除了存放這些編譯時
生成的各種字面量和符號引用外,還包含了翻譯出來的直接引用。但這不代表運(yùn)行時常量池
就不會改變。比如運(yùn)行時可以調(diào)用String的intern方法,將新的字符串常量放入池中。
package com.cdai.jvm;
public class RuntimeConstantPool {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println("Before intern, s1 == s2: " + (s1 == s2));
s1 = s1.intern();
s2 = s2.intern();
System.out.println("After intern, s1 == s2: " + (s1 == s2));
}
}
4.所有的對象實例都在堆上分配嗎?
隨著逃逸分析技術(shù)的逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)使得“所有對象都分配
在堆上”也變得不那么絕對。
所謂逃逸就是當(dāng)一個對象的指針被多個方法或線程引用時,我們稱這個指針發(fā)生逃逸。
一般來說,Java對象是在堆里分配的,在棧中只保存了對象的指針。假設(shè)一個局部變量
在方法執(zhí)行期間未發(fā)生逃逸(暴露給方法外),則直接在棧里分配,之后繼續(xù)在調(diào)用棧
里執(zhí)行,方法執(zhí)行結(jié)束后??臻g被回收,局部變量就也被回收了。這樣就減少了大量臨時
對象在堆中分配,提高了GC回收的效率。
另外,逃逸分析也會對未發(fā)生逃逸的局部變量進(jìn)行鎖省略,將該變量上擁有的鎖省略掉。
啟用逃逸分析的方法時加上JVM啟動參數(shù):-XX:+DoEscapeAnalysis?EscapeAnalysisTest。
5.訪問堆上的對象有幾種方式?
(1)指針直接訪問
棧上的引用保存的就是指向堆上對象的指針,一次就可以定位對象,訪問速度比較快。
但是當(dāng)對象在堆中被移動時(垃圾回收時會經(jīng)常移動各個對象),棧上的指針變量的值
也需要改變。目前JVM HotSpot采用的是這種方式。

(2)句柄間接訪問
棧上的引用指向的是句柄池中的一個句柄,通過這個句柄中的值再訪問對象。因此句柄
就像二級指針,需要兩次定位才能訪問到對象,速度比直接指針定位要慢一些,但是當(dāng)
對象在堆中的位置移動時,不需要改變棧上引用的值。

JVM內(nèi)存溢出的方式
了解了Java虛擬機(jī)五個內(nèi)存區(qū)域的作用后,下面我們來繼續(xù)學(xué)習(xí)下在什么情況下
這些區(qū)域會發(fā)生溢出。
1.虛擬機(jī)參數(shù)配置
-Xms:初始堆大小,默認(rèn)為物理內(nèi)存的1/64(<1GB);默認(rèn)(MinHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存小于40%時,JVM就會增大堆直到-Xmx的最大限制。
-Xmx:最大堆大小,默認(rèn)(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時,JVM會減少堆直到 -Xms的最小限制。
-Xss:每個線程的堆棧大小。JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。應(yīng)根據(jù)應(yīng)用的線程所需內(nèi)存大小進(jìn)行適當(dāng)調(diào)整。在相同物理內(nèi)存下,減小這個值能生成更多的線程。但是操作系統(tǒng)對一個進(jìn)程內(nèi)的線程數(shù)還是有限制的,不能無限生成,經(jīng)驗值在3000~5000左右。一般小的應(yīng)用, 如果棧不是很深, 應(yīng)該是128k夠用的,大的應(yīng)用建議使用256k。這個選項對性能影響比較大,需要嚴(yán)格的測試。
-XX:PermSize:設(shè)置永久代(perm gen)初始值。默認(rèn)值為物理內(nèi)存的1/64。
-XX:MaxPermSize:設(shè)置持久代最大值。物理內(nèi)存的1/4。
2.方法區(qū)溢出
因為方法區(qū)是保存類的相關(guān)信息的,所以當(dāng)我們加載過多的類時就會導(dǎo)致方法區(qū)
溢出。在這里我們通過JDK動態(tài)代理和CGLIB代理兩種方式來試圖使方法區(qū)溢出。
2.1 JDK動態(tài)代理
package com.cdai.jvm.overflow;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MethodAreaOverflow {
static interface OOMInterface {
}
static class OOMObject implements OOMInterface {
}
static class OOMObject2 implements OOMInterface {
}
public static void main(String[] args) {
final OOMObject object = new OOMObject();
while (true) {
OOMInterface proxy = (OOMInterface) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
OOMObject.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("Interceptor1 is working");
return method.invoke(object, args);
}
}
);
System.out.println(proxy.getClass());
System.out.println("Proxy1: " + proxy);
OOMInterface proxy2 = (OOMInterface) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
OOMObject.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("Interceptor2 is working");
return method.invoke(object, args);
}
}
);
System.out.println(proxy2.getClass());
System.out.println("Proxy2: " + proxy2);
}
}
}
雖然我們不斷調(diào)用Proxy.newInstance()方法來創(chuàng)建代理類,但是JVM并沒有內(nèi)存溢出。
每次調(diào)用都生成了不同的代理類實例,但是代理類的Class對象沒有改變。是不是Proxy
類對代理類的Class對象有緩存?具體原因會在之后的《JDK動態(tài)代理與CGLIB》中進(jìn)行
詳細(xì)分析。
2.2 CGLIB代理
CGLIB同樣會緩存代理類的Class對象,但是我們可以通過配置讓它不緩存Class對象,
這樣就可以通過反復(fù)創(chuàng)建代理類達(dá)到使方法區(qū)溢出的目的。
package com.cdai.jvm.overflow;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class MethodAreaOverflow2 {
static class OOMObject {
}
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) throws Throwable {
return method.invoke(obj, args);
}
});
OOMObject proxy = (OOMObject) enhancer.create();
System.out.println(proxy.getClass());
}
}
}
3.堆溢出
堆溢出比較簡單,只需通過創(chuàng)建一個大數(shù)組對象來申請一塊比較大的內(nèi)存,就可以使
堆發(fā)生溢出。
package com.cdai.jvm.overflow;
public class HeapOverflow {
private static final int MB = 1024 * 1024;
@SuppressWarnings("unused")
public static void main(String[] args) {
byte[] bigMemory = new byte[1024 * MB];
}
}
4.棧溢出
棧溢出也比較常見,有時我們編寫的遞歸調(diào)用沒有正確的終止條件時,就會使方法不斷
遞歸,棧的深度不斷增大,最終發(fā)生棧溢出。
package com.cdai.jvm.overflow;
public class StackOverflow {
private static int stackDepth = 1;
public static void stackOverflow() {
stackDepth++;
stackOverflow();
}
public static void main(String[] args) {
try {
stackOverflow();
}
catch (Exception e) {
System.err.println("Stack depth: " + stackDepth);
e.printStackTrace();
}
}
}
相關(guān)文章
Java中forward轉(zhuǎn)發(fā)與redirect重定向的區(qū)別
轉(zhuǎn)發(fā)和重定向都是常用的頁面跳轉(zhuǎn)方式,但在實現(xiàn)上有一些區(qū)別,本文主要介紹了Java中forward轉(zhuǎn)發(fā)與redirect重定向的區(qū)別,具有一定的參考價值,感興趣的可以了解一下2023-11-11
Triple協(xié)議支持Java異?;貍髟O(shè)計實現(xiàn)詳解
這篇文章主要為大家介紹了Triple協(xié)議支持Java異?;貍髟O(shè)計實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Nacos1.4.0 Windows10單機(jī)模式啟動和集群啟動過程解析
這篇文章主要介紹了Nacos1.4.0 Windows10單機(jī)模式啟動和集群啟動,第一次使用nacos,廢話不多說,記錄下自己啟動Nacos遇到的坑,感興趣的朋友跟隨小編一起看看吧2023-10-10
Spring和MyBatis整合自動生成代碼里面text類型遇到的坑
Spring和MyBatis整合以后,使用自動生成代碼工具生成dao和mapper配置文件。下面通過本文給大家介紹Spring和MyBatis整合自動生成代碼里面text類型遇到的坑,需要的朋友參考下吧2018-01-01
配置java環(huán)境變量(linux mac windows7)
本文給大家詳細(xì)總結(jié)介紹了Linux、MAC以及Windows下配置java環(huán)境變量的方法,非常的細(xì)致全面,有需要的小伙伴可以參考下2015-11-11

