本文摘自公司牛人分享的ppt。
堆内存的分配和回收步骤
【一些基础知识】
-Xms:为jvm启动时分配的内存,比如-Xms200m,表示分配200M。(一般该值设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。-Xms设置的内存,不包含持久代,只包含年轻代和年老代)
-Xmx:为jvm运行过程中分配的最大内存,比如-Xms500m,表示jvm进程最多只能够占用500M内存。
-Xss:为jvm启动的每个线程分配的内存大小, JDK1.4中该值默认是256K,JDK1.5+中是1M。(在相同物理内存下,减小这个值能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制的)
操作系统的线程是一种资源,数量是有限制的,32位操作系统分配给一个进程的用户内存是2G,系统本身使用2G,所以jvm允许创建线程的最大值应该是 小于2G/256K=8000 (JDK1.4默认的Xss的值为256k),在linux服务器上测试发现默认允许创建7409个线程 (-Xss的值会决定能创建多少线程),如果设置-Xss1M,则最大可以创建1896个线程,超过这些线程数量的极限值,则抛出:java.lang.OutOfMemoryError: unable to create new nativethread at java.lang.Thread.start0(Native Method)。(注:线程本身消耗的内存是os级别的内存,而非进程的用户内存)
堆内存的结构
jvm的堆分为3个部分,yong、old和perm。其中young和Old区是我们重点关注的。
1)young(分为一个Eden区和两个survivor区,用s0、s1表示)
所有对象的创建都是在Eden区完成的,Eden满了之后会进行minorGC,将不能回收的对象放入到survivor区。
young区通过-XX:NewRatio=n(表示年轻代和年老代的比值为1:n)或者-XX:NewSize=n或者–Xmn设置
survivor区通过-XX:SurvivorRatio设置
2)Old
survivor区满了之后,或者对象已经足够的老,则放入Old区,这个行为也是由minorGC触发。old空间不足时,则进行FullGC。
3)Perm
主要存放类的一些数据,类的频繁创建会导致Perm OOM,Perm区占用的内存空间,不属于-Xms的设置的空间。类的频繁创建(动态创建的类),会导致持久度空间不够,从而触发FullGC。FullGC频繁,会影响服务器的性能。
【堆内存的分配和回收步骤】
1、对象在Eden区完成内存分配。
2、当Eden区满了,再创建对象时,会因为申请不到空间,触发minorGC,进行young(eden+1个survivor)区的垃圾回收。
3、minorGC时,Eden不能被回收的对象被放入到空的survivor区(Eden肯定会被清空),另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的。
4、当做第3步的时候,如果发现survivor区空间不够了,则这些对象被copy到old区,或者survivor区空间足够,但是有些对象已经足够Old,也被放入Old区(XX:MaxTenuringThreshold,MaxTenuringThreshold这个参数用于控制对象能经历多少次Minor GC才晋升到旧生代,默认值是15,如果设置为0,则直接进入Old区)
5、当Old区被放满之后,进行FullGC。
【堆内存分配的例子】
下面用例子来说明jvm堆内存的分配和回收,例子使用的代码片段如下:
memory.jsp,这个jsp页面主要包含2个功能:1、申请指定大小的内存 2、线程等待指定的时间
<%
// 申请内存
%>
<%
longstart = System.currentTimeMillis();
String msize= request.getParameter("m");
intm= Integer.parseInt(msize);
// 申请内存
intsize= 1024 * 1024 * m; // 1M byte
byte[]allocate = new byte[size];
%>
<%
//Thread sleep
%>
<%
Stringsleep = request.getParameter("s");
ints= Integer.parseInt(sleep);
Thread.sleep(s);
%>
<%
// 使用申请的内存
out.println(allocate);
%>
<%
// 输出耗时
longend = System.currentTimeMillis();
out.println("elapsed:");
out.println(end- start);
out.println("ms<br>");
%>
【例子1】:单线程情况下,eden区的内存大小>线程每次申请的内存大小
jvm的配置:JAVA_OPTS=-Xms256m -Xmx256m -Xss128k-XX:MaxTenuringThreshold=15
堆内存分布:Eden 15.812M S0S1 1.938M Old 236.312M
url请求:http://localhost/perf/memory.jsp?m=15&s=10
申请15M内存,10ms sleep,1个线程
单线程情况下,堆内存分配,回收的步骤:
1、A线程在Eden申请了15M内存,A线程需要10ms左右的时间才能释放15M内存
2、10ms之后,A线程执行结束,这个时候新的请求进来,接下去可能是A线程也可能是其他线程接收这个请求,同样需要在Eden申请15M内存
3、由于Eden区内存不够,因为只有0.812M空闲内存,所以触发minorGC,由于Eden区的先前A线程申请的15M内存已经没有引用,所以全部收回,几乎没有数据进入survivor区,所以也几乎没有数据进入Old区。
4、Eden又空出15.812M内存空间,给新的A线程分配15M内存
5、新的A线程内存分配完成,Eden区又只剩下0.812M的空闲内存,重复1~5的过程
....
结果:无Full GC,内存分配,回收正常(同时也说明单线程情况下,例子所使用的代码,内存是不可能成为瓶颈的)
【例子2】:2个并发线程
jvm的配置:JAVA_OPTS=-Xms256m -Xmx256m -Xss128k-XX:MaxTenuringThreshold=15
堆内存分布:Eden 15.812M S0S11.938M Old 236.312M
url请求:http://localhost/perf/memory.jsp?m=15&s=10
2个线程并发的时候,堆内存分配,回收步骤:
1、A线程在Eden申请了15M内存,A线程需要10ms左右的时间才能释放15M内存
2、B线程此时也申请了15M内存,因为Eden区总共只有15.812M内存,除去被A申请的15M,所以只剩下0.812M
3、由于Eden区内存不够,触发minorGC,由于此时A线程并没有执行完成,那个15M内存不能被回收。所以将A线程的15M内存copy到其中一个Survivor区
4、由于Survivor区只有1.938M空间,放不下A线程的15M内存,则又将A线程的15M内存直接拷贝到Old区。(如果这个时候survivor区的空间太小,则会直接被放到Old区)
5、拷贝完成之后,Eden又有了15.812M内存空间
6、B线程的15M内存在Eden完成了分配
7、重复执行以上步骤,不停有15M的内存被放入Old区
结果:Full GC频繁 (同时也说明了一旦到了某个临界值,则Old区再大也是杯水车薪)
【例子3】:2个并发线程,线程占有内存调整到1M
堆内存分布:Eden 15.812M S0S11.938M Old 236.312M
url请求:http://localhost/perf/memory.jsp?m=1&s=10
2个线程并发的时候,堆内存分配,回收步骤:
1、A线程在Eden申请了1M内存,B线程也申请了1M内存,持续轮流申请,当第8个B线程开始申请1M内存的时候,发现Eden没有足够的空间,此时Eden已被占用了15M
2、触发minorGC,首先所有之前B线程申请的内存被全部回收,前7个A线程申请的内存也全部被回收,由于每个线程的执行时间是10ms,所以第8个A线程没有执行完成的概率非常大,其申请的内存无法被回收。
3、A线程的1M内存由于不能被回收,被copy放入survivor区,survivor区空间是1.938M,所以没有问题
5、重复执行以上步骤,当再次触发minorGC的时候,survivor区的之前的A线程留下的1M内存也会被这次gc所回收
6、没有发生往Old区copy数据的事件
....
结果:无Full GC,则内存分配,回收正常
【例子4】:2个并发线程,线程占有内存调整到2M
堆内存分布:Eden 15.812M S0S11.938M Old 236.312M
url请求:http://localhost/perf/memory.jsp?m=2&s=10
2个线程并发的时候,堆内存分配,回收步骤:
1、A线程在Eden申请了2M内存,B线程也申请了2M内存,持续重复之前的申请,当第4个B线程开始申请2M内存的时候,发现Eden没有足够的空间,此时Eden已被占用了14M
2、触发minorGC,首先所有之前B线程申请的内存被全部回收,前3个A线程申请的内存也全部被回收,由于每个线程的执行时间是10ms,所以第4个A线程没有执行完成的概率非常大,其申请的内存无法被回收
3、A线程的2M内存由于不能被回收,被copy放入survivor区,survivor区空间是1.938M,内存不够,直接被放入Old区
5、重复执行以上步骤,不停有2M的内存被放入Old区
....
结果:Full GC频繁
【总结】
1、eden区太小,则容易导致minorGC频繁(minorGC的触发条件,当eden区申请空间失败,则进行minorGC)
2、survivor太小,则非常容易导致对象被直接copy到old区(survivor只存放eden区无法被回收的对象,并不能直接说明这些对象相对较老,很多刚刚创建的对象也可能被直接拷贝进来)
3、young区太大,则容易导致一次minorGC 耗时。GC的时候,jvm是不允许内存分配的,所以GC时间越短越好
4、一般建议young区为整个堆的1/4,如果堆为2g,则young区分配500M。sun推荐的配置,survivor区一般设置为young区的1/8,如果young区为500M,则survivor可以设置为60M。如果一个线程占用的内存为2M,则60M的survivor 支持30个并发线程是肯定可以的。
5、survivor区的大小、线程平均占用内存大小、与jvm所能支持的并发线程数量,存在一定的关系。最保险的公式:线程平均占用内存=(survivor区大小/并发线程数量)。在满足这个公式条件下,理论上不会有对象被迁移到Old区。但是实际情况,不需要这么保守,原因有2:
1)、一般来说,线程就算其还没有执行完成,但是也是有大部分临时对象是可以被回收的,只有少部生命周期比较长的临时对象才不能被回收;
2)、实际情况是允许对象迁移到Old区的。
所以上面的最保险公式,还可以再乘以一个系数,得到新的公式:
线程平均占用内存=(survivor区大小/并发线程数量) *系数
实际情况还是通过不断增加用户进行性能压测,获取gc日志来分析内存分配回收是否合理为准。同时不断调整jvm参数,以达到最佳情况;jvm调优应该是以减少GC的时间和系统停顿时间为目的而进行堆内存的各个区段的分配,以及GC策略的调整。