【Java虚拟机探究】4.常用JVM配置参数-堆的分配参数

在使用JVM编译java时,都会去设置相关的参数(例如使用eclipse的时候,可以设置eclipse的eclipse.ini文件来对jvm做一些参数配置)。jvm的参数设置主要涉及到三种,分别是Trace跟踪参数、堆的分配参数、栈的分配参数。

本章主要讲解堆的分配参数的相关信息。

我们都知道,Java程序运行过程中,类的实例对象都是存储在堆内存中的,堆内存的设置对程序的运行和优化有着十分重要的左右。

(1)最大堆和最小堆的设置
我们可以指定程序运行过程中堆内存的最大堆值和最小堆值。其中最大堆表示jvm最多能使用多少堆空间,最小堆的意思是jvm至少要使用多少堆空间,也及时jvm启动时,至少占用的空间。如果初始堆空间耗尽,虚拟机会对堆空间继续扩展,其扩展上限为最大堆空间。

使用以下参数来指定最大堆和最小堆:
-Xmx具体兆m -Xms具体兆m

在Java程序中我们也可以查看目前内存的使用情况:
System.out.print("Xmx=");//最大堆空间
System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024.0+"M");
		
System.out.print("free mem=");//空闲堆空间
System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024.0+"M");
		
System.out.print("total mem=");//被分配的总空间
System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024.0+"M");
上图程序分别计算了系统最大的内存空间、可用的空闲空间以及系统当前被分配的内存空间大小。

我们来做一个测试,在Eclipse中创建一个测试工程和一个测试类:
public class JvmXMTest {
    public static void main(String[] args) {
		byte[] b = new byte[1*1024*1024];
		System.out.println("分配了1M空间给数组");
		
		System.out.print("Xmx=");//最大堆空间
		System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024.0+"M");
		
		System.out.print("free mem=");//空闲堆空间
		System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024.0+"M");
		
		System.out.print("total mem=");//被分配的总空间
		System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024.0+"M");
	}
}
然后调整它的运行参数中的VM虚拟机参数:


这里最大内存给了20M,最小给了5M
运行上面的程序,在控制台中打印的结果如下:

可以看到最大空间为18M(去除预加载的一些内存),空余的接近4M(因为byte使用了1M),而一共被分配的是5M左右,没有低于最小内存。

一般情况下,在Java运行过程中,会尽可能的维持在最小堆。如果在GC后,程序内存无法保证在最小内存内,虚拟机会自动扩容,但不会超过最大堆内存。

我们将:
byte[] b = new byte[1*1024*1024];
System.out.println("分配了1M空间给数组");
改为
byte[] b = new byte[4*1024*1024];
System.out.println("分配了4M空间给数组");
运行程序,此时控制台打印信息如下:

可以看到,最大空间还是20M左右,而总空间被分配了10M,因为我们的使用该空间已经到达4M,原来的最小空间已经不足,所以虚拟机将分配空间拓展到了10M。

我们在原来代码最后添加以下代码:
System.gc();
System.out.println("回收内存");
手动去进行垃圾回收,然后再次打印三个内存参数,控制台输出如下:

可以看到空间空间被释放了一小部分。

思考:-Xmx和-Xms应该保持一个什么关系,可以让系统的性能尽可能的好呢?

(2)设置堆内存中具体类型的内存大小
记得之前提到过,java中堆内存被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。 
上述划分是为了让jvm能够更好的管理堆内存中的对象,包括内存的分配以及回收。 
下面是堆内存的大致模型图:

其中:
新生代:Young Generation,主要用来存放新生的对象。 
老年代:Old Generation或者称作Tenured Generation,主要存放应用程序声明周期长的内存对象。
永久代:(方法区,不属于java堆,另一个别名为“非堆Non-Heap”但是一般查看PrintGCDetails都会带上PermGen区)是指内存的永久保存区域,主要存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域. 它和和存放Instance的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用会加载很多Class的话,就很可能出现PermGen space错误。

