关于java:面试题JVM性能调优

4次阅读

共计 9651 个字符,预计需要花费 25 分钟才能阅读完成。

目录

前言
JVM 性能调优
内存溢出谬误
堆溢出谬误和预判堆溢出的谬误
虚拟机栈和本地办法栈溢出谬误
办法区(元数据区)和运行时常量池溢出
间接内存区域的溢出
实际案例
如何正确利用大内存 - 高性能硬件上的程序部署策略
如何排查内存溢出谬误
如何排查零碎 CPU 性能指标异样 - 外部命令导致系统迟缓
返回目录

前言
JVM 性能调优是一个很大的话题,很多中小企业的业务规模受限,没有迫切的性能调优需要,然而如果不晓得 JVM 相干的理论知识,写进去的代码或者配置的 JVM 参数不合理时,就会呈现很重大的性能问题,到时候开发就会像热锅上的蚂蚁,期待各方的烧灼。笔者始终在学习 JVM 相干的实践书籍,看过周志明老师的 深刻了解 Java 虚拟机,也学习过 葛鸣老师的 实战 Java 虚拟机,然而在理论工作中,只有过寥寥几次的调优教训,简直无处施展学习到的理论知识,以致常识大部分都存在在笔记和书本中,这次总结面试题,一是心愿可能应答性能调优岗位相干的面试;二是心愿总结一下具体的实战步骤,并致力排汇书中的实际案例,让本人的教训更丰盛一些。

返回目录

JVM 性能调优
返回目录

内存溢出谬误
学习目标:

通过异样信息及时定位到产生内存溢出的运行时数据区域
理解什么样的代码会导致内存溢出,避免写出这样的代码
出现异常后该如何解决,也就是学习事中的解决伎俩
内存溢出和内存泄露的区别

内存泄露:不该留存在过程中的内存数据,尽管很小,然而在通过屡次长期的积攒后,会导致内存溢出
内存溢出:程序申请内存时,内存不足的景象
返回目录

堆溢出谬误和预判堆溢出的谬误
如何复现出堆溢出谬误?

JVM 参数局部:最大堆和最小堆设置雷同并且设置的比拟小,比方只有 10M,这样就不会主动扩大堆
代码局部:在一个办法中一直地往汇合中退出元素
代码实际

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 产生。

案例心得:

堆内存溢出的辨认:java.lang.OutOfMemoryError: Java heap space 或者 java.lang.OutOfMemoryError: GC overhead limit exceeded
死循环中一直创建对象这种代码应该躲避
提前设置好主动转储的参数,出现异常可能复原现场查看问题
预先排查思路:先用 JvisualVM 这样的软件查看具体对象,核查是内存溢出还是内存透露,如果确定没有泄露,须要排查堆的参数设置是否正当,从代码上剖析对象存活时长比拟长是否必要,是否能够优化等等。
返回目录

虚拟机栈和本地办法栈溢出谬误
个别咱们会遇到两种栈相干的谬误:

单个线程中,一直的调用办法入栈,当栈深度超过虚拟机所容许的最大深度时,抛出 StackOverflowError
一直地创立线程,创立线程就须要创立栈,当无奈申请到足够的内存,就会报 unable to create new native thread 谬误
如何复现?

JVM 参数:-Xss128k,每个线程的栈内存大小
代码局部:没有进口的递归调用
代码实际

