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这样的错误,并且输出了堆的快照文件,如下图所示

JVM之---Java内存结构(第二篇)_第1张图片
再次必须要清楚的两个概念就是,内存溢出和内存泄漏,如果是内存泄漏,我们就需要查看一下是什么样的对象导致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();
		}
	}
}
运行之后,出现了如下的结果:

JVM之---Java内存结构(第二篇)_第2张图片

      经过多次测试,始终都出现的是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的内存分布如下所示:
JVM之---Java内存结构(第二篇)_第3张图片

    这就是为什么我们调用equals方法相等,因为他们的Ascii码值相等,使用==判断是发现是false,那是因为他们的堆空间地址不一样,然后调用intern方法判断,他们在常量池中的地址又是一样的。了解了上述的代码和图示,我们就来作一个实验,然常量池溢出。

    

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

/**
 *说明:
 *intern()方法在调用的时候会首先到常量池中判断,有没有该常量,如果没有则加入,并且返回
 *引入List的目的是为了将intern之后的字符串加入到list中,这样可以避免垃圾回收期不对常量池中的常量进行回收
 *我们在编译的时候会使用PermSize参数
 */
public class ContantTest
{

	public void test()
	{
		List lists = new ArrayList();
		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机制!


你可能感兴趣的:(java虚拟机,Java,虚拟机,JVM,内存分布,实战)