JVM之---Java內存結構(第二篇)

在上一篇中我們大致瞭解了JVM的內存結構,在本節中,我們將通過一些小實驗,來驗證這些內存空間的存在,並且通過內存鏡像文件(dump)來分析一下內存溢出的原因。

本節的內容主要有:

1、用代碼驗證JVM內存的存儲內容

2、根據內存溢出的信息判斷是那部分出現問題;

3、如何解決2中出現的問題;

第一:堆內存溢出

java中的堆,主要存放Java對象的信息,想要JVM的堆出現溢出,只需要不斷的創建對象,並且避免垃圾回收器回收這些對象,就可以做到讓堆內存溢出,如何避免對象被GC,簡單的說就是該對象還在被引用或者持有(但是這樣的說法不嚴謹甚至不正確,我們在以後的JVM GC中將會講到,JVM如何進行垃圾回收)

在進行測試之前,我們先來說一下兩個JVM參數,那就是-Xms和-Xmx,其中第一個是JVM堆內存的最小值,第二個是JVM堆內存的最大值,當-Xms和-Xmx設置成一樣的就可以避免JVM自動擴展堆內存了,然後我們還可以通過參數-XX:+HeapDumpOnOutMemoryError來設定,當出現內存溢出時,Dump出當前的內存堆存儲快照文件,代碼如下所示:

import java.util.List;
import java.util.ArrayList;

/**
 *@desc 進行堆內存溢出的測試
 *@author wangwenjun(QQ:532500648)
 *@since 1.0.0
 * */
public class HeapDumpTest
{

	static class Test{}

	public static void main(String[] args)
	{
		List list = new ArrayList();
		while(true)
		{
			list.add(new Test());
		}
	}
}
編譯之後,運行的java命令爲:

java -verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError HeapDumpTest

運行之後,出現了java.lang.OutOfMemoryError: Java heap space這樣的錯誤,並且輸出了堆的快照文件,如下圖所示

再次必須要清楚的兩個概念就是,內存溢出和內存泄漏,如果是內存泄漏,我們就需要查看一下是什麼樣的對象導致java的垃圾回收器不能將對象回收,這也許就是我們代碼中的問題,如果是內存溢出,我們就需要看看是否我們申請的堆內存不足引起的,需要根據硬件配置適當的增加堆內存的大小,在後面的文章中我們學習如何讀懂dump文件。

第二、虛擬機棧和本地方法區的內存溢出

還記得虛擬機棧存放的是什麼嗎?是每一個方法運行期的棧幀,包括局部變量表,方法出口地址,動態鏈接,棧操作等,要是棧出現內存溢出有下面兩種可能

1、線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverFlowError

2、虛擬機無法擴展棧的大小是,將拋出OutOfMemoryError

其中設置棧內存大小的參數爲-Xss,知道了參數,知道了棧中存放的數據類型,設計出這樣的溢出情況是非常容易的,如下代碼所示

/**
 * @author wangwenjun(QQ:532500648)
 * @desc 測試棧內存的溢出
 */
public class StackOOMTest {

	private int i = 0;
	
	public void add()
	{
		i++;
		add();
	}
	
	public static void main(String[] args)
	{
		try {
			new StackOOMTest().add();
		} catch (Error e) {
			e.printStackTrace();
		}
	}
}
運行之後,出現瞭如下的結果:


      經過多次測試,始終都出現的是StackOverflowError,但是說好的OutOfMemoryError貌似怎麼都沒有出現,其實上面的異常屬於棧的訪問深度問題,爲了能夠測試出OutOfMemoryError,我們設計如下的程序代碼

public class OOMTest
{

	public void test()
	{
		while(true)
		{

			Thread t = new Thread(){
				public void run()
				{

					while(true)
					{
						//do something...
					}
				}
	
			};
			t.start();
		}
	}

	public static void main(String[] args)
	{
		try
		{
			new OOMTest().test();
		}catch(Error e)
		{
			e.printStackTrace();
		}
	}
}
運行命令爲:java -Xmx10M -Xms10M -Xss5M OOMTest,執行後的效果如下所示:



第三、常量池內存溢出

       如果要往常量池中添加常量,最簡單的方法是使用String的intern,在使用該方法之前,我們現來寫一個代碼測試一下intern,看如下的代碼:
public class InternTest
{