/**

-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 这个异样,这里就不尝试了,因为可能会导致操作系统假死等问题。

案例心得:

栈谬误的辨认:StackOverflowError 或者 java.lang.OutOfMemoryError: unable to create new native thread
没有进口的递归调用要防止;默认的 JVM 栈大小的参数针对个别的办法调用深度是足够的
如果必须要创立大量的常驻线程,并且是 32 位的虚拟机,要测试协调好 栈内存和其余内存的大小,防止出现溢出谬误
预先排查思路:先确定是哪种谬误,而后查看递归调用或者查看线程数
返回目录

办法区(元数据区)和运行时常量池溢出
办法区和运行时常量池异样
在 JDK1.6 以及以前的版本中,运行时常量池是放在办法区中的,咱们能够通过限度办法区的大小而后增大常量池来模仿溢出。

如何模仿:

JDK 应用 1.6 版本,这里留神,要对立 idea 所有的版本,否则出错具体细节能够参考这里:idea 启动时报 error:java 有效的源发行版 11
JVM 参数:–XX:PermSize=10M -XX:MaxPermSize=10M
利用代码:应用 String.intern 办法一直创立新的常量对象到常量池中,并始终用汇合放弃强援用
代码实际:

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 之后,元数据区被放在了间接内存中,能够指定上面的参数来模仿溢出状况

JVM 参数:-XX:MetaspaceSize=10M-XX:MaxMetaspaceSize=10M-XX:+HeapDumpOnOutOfMemoryError
代码:通过应用 cglib 生成大量的动静类
代码实战:

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

案例心得
元数据区和办法区谬误的辨认:java.lang.OutOfMemoryError: Metaspace;java.lang.OutOfMemoryError: PermGen space
元数据区的溢出个别和框架代码或者本地代码中大量创立动静类无关
核查问题时,也是依据具体的问题剖析是哪个动静类被大量创立,是否有必要,是否须要调整办法区的大小。
返回目录

间接内存区域的溢出
间接内存区域,如果内存达到设置的 MaxDirectMemorySize 后,就会触发垃圾回收,如果垃圾回收不能无效回收内存,也会引起 OOM 溢出。

如何复现?

JVM 参数:-XX:MaxDirectMemorySize,如果不指定,和 -Xmx 指定的最大堆一样
代码局部:应用 unsafe 一直的调配间接内存
代码实战

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)

案例心得:

间接内存溢出的辨认:Exception in thread “main” java.lang.OutOfMemoryError,并且 dump 出的堆栈文件不大
核查问题时,依据异样堆栈查看引发 error 的代码,个别都是 NIO 代码引起的。
返回目录

实际案例
返回目录

如何正确利用大内存 - 高性能硬件上的程序部署策略
高性能硬件程序部署次要有两种形式:

通过 64 位 JDK 应用来大内存
应用若干个 32 位的虚拟机建设逻辑集群以利用硬件资源
如果程序是对响应工夫敏感的零碎,想配置大堆的前提是,要保障利用的 Full GC 频率足够低,不会影响用户的应用,比方,能够设置深夜定时工作触发 full-gc 甚至主动重启应用服务器来保障可用空间在一个稳固的程度。管制 Full GC 频率的要害就是保障大多数对象都是朝生夕灭,不会短时间有大量对象进入老年代,引起频繁的 FullGC。

不仅如此,还须要思考大堆带来的其余问题:

内存回收工夫变长
如果呈现 OOM,因为堆过大,简直无奈剖析 dump 文件
64 位的 JDK 性能测试后果要弱于 32 位的 JDK,并且占用内存也较大
所以倡议,如非必要,尽可能应用第二种形式来部署以充分利用高性能硬件资源。

第二种形式就是集群部署形式,采纳集群部署就须要思考额定的问题,比方,如何保留用户的状态,个别有两种解决方案:

亲和式集群:同一个用户都转发到同一个服务器去解决
长处:实现简略,只须要在 apache 等负载均衡器中配置即可;网络流量较少,客户端只须要传递 sessionID 即可
毛病:用户和对应服务器绑定,一旦服务器宕机,用户的 session 状态即隐没
apache 中这样配置:
worker.controller.sticky_session=true|false true 为亲和式集群 worker.controller.sticky_session_force=true|false false 为当服务器不可用,转发到其余服务器
共享 session:集群内服务器共享 session
长处:服务器宕机,用户也不会失落 session 状态
毛病:在零碎中引入了新的组件,进步了零碎的复杂度,实现复杂度
针对第二种形式,和第一种形式比照,也有本人的毛病,咱们在设计零碎机构时也须要思考到:

同一台物理机器的上的资源竞争(并发写),首先会想到能够应用同步机制,能够学习锁设计中的分段锁和间隙锁,通过锁一部分来进步并发度;或者通过乐观锁的设计,一直循环更新直到胜利;还能够思考建设热拜访资源,提前把一部分资源缓存到集中缓存中,通过集中式缓存缩小磁盘 IO 竞争。
冗余的本地内存,能够思考应用集中式内存数据库解决
资源池节约,能够思考应用 JNDI(对立命名服务,我感觉和 Springcloud 中的对立配置核心核心思想是统一的,都是把配置文件对立放在一个中央,便于援用保护),然而也会带来新的复杂度
总结:

高性能硬件的部署策略有两种,思考到 GC 工夫过长,堆转出日志无奈剖析等毛病,尽量抉择多实例部署的逻辑集群形式
逻辑集群的部署形式要思考 状态放弃、资源竞争和资源冗余等状况,依据具体业务场景灵便利用。
返回目录

如何排查内存溢出谬误
堆外内存溢出个别次要来源于操作系统对过程的内存限度 和 堆外内存回收的机制。

针对操作系统堆过程的内存限度。比方: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 调用,问题解决。

案例播种:

cpu 等零碎指标异样,个别都是来源于利用代码的某些操作,须要仔细检查代码中会导致系统调用的局部,采纳其余代替形式实现。
牛牛科技,心愿能帮忙到大家!

正文完
 0