java后端面试题必问的-JVM性能调优

目录

前言

JVM性能调优是一个很大的话题,很多中小企业的业务规模受限,没有迫切的性能调优需求,但是如果不知道JVM相关的理论知识,写出来的代码或者配置的JVM参数不合理时,就会出现很严重的性能问题,到时候开发就会像热锅上的蚂蚁,等待各方的炙烤。笔者一直在学习JVM相关的理论书籍,看过周志明老师的 深入理解Java虚拟机,也学习过 葛鸣老师的 实战Java虚拟机 ,但是在实际工作中,只有过寥寥几次的调优经验,几乎无处施展学习到的理论知识,致使知识大部分都存在在笔记和书本中,这次总结面试题,一是希望能够应对性能调优岗位相关的面试;二是希望总结一下具体的实战步骤,并努力吸收书中的实践案例,让自己的经验更丰富一些。

JVM性能调优

内存溢出错误

学习目的:

内存溢出和内存泄露的区别

堆溢出错误和预判堆溢出的错误

如何复现出堆溢出错误?

代码实践

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 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的引用


        cglib
        cglib
        3.2.4

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。

不仅如此,还需要考虑大堆带来的其他问题:

所以建议,如非必要,尽可能使用第二种方式来部署以充分利用高性能硬件资源

第二种方式就是集群部署方式,采用集群部署就需要考虑额外的问题,比如,如何保留用户的状态,一般有两种解决方案:

针对第二种方式,和第一种方式对比,也有自己的缺点,我们在设计系统机构时也需要考虑到:

总结:

如何排查内存溢出错误

堆外内存溢出一般主要来源于操作系统对进程的内存限制 和 堆外内存回收的机制。

针对操作系统堆进程的内存限制。比如:32位的windows操作系统对进程的限制为2G,如果堆等其他区域划分的内存过大,那么留给直接内存区域的内存就非常小了。

针对堆外内存的回收机制。堆外内存需要等到满了之后,再在代码中触发System.gc来回收,如果服务器开启-XX:+DisableExplicitGC参数开关,那么就不会响应这次垃圾回收的请求。

总结:

因为限制以及其他区域不合理的参数配置,直接内存区域只有很小的一块内存;并且垃圾回收需要依靠手动触发System.gc来回收无法保证回收的可靠性,所以溢出就是必然的了。

我这里又查阅了之前看过印象深刻的一个关于美团使用网络框架的一个堆外内存泄漏bug。这里给大家简单介绍下,原文详见这里:Netty堆外内存泄露排查盛宴

首先作者通过nginx不断报5XX异常发现服务不可用,然后核查jvm发现频繁的fullgc导致用户线程阻塞(其实就是netty的nio线程),最后查出是log4j2在某个时点大量频繁的打印堆外内存不足的error日志导致的,所以这个问题的核心在于排查堆外内存为何泄漏

排查的步骤首先是基于异常的堆栈日志,找到对应的代码,用反射机制每隔N秒观察堆外内存的大小,发现了堆外内存增长的规律。然后猜测原因,模拟测试查看是否可以复现问题,成功复现问题后,就能大约找到出现问题的代码,继续通过debug查找根源代码处,最终通过修改代码,重新build后最终解决问题。

我个人认为这个问题解决的关键在于开发者能够读懂框架自己使用变量统计堆外内存,然后得以跟踪这个变量最终解决问题。我们在排查问题的时候如果也可以多想一些,多去琢磨框架报出异常的原因,也许就能找到解决问题的办法。

如何排查系统CPU性能指标异常-外部命令导致系统缓慢

案例介绍:在做压力测试时发现系统的CPU指标异常,大量的时间被系统调用fork占用,最后核查代码发现这个fork系统调用是在每一个请求来临时,都会调用以获取系统相关的信息的,具体是使用Runtime.getRuntime().exec()来执行一段shell脚本,最后修改为通过java api调用,问题解决。

案例收获:

java后端面试题必问的-JVM性能调优

最后给大家分享Spring系列的学习笔记和面试题,包含spring面试题、spring cloud面试题、spring boot面试题、spring教程笔记、spring boot教程笔记、最新阿里巴巴开发手册(63页PDF总结)、2022年Java面试手册。一共整理了1184页PDF文档。私信博主(777)领取,祝大家更上一层楼!!!

java后端面试题必问的-JVM性能调优















原文作者:Ging

原文出处:https://www.cnblogs.com/ging/p/13853741.html

展开阅读全文

页面更新:2024-03-07

标签:会报   集群   线程   异常   内存   性能   错误   参数   代码   方法   系统

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top