以下是有关堆空间的不同内存类型的内存分配配置:
①-Xmn具体兆m
该配置用来指定新生代的大小。
而使用
②-XX:NewRatio=具体数字
该配置用来设置新生代(eden+两个Survivor)和老年代(不包含永久区)的比值。例如设置设个值为4,就代表新生代:老年代=1:4,即年轻代占堆的1/5。默认的,新生代与老年代的比例为1:2。
③-XX:SurvivorRatio=具体数字
该配置用来设置两个Survivor(from和to)与eden的比例。例如设置设个值为8,就代表两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10(默认Edem:from:to=8:1:1 )。
JVM每次只会使用Eden 和其中的一块Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

下面我们在上个MyEclipse的Java工程下再创建一个类,用来测试堆的详细内存配置:
public class HeapXMTest {
    public static void main(String[] args) {
		byte[] b = null;
		for (int i = 0; i < 10; i++) {
			b = new byte[1*1024*1024];//分配1M的byte数组对象
		}
	}
}
然后在运行参数这里做如下设置:

其中设置了最大堆内存和最小堆内存都是20M,然后设置了新生代大小为1M,最后设置打印GC回收详情。
运行后,观察控制台结果(本次代码是运行在JDK1.8上的):

可以看到发生了一次新生代的GC,而我们新生代(PSYoungGen)一共分配了1M空间,很显然是不够用的,所以可以看到剩下的都被分配到了老年代(ParOldGen)。

我们将参数修改:

此时为新生代分配了15M的空间,运行后观察控制台结果:

可以看到没有发生GC垃圾回收,证明空间是足以使用的,并且所有内存都在新生代使用了,利用率为93%,而老年代利用率0%,证明没有使用到任何老年代内存。

然后我们将堆内存分配一个既不大也不小的内存:

此时为新生代分配了7M的空间,运行后观察控制台结果:

可以看到,此时为新生代进行了GC回收,因为新生代的内存不足以放置10M的对象。GC回收大概4M左右的内存,此时在堆中还剩余6M左右的内存,此时新生代被使用5M左右内存,利用率为86%、96%,而老年代因为新生代的Survivor 区内存不足,替Survivor 区承载了一部分对象(1M左右),利用率是9%。

上面我们没有分配新生代中的eden和两个Survivor的具体内存比例,下面我们配置一下:

这里配置为2的意思就是,Eden:Survivor = 2,总大小为7M,2x+x+x=7M,x=1.75,Survivor内存占7M的1.75M左右。
运行后观察控制台结果:

可以发现触发了3次GC,分别回收了1M、2M、2M左右的空间,还剩下2M左右位回收的空间,其中新生代2M左右,利用率27%和97%,老年代228K,利用率1%。可以发现重新分配后,新生代利用率变得高了。


然后我们去除新生代的总配置,改为XX:NewRatio来配置新生代和老年代的比例:

此时设置新生代:老年代=1:1,即一共20M的内存,新老代区各10M,此时新生代利用率应该是最高的了。运行后观察控制台结果:

可以看到除了GC意外,新生代存储了大部分的byte对象。

理论上来说,GC次数越多,对系统越不利。所以对于eden区,合理的增大,可以减少GC的次数,而新生代的Survivor代要合理的减小。由于GC减少了,所以很多对象就没有机会被分配进老年代空间了,充分利用了新生代。

(3)堆的其它参数
-XX:+HeapDumpOnOutOfMemoryError
该参数意思就是,当发生OutOfMemoryError(OOM)的异常时,可以将该信息导出到文件中。
-XX:HeapDumpPath
该参数的意思就是,当程序Dump时(就是上面OOM的情况),导出OOM异常信息的路径。

下面是一个例子:

运行以下代码:
import java.util.Vector;

public class OOMTest {
    public static void main(String[] args) {
		Vector v = new Vector();
		for (int i = 0; i < 25; i++) {
			v.add(new byte[1*1024*1024]);
		}
	}
}
然后控制台报错:

在D盘下可以看到导出的报错文件:

我们使用IBM Memory Analyzer内存分析工具将dump文件打开,可以看到具体的内存分析:



-XX:OnOutOfMemoryError=脚本路径
在OOM的异常发生时,执行一个脚本,例如:
-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p
可以在printstack.bat脚本中编写任何程序,例如发邮件警告、或者重启程序等操作。

(4)堆空间分配总结
1.根据实际业务调整新生代和幸存代的大小
2.官方推荐新生代占堆的3/8
3.幸存代占新生代的1/10
4.在OOM时,记得Dump出堆,确保可以现场排查问题。

(5)永久区分配
这里要注意的是,永久代在JDK 7中逐渐变化,到JDK 8之后完全消失,合并到了Native堆中。
在JDK7下的永久区设置:
-XX:PermSize -XX:MaxPermSize
PermSize表示系统可以容纳多少个类,上面的两个参数分别设置永久区的初始空间和最大空间。

在JDK8下PermSize和MaxPermSize参数一并移除了,使用MetaSpace来替代,这些空间都直接在堆上来进行分配。在JDK8中,类的元数据存放在native堆中,这个空间被叫做:元数据区。相关的配置参数如下:
-XX:MetaspaceSize=<NNN> 
<NNN>是分配给类元数据区(以字节计)的初始大小(初始高水位),超过会导致垃圾收集器卸载类。这个数量是一个估计值。当第一次到达高水位的时候,下一个高水位是由垃圾收集器来管理的。
-XX:MaxMetaspaceSize=<NNN> 
<NNN>是分配给类元数据区的最大值(以字节计)。这个参数可以用来限制分配给类元数据区的大小。这个值也是个估计值。默认无上限。
-XX:MinMetaspaceFreeRatio=<NNN>
<NNN>是一次GC以后,为了避免增加元数据区(高水位)的大小,空闲的类元数据区的容量的最小比例,不够就会导致垃圾回收。
-XX:MaxMetaspaceFreeRatio=<NNN>
<NNN>是一次GC以后,为了避免减少元数据区(高水位)的大小,空闲的类元数据区的容量的最大比例,超过就会导致垃圾回收。

当使用cglib等库的时候,可能会产生大量的类,这些类有可能撑爆永久区导致OOM。
如下面的代码(需要加入cglib-nodep-2.2.jar依赖):
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;

public class PermSizeTest {
    public static void main(String[] args) {
    	for (int i = 0; i < 100000; i++) {
    		CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
		}
	}
}

class CglibBean {
	/**
	 * 实体Object
	 */
	public Object object = null;


	/**
	 * 属性map
	 */
	public BeanMap beanMap = null;


	public CglibBean() {
		super();
	}


	@SuppressWarnings("unchecked")
	public CglibBean(String name,Map propertyMap) {
		this.object = generateBean(name,propertyMap);
		this.beanMap = BeanMap.create(this.object);
	}


	/**
	 * 给bean属性赋值
	 * 
	 * @param property
	 *            属性名
	 * @param value
	 *            值
	 */
	public void setValue(String property, Object value) {
		beanMap.put(property, value);
	}


	/**
	 * 通过属性名得到属性值
	 * 
	 * @param property
	 *            属性名
	 * @return 值
	 */
	public Object getValue(String property) {
		return beanMap.get(property);
	}


	/**
	 * 得到该实体bean对象
	 * 
	 * @return
	 */
	public Object getObject() {
		return this.object;
	}


	@SuppressWarnings("unchecked")
	private Object generateBean(String name,Map propertyMap) {
		BeanGenerator generator = new BeanGenerator();
		Set keySet = propertyMap.keySet();
		for (Iterator i = keySet.iterator(); i.hasNext();) {
			String key = name +(String) i.next();
			generator.addProperty(key, (Class) propertyMap.get(key));
		}
		return generator.create();
	}
}
运行参数(这里使用的是JDK1.8,所以参数配置使用的是MetaspaceSize):

运行结果:

这里报JVM初始化的时候发生异常,提示MaxMetaspaceSize最大mate空间太小。其实就是一次性加载的类太多,导致方法区(7的永久代或8的MetaSpace)内存泄漏。
转载请注明出处:https://blog.csdn.net/acmman/article/details/80462103

猜你喜欢

转载自blog.csdn.net/u013517797/article/details/80462103