在使用JVM编译java时,都会去设置相关的参数(例如使用eclipse的时候,可以设置eclipse的eclipse.ini文件来对jvm做一些参数配置)。jvm的参数设置主要涉及到三种,分别是Trace跟踪参数、堆的分配参数、栈的分配参数。
本章主要讲解堆的分配参数的相关信息。
我们都知道,Java程序运行过程中,类的实例对象都是存储在堆内存中的,堆内存的设置对程序的运行和优化有着十分重要的左右。
(1)最大堆和最小堆的设置
我们可以指定程序运行过程中堆内存的最大堆值和最小堆值。其中最大堆表示jvm最多能使用多少堆空间,最小堆的意思是jvm至少要使用多少堆空间,也及时jvm启动时,至少占用的空间。如果初始堆空间耗尽,虚拟机会对堆空间继续扩展,其扩展上限为最大堆空间。
使用以下参数来指定最大堆和最小堆:
-Xmx具体兆m -Xms具体兆m
在Java程序中我们也可以查看目前内存的使用情况:
我们来做一个测试,在Eclipse中创建一个测试工程和一个测试类:
运行上面的程序,在控制台中打印的结果如下:
可以看到最大空间为18M(去除预加载的一些内存),空余的接近4M(因为byte使用了1M),而一共被分配的是5M左右,没有低于最小内存。
一般情况下,在Java运行过程中,会尽可能的维持在最小堆。如果在GC后,程序内存无法保证在最小内存内,虚拟机会自动扩容,但不会超过最大堆内存。
我们将:
可以看到,最大空间还是20M左右,而总空间被分配了10M,因为我们的使用该空间已经到达4M,原来的最小空间已经不足,所以虚拟机将分配空间拓展到了10M。
我们在原来代码最后添加以下代码:
可以看到空间空间被释放了一小部分。
思考:-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工程下再创建一个类,用来测试堆的详细内存配置:
其中设置了最大堆内存和最小堆内存都是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异常信息的路径。
下面是一个例子:
运行以下代码:
在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依赖):
运行结果:
这里报JVM初始化的时候发生异常,提示MaxMetaspaceSize最大mate空间太小。其实就是一次性加载的类太多,导致方法区(7的永久代或8的MetaSpace)内存泄漏。
转载请注明出处:https://blog.csdn.net/acmman/article/details/80462103
本章主要讲解堆的分配参数的相关信息。
我们都知道,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虚拟机参数:
运行上面的程序,在控制台中打印的结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/cc12c22c24fbf857d125f5a928b38577.png)
可以看到最大空间为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空间给数组");
运行程序,此时控制台打印信息如下:
![](https://i-blog.csdnimg.cn/blog_migrate/b26fd3bc84b1b60184f5260f310c0b29.png)
可以看到,最大空间还是20M左右,而总空间被分配了10M,因为我们的使用该空间已经到达4M,原来的最小空间已经不足,所以虚拟机将分配空间拓展到了10M。
我们在原来代码最后添加以下代码:
System.gc();
System.out.println("回收内存");
手动去进行垃圾回收,然后再次打印三个内存参数,控制台输出如下:
![](https://i-blog.csdnimg.cn/blog_migrate/a3459e42dd60509b2c11ec11380b8489.png)
可以看到空间空间被释放了一小部分。
思考:-Xmx和-Xms应该保持一个什么关系,可以让系统的性能尽可能的好呢?
(2)设置堆内存中具体类型的内存大小
记得之前提到过,java中堆内存被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
上述划分是为了让jvm能够更好的管理堆内存中的对象,包括内存的分配以及回收。
下面是堆内存的大致模型图:
![](https://i-blog.csdnimg.cn/blog_migrate/0737a14589524d639cfa7dda8e21843c.jpeg)
其中:
新生代: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数组对象
}
}
}
然后在运行参数这里做如下设置:
![](https://i-blog.csdnimg.cn/blog_migrate/54b494aebec2e96ee0b49afdee107393.png)
其中设置了最大堆内存和最小堆内存都是20M,然后设置了新生代大小为1M,最后设置打印GC回收详情。
运行后,观察控制台结果(本次代码是运行在JDK1.8上的):
![](https://i-blog.csdnimg.cn/blog_migrate/3d818b4a12273f11502881fe8faa40a2.png)
可以看到发生了一次新生代的GC,而我们新生代(PSYoungGen)一共分配了1M空间,很显然是不够用的,所以可以看到剩下的都被分配到了老年代(ParOldGen)。
我们将参数修改:
![](https://i-blog.csdnimg.cn/blog_migrate/2836e3cf9bb8afa1979a6a839db537ff.png)
此时为新生代分配了15M的空间,运行后观察控制台结果:
![](https://i-blog.csdnimg.cn/blog_migrate/c44fadc96ad6f600af1ba73c3d95566a.png)
可以看到没有发生GC垃圾回收,证明空间是足以使用的,并且所有内存都在新生代使用了,利用率为93%,而老年代利用率0%,证明没有使用到任何老年代内存。
然后我们将堆内存分配一个既不大也不小的内存:
![](https://i-blog.csdnimg.cn/blog_migrate/9756991da342d6276f04307906e2c40c.png)
此时为新生代分配了7M的空间,运行后观察控制台结果:
![](https://i-blog.csdnimg.cn/blog_migrate/0d6649bae5c428204b6c01e125068013.png)
可以看到,此时为新生代进行了GC回收,因为新生代的内存不足以放置10M的对象。GC回收大概4M左右的内存,此时在堆中还剩余6M左右的内存,此时新生代被使用5M左右内存,利用率为86%、96%,而老年代因为新生代的Survivor 区内存不足,替Survivor 区承载了一部分对象(1M左右),利用率是9%。
上面我们没有分配新生代中的eden和两个Survivor的具体内存比例,下面我们配置一下:
![](https://i-blog.csdnimg.cn/blog_migrate/e11dcd8003cbd86796f1340877b5e3bb.png)
这里配置为2的意思就是,Eden:Survivor = 2,总大小为7M,2x+x+x=7M,x=1.75,Survivor内存占7M的1.75M左右。
运行后观察控制台结果:
![](https://i-blog.csdnimg.cn/blog_migrate/0ca40ea526d0a459e7999ea95abe9ad7.png)
可以发现触发了3次GC,分别回收了1M、2M、2M左右的空间,还剩下2M左右位回收的空间,其中新生代2M左右,利用率27%和97%,老年代228K,利用率1%。可以发现重新分配后,新生代利用率变得高了。
然后我们去除新生代的总配置,改为XX:NewRatio来配置新生代和老年代的比例:
![](https://i-blog.csdnimg.cn/blog_migrate/56fc64b5ef804fa16954b62ba5a0b38f.png)
此时设置新生代:老年代=1:1,即一共20M的内存,新老代区各10M,此时新生代利用率应该是最高的了。运行后观察控制台结果:
![](https://i-blog.csdnimg.cn/blog_migrate/16210f6f6296f7d38b473f183c6d07df.png)
可以看到除了GC意外,新生代存储了大部分的byte对象。
理论上来说,GC次数越多,对系统越不利。所以对于eden区,合理的增大,可以减少GC的次数,而新生代的Survivor代要合理的减小。由于GC减少了,所以很多对象就没有机会被分配进老年代空间了,充分利用了新生代。
(3)堆的其它参数
-XX:+HeapDumpOnOutOfMemoryError
该参数意思就是,当发生OutOfMemoryError(OOM)的异常时,可以将该信息导出到文件中。
-XX:HeapDumpPath
该参数的意思就是,当程序Dump时(就是上面OOM的情况),导出OOM异常信息的路径。
下面是一个例子:
![](https://i-blog.csdnimg.cn/blog_migrate/d717b749d60bf03d8e1b36180807f538.png)
运行以下代码:
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]);
}
}
}
然后控制台报错:
![](https://i-blog.csdnimg.cn/blog_migrate/3bd5e7d81f949b3b8509a938158327a9.png)
在D盘下可以看到导出的报错文件:
![](https://i-blog.csdnimg.cn/blog_migrate/0dafbb381841d2c354b2943192815758.png)
我们使用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):
![](https://i-blog.csdnimg.cn/blog_migrate/f169e59d7c9c3388f393a8b21108cc20.png)
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/051b613b7d6b23bdf44ff027e3b4d8bb.png)
这里报JVM初始化的时候发生异常,提示MaxMetaspaceSize最大mate空间太小。其实就是一次性加载的类太多,导致方法区(7的永久代或8的MetaSpace)内存泄漏。
转载请注明出处:https://blog.csdn.net/acmman/article/details/80462103