	public static void main(String[] args)
	{
		String s1 = new String("hello");
		String s2 = new String("hello");
		System.out.println(s1.equals(s2));	//判斷值是否相等
		System.out.println(s1==s2);			//判斷堆地址是否相等
		System.out.println(s1.intern()==s2.intern());	//判斷常量池地址是否相等
	}
}
      通過上面的實例,我們可以大致瞭解String的內存分佈如下所示:

    這就是爲什麼我們調用equals方法相等,因爲他們的Ascii碼值相等,使用==判斷是發現是false,那是因爲他們的堆空間地址不一樣,然後調用intern方法判斷,他們在常量池中的地址又是一樣的。瞭解了上述的代碼和圖示,我們就來作一個實驗,然常量池溢出。

    

import java.util.List;
import java.util.ArrayList;

/**
 *說明:
 *intern()方法在調用的時候會首先到常量池中判斷,有沒有該常量,如果沒有則加入,並且返回
 *引入List的目的是爲了將intern之後的字符串加入到list中,這樣可以避免垃圾回收期不對常量池中的常量進行回收
 *我們在編譯的時候會使用PermSize參數
 */
public class ContantTest
{

	public void test()
	{
		List<String> lists = new ArrayList<String>();
		String s1="hello";
		int index = 1;
		while(true)
		{
			lists.add((s1+(index++)).intern());
		}
	}

	public static void main(String[] args)
	{
		new ContantTest().test();
	}
}
運行命令爲:java -XX:PermSize=1M -XX:MaxPermSize=2M ContantTest 運行一段時間之後就會出現

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
 at java.lang.String.intern(Native Method)
     在上面的例子中,要等待PermGen space是一個比較漫長的過程,個人懷疑是jvm在運行起做了一定的優化,但常量池中的空間不夠使用的時候會到堆內存中劃分一定的空間給他使用,當然這樣的說法完全是個人的懷疑,其實我更多的懷疑是不是GC的問題,等有時間了做一下GC的內存快照文件,看看GC的運行情況。

第三:方法區溢出

      方法區有稱之爲非堆區,主要存放class相關信息,如類名,訪問修飾符,父類,方法描述,屬性描述等,如果要讓該內存出現溢出,就需要動態產生很多方法和屬性,然後填充方法區,直到他溢出,我們使用java中的動態代理,不斷的生成一些代理對象。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

class ProxyObject implements InvocationHandler
{
	private Object obj  = null;
	
	public  ProxyObject(Object obj)
	{
		this.obj = obj;
	}

	public Object invoke(Object proxy,Method m,Object[] args)
	{
		Object result = null;
		try
		{
			result = m.invoke(obj,args);
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		return result;
	}
}


interface SimpleInterface
{
	public void simpleMethod();
}


class SimpleImpl implements SimpleInterface
{
	public void simpleMethod()
	{
		//do nothing.
	}
}

/**
 * 運行命令:java -XX:permSize:10M -XX:MaxPermSize:10M MethodAreaTest
 */
public class MethodAreaTest
{

	public static void main(String[] args)
	{
		while(true)
		{
			SimpleInterface realObj = new SimpleImpl();
			SimpleInterface proxy = (SimpleInterface) Proxy.newProxyInstance(  
		        realObj.getClass().getClassLoader(),realObj.getClass().getInterfaces(),new ProxyObject(realObj));  
			proxy.simpleMethod();  
		}
	}
}

   程序運行若干時間之後將會拋出異常如下所示:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space


第四:本地方法區

     本地方法區,我們很難操作的到,但是在NIO和併發包中有一個Unsafe.java文件,該文件被很多併發庫以及nio的類庫進行使用,甚至在大名鼎鼎的Disruptor框架中大量的是用到了Unsafe類,該類不會讓你直接使用,但是可以通過反射的方式獲取得到,在本節中,我們通過該類直接創建內存,也就是本地方法區,代碼如下所示:

import sun.misc.Unsafe;  
import java.lang.reflect.Field; 

public class DirectMemoryTest
{

	public static void main(String[] args) throws Exception
        {
		int _1M = 1024*1024;
		Field field = Unsafe.class.getDeclaredField("theUnsafe");
		field.setAccessible(true);
		Unsafe unsafe = (Unsafe) field.get(null);
		while(true)
		{
			unsafe.allocateMemory(_1M);
		}
	}
}
     需要注意的是,直接內存的大小和堆內存大小一致,在運行的時候需要設置,否則他會以Xmx作爲最大值,運行方法爲:

java -XX:MaxDirectMemorySize=10M DirectMemoryTest
  運行結果如下所示:

Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at DirectMemoryTest.main(DirectMemoryTest.java:15)

好了,關於內存分佈的內容到這你就結束了,在未來的幾天,讓我們一起探究java的GC機制!


發佈了32 篇原創文章 · 獲贊 21 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章