赞
踩
对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。
1.Full GC
会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。
2.导致Full GC的原因
1)年老代(Tenured)被写满
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。
2)持久代Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例
3)System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节,下面详细介绍对应JVM调优的方法和步骤。
1.监控GC的状态
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。
举一个例子: 系统崩溃前的一些现象:
之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
2.生成堆的dump文件
通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。
3.分析dump文件
打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件:
备注:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。
4.分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
注:如果满足下面的指标,则一般不需要进行GC:
5.调整GC类型和内存分配
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。
6.不断的分析和调整
通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。
cms参数优化步流程
下面我再继续介绍下JVM的关键参数配置(仅用于参考)。
1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;
2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。
比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。
3.年轻代和年老代设置多大才算合理
1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。
在抉择时应该根 据以下两点:
(1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。
(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。
4.在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC 。
5.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。
理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。
目录
JVM性能调优是一个很大的话题,很多中小企业的业务规模受限,没有迫切的性能调优需求,但是如果不知道JVM相关的理论知识,写出来的代码或者配置的JVM参数不合理时,就会出现很严重的性能问题,到时候开发就会像热锅上的蚂蚁,等待各方的炙烤。笔者一直在学习JVM相关的理论书籍,看过周志明老师的 深入理解Java虚拟机,也学习过 葛鸣老师的 实战Java虚拟机 ,但是在实际工作中,只有过寥寥几次的调优经验,几乎无处施展学习到的理论知识,致使知识大部分都存在在笔记和书本中,这次总结面试题,一是希望能够应对性能调优岗位相关的面试;二是希望总结一下具体的实战步骤,并努力吸收书中的实践案例,让自己的经验更丰富一些。
学习目的:
内存溢出和内存泄露的区别
如何复现出堆溢出错误?
代码实践
- package org.example;
-
- import java.util.ArrayList;
- import java.util.List;
-
- /**
- * -Xmx10M -Xms10M -XX:+HeapDumpOnOutOfMemoryError
- */
- public class App {
- static class OOMObject {
- int a = 1;
- long b = 2;
- float c = 2.1f;
- }
-
- public static void main(String[] args) {
- List<OOMObject> list = new ArrayList<>();
- while (true) {
- list.add(new OOMObject());
- }
- }
- }
正确的出现了我们想要的结果:
- java.lang.OutOfMemoryError: Java heap space
- Dumping heap to java_pid24476.hprof ...
- Heap dump file created [13268403 bytes in 0.077 secs]
- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- at java.util.Arrays.copyOf(Arrays.java:3210)
- at java.util.Arrays.copyOf(Arrays.java:3181)
- at java.util.ArrayList.grow(ArrayList.java:265)
- at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
- at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
- at java.util.ArrayList.add(ArrayList.java:462)
- at org.example.App.main(App.java:22)
-
- Process finished with exit code 1
如果把参数调大,调整20M,那么会报另外的error
- java.lang.OutOfMemoryError: GC overhead limit exceeded
- Dumping heap to java_pid8796.hprof ...
- Heap dump file created [27391983 bytes in 0.141 secs]
- Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
- at org.example.App.main(App.java:19)
-
- Process finished with exit code 1
这个错误的原因是,JVMGC时间占据了整个运行时间的98%,但是回收只得到了2%可用的内存,至少出现5次,就会报这个异常。
这个异常是Jdk1.6定义的策略, 通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。
案例心得:
一般我们会遇到两种栈相关的错误:
如何复现?
代码实践
- /**
- * -Xss128k
- */
- public class App {
- static int length = 0;
-
- private static void reverse() {
- length++;
- reverse();
- }
-
- public static void main(String[] args) {
- try {
- reverse();
- } catch (Throwable e) {
- System.out.println("length:" + length);
- throw e;
- }
- }
- }
结果验证:
- length:1096
- Exception in thread "main" java.lang.StackOverflowError
- at org.example.App.reverse(App.java:10)
- at org.example.App.reverse(App.java:11)
- at org.example.App.reverse(App.java:11)
- at org.example.App.reverse(App.java:11)
- 太多了,这里只截取部分
关于unable to create new native thread这个异常,这里就不尝试了,因为可能会导致操作系统假死等问题。
案例心得:
方法区和运行时常量池异常
在JDK1.6以及以前的版本中,运行时常量池是放在方法区中的,我们可以通过限制方法区的大小然后增大常量池来模拟溢出。
如何模拟:
代码实践:
- package org.example;
-
- import java.util.ArrayList;
- import java.util.List;
-
-
- public class App {
-
- public static void main(String[] args) {
- int i = 0;
- List list = new ArrayList();
- while (true) {
- list.add(String.valueOf(i++).intern());
- }
- }
- }
结果:
- Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
- at java.lang.String.intern(Native Method)
- at org.example.App.main(App.java from InputFileObject:15)
-
- Process finished with exit code 1
在JDK1.7以后,常量池就被移动到了堆中,所以如果限制了堆的大小,那么最终会报堆溢出异常或者预判堆异常的错误的。
同样的代码使用JDK1.8版本测试,并指定了堆的最大和初始大小后,果然出现了我预计的异常。
参数:-XX:PermSize=10M -XX:MaxPermSize=10M -Xmx10M -Xms10M
- Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
- at java.lang.Integer.toString(Integer.java:403)
- at java.lang.String.valueOf(String.java:3099)
- at org.example.App.main(App.java:13)
如果加上不使用 预判断限制参数 -XX:-UseGCOverheadLimit,就会直接报堆溢出异常
-XX:PermSize=10M -XX:MaxPermSize=10M -Xmx10M -Xms10M -XX:-UseGCOverheadLimit
- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- at java.lang.Integer.toString(Integer.java:401)
- at java.lang.String.valueOf(String.java:3099)
- at org.example.App.main(App.java:13)
说明,常量池分配在堆中。
元数据区异常
JDK1.8之后,元数据区被放在了直接内存中,可以指定下面的参数来模拟溢出情况
代码实战:
pom文件中添加cglib的引用
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>3.2.4</version>
- </dependency>
- package org.example;
-
- import net.sf.cglib.proxy.Enhancer;
- import net.sf.cglib.proxy.MethodInterceptor;
- import net.sf.cglib.proxy.MethodProxy;
-
- import java.lang.reflect.Method;
-
-
- public class App {
-
- public static void main(String[] args) {
- while (true) {
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(OOMObject.class);
- enhancer.setUseCache(false);
- enhancer.setCallback(new MethodInterceptor() {
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
- return methodProxy.invokeSuper(o, args);
- }
- });
- enhancer.create();
- }
- }
-
- static class OOMObject {
-
- }
- }
-
运行结果:
- java.lang.OutOfMemoryError: Metaspace
- Dumping heap to java_pid26272.hprof ...
- Heap dump file created [3395669 bytes in 0.015 secs]
- Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
- at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
- at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
- at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
- at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
- at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
- at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
- at org.example.App.main(App.java:23)
- Caused by: java.lang.reflect.InvocationTargetException
- at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
- at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
- at java.lang.reflect.Method.invoke(Method.java:498)
- at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:413)
- at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
- ... 6 more
- Caused by: java.lang.OutOfMemoryError: Metaspace
- at java.lang.ClassLoader.defineClass1(Native Method)
- at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
- ... 11 more
案例心得
直接内存区域,如果内存达到设置的MaxDirectMemorySize后,就会触发垃圾回收,如果垃圾回收不能有效回收内存,也会引起OOM溢出。
如何复现?
代码实战
- package org.example;
-
- import sun.misc.Unsafe;
-
- import java.lang.reflect.Field;
-
-
- public class App {
-
- public static void main(String[] args) throws IllegalAccessException {
- Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];
- unsafeFiled.setAccessible(true);
- Unsafe unsafe = (Unsafe) unsafeFiled.get(null);
- while (true) {
- unsafe.allocateMemory(1024 * 1024);
- }
- }
-
- }
-
运行结果
- Exception in thread "main" java.lang.OutOfMemoryError
- at sun.misc.Unsafe.allocateMemory(Native Method)
- at org.example.App.main(App.java:15)
案例心得:
高性能硬件程序部署主要有两种方式:
如果程序是对响应时间敏感的系统,想配置大堆的前提是,要保证应用的Full GC频率足够低,不会影响用户的使用,比如,可以设置深夜定时任务触发full-gc甚至自动重启应用服务器来保证可用空间在一个稳定的水平。控制Full GC频率的关键就是保证大多数对象都是朝生夕灭,不会短时间有大量对象进入老年代,引起频繁的FullGC。
不仅如此,还需要考虑大堆带来的其他问题:
所以建议,如非必要,尽可能使用第二种方式来部署以充分利用高性能硬件资源。
第二种方式就是集群部署方式,采用集群部署就需要考虑额外的问题,比如,如何保留用户的状态,一般有两种解决方案:
亲和式集群:同一个用户都转发到同一个服务器去处理
优点:实现简单,只需要在apache等负载均衡器中配置即可;网络流量较少,客户端只需要传递sessionID即可
缺点:用户和对应服务器绑定,一旦服务器宕机,用户的session状态即消失
apache中这样配置:
- worker.controller.sticky_session=true|false true为亲和式集群
- worker.controller.sticky_session_force=true|false false为当服务器不可用,转发到其他服务器
共享session:集群内服务器共享session
针对第二种方式,和第一种方式对比,也有自己的缺点,我们在设计系统机构时也需要考虑到:
总结:
堆外内存溢出一般主要来源于操作系统对进程的内存限制 和 堆外内存回收的机制。
针对操作系统堆进程的内存限制。比如:32位的windows操作系统对进程的限制为2G,如果堆等其他区域划分的内存过大,那么留给直接内存区域的内存就非常小了。
针对堆外内存的回收机制。堆外内存需要等到满了之后,再在代码中触发System.gc来回收,如果服务器开启-XX:+DisableExplicitGC参数开关,那么就不会响应这次垃圾回收的请求。
总结:
因为限制以及其他区域不合理的参数配置,直接内存区域只有很小的一块内存;并且垃圾回收需要依靠手动触发System.gc来回收无法保证回收的可靠性,所以溢出就是必然的了。
我这里又查阅了之前看过印象深刻的一个关于美团使用网络框架的一个堆外内存泄漏bug。这里给大家简单介绍下,原文详见这里:Netty堆外内存泄露排查盛宴
首先作者通过nginx不断报5XX异常发现服务不可用,然后核查jvm发现频繁的fullgc导致用户线程阻塞(其实就是netty的nio线程),最后查出是log4j2在某个时点大量频繁的打印堆外内存不足的error日志导致的,所以这个问题的核心在于排查堆外内存为何泄漏。
排查的步骤首先是基于异常的堆栈日志,找到对应的代码,用反射机制每隔N秒观察堆外内存的大小,发现了堆外内存增长的规律。然后猜测原因,模拟测试查看是否可以复现问题,成功复现问题后,就能大约找到出现问题的代码,继续通过debug查找根源代码处,最终通过修改代码,重新build后最终解决问题。
我个人认为这个问题解决的关键在于开发者能够读懂框架自己使用变量统计堆外内存,然后得以跟踪这个变量最终解决问题。我们在排查问题的时候如果也可以多想一些,多去琢磨框架报出异常的原因,也许就能找到解决问题的办法。
案例介绍:在做压力测试时发现系统的CPU指标异常,大量的时间被系统调用fork占用,最后核查代码发现这个fork系统调用是在每一个请求来临时,都会调用以获取系统相关的信息的,具体是使用Runtime.getRuntime().exec()来执行一段shell脚本,最后修改为通过java api调用,问题解决。
案例收获:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。