JAVA 内存泄漏-JVM调优-代码性能优化

247 阅读7分钟

SpringBoot 服务进行[性能优化]之前,需要做一些准备,把 SpringBoot 服务的一些数据暴露出来。 比如,需要把缓存命中率收集;数据库连接池的参数收集,通常需要用到JVM 的分析和诊断工具,比如:arthus、Jprofiler、jvisualvm、jmap\jstat

分析工具:JDK

arthas: 开源的 Java 诊断工具

Alibaba 在 2018 年 9 月开源的 Java 诊断工具。

**快速退出某个命令**:Q或者Ctrl+C  
**退出Arthas:**  exit或者quit, 退出当前session,Arthas server还在目标进程中运行。  
**彻底退出**: stop. 用完一定要stop哦,避免Arthas server依然运行占用系统资源。

  • 下载地址 及启动
curl -O https://alibaba.github.io/arthas/arthas-boot.jar

等价于:wget https://alibaba.github.io/arthas/arthas-boot.jar
启动 
java -jar arthas-boot.jar 

全局视角查看系统运行状况 Dashboard

image.png

image.png

CPU飙升原因定位

内存泄漏原因定位

直接用heapdump命令:heapdump --live /root/jvm.hprof 把内存快照dump出来,作用和jmap工具一样(jmap -dump:live)

多线程问题(死锁,阻塞) thread

通过thread加线程id输出该线程的栈信息 thread -n 3 查看CPU使用率top n线程的栈: thread -b 找出当前阻塞其他线程的线程

//线程
thread -n 5


jvm



[tomcat@iZ2zeb4tt2ppjt9ok0779rZ soft]$ jstat -gcutil 1 1000 5
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
 59.80   0.00  42.26  39.52  96.39  93.89     92    3.766     5    3.758    7.524
 59.80   0.00  42.70  39.52  96.39  93.89     92    3.766     5    3.758    7.524
 59.80   0.00  43.11  39.52  96.39  93.89     92    3.766     5    3.758    7.524
 59.80   0.00  43.13  39.52  96.39  93.89     92    3.766     5    3.758    7.524
 59.80   0.00  43.53  39.52  96.39  93.89     92    3.766     5    3.758    7.524
[tomcat@iZ2zeb4tt2ppjt9ok0779rZ soft]$ jstack 1 >./dump01

[tomcat@iZ2zeb4tt2ppjt9ok0779rZ soft]$ jmap -heap 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 2 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2147483648 (2048.0MB)
   NewSize                  = 178782208 (170.5MB)
   MaxNewSize               = 715653120 (682.5MB)
   OldSize                  = 358088704 (341.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 585105408 (558.0MB)
   used     = 314888272 (300.3008575439453MB)
   free     = 270217136 (257.6991424560547MB)
   53.8173579827859% used
From Space:
   capacity = 63438848 (60.5MB)
   used     = 37695096 (35.94884490966797MB)
   free     = 25743752 (24.55115509033203MB)
   59.41957836308755% used
To Space:
   capacity = 62914560 (60.0MB)
   used     = 0 (0.0MB)
   free     = 62914560 (60.0MB)
   0.0% used
PS Old Generation
   capacity = 1046478848 (998.0MB)
   used     = 413602744 (394.4423141479492MB)
   free     = 632876104 (603.5576858520508MB)
   39.523277970736395% used


程序运行耗时监测 (没记日志的情况下:定位哪里运行时长最长) trace命令

 trace com.xxxx.controller.DubboCaseController getList  '#cost > 2'
 
[arthas@1]$ trace com.better517na.cLReportService.controller.SettlementPublicController  [arthas@1]$ trace com.better517na.cLReportService.controller.SettlementPublicController queryPersonConsumptionsFromESApply '#cost > 2' 

image.png

无法线上DEBUG,缺少日志情况下的问题定位

  • trace命令
  • watch命令:查看指定方法的调用情况

watch com.xxxx.xxxxController update “{params,returnObj}” -x 3 -b -s,查看xxxxController的update方法的返回值:

-x 3是指定输出结果的属性遍历深度,默认为 1
-b方法调用前观察,用于返回方法入参
-s方法调用后观察,用于返回方法返回值

异常

  • monitor命令:监控方法的执行情况

包括:成功次数、失败次数、平均响应时间、失败率
monitor -c 10 com.xxxx.xxxxController update

  • TimeTunnel 记录下方法执行数据的时空隧道

tt -i 1003 -p 表示重做Index为1003的那次调用

  • stack命令:监控方法的被执行的路径

stack 命令, 主要用于监控方法被谁调用了: stack com.xxxx.xxxxController list

  • sm命令:能搜索出所有已经加载了 Class 信息的方法信息

sm -d com.xxxx.xxxxController

  • jad命令:反编译指定已加载类的源码

image.png 反编译源码到指定文件: jad --source-only com.xxxx.xxxxController > /tmp/xxxxController.java

  • logger命令:实现动态更新logger level

使用sc命令查看你需要改变的类信息,关注classLoaderHash

sc -d com.xxxx.xxxxController logger -c 70ac4376 查看当前的日志级别

image.png

将日志级别改为info: logger -c 70ac4376 --name com.xxxx.xxxx --level info

Jprofiler 性能分析神器JProfiler

CPU分析

内存分析

image.png

img_v2_d05fc473-4961-4fa3-908f-16e38f5d0beg.jpg

image.png

线程分析

I/O分析

生成快照

image.png

image.png

jvisualvm : JAVA 自带诊断工具

概念

JVisualVM是一个集成了命令行JDK工具和轻量级分析功能的可视化工具,专为开发和生产环境使用而设计。 官方地址:VisualVM.github.io/ JVisualVM已作为Java JVisualVM在Oracle JDK 6~8中分发。它已在Oracle JDK9中停止。 独立的VisualVM可以从官网下载,可以在任何兼容的JDK版本上运行。

VisualVM 是[Netbeans]的profile子项目,已在JDK6.0 update 7 中自带,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。

VisualVM 提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序的详细信息。 VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。 您可以查看本地应用程序或远程主机上运行的应用程序的相关数据

VisualVM非常多的其它功能,可以分析dump的内存快照,

dump出来的线程快照并且进行分析等,还有其它很多的插件大家可以去探索

image.png

image.png

image.png

jmap.exe

jmap命令是Jdk自带的一个,查看jvm内存使用详情的命令

jmap -histo 17 | head -n 30

jmap -histo 17 | sort -k 2 -g -r

当前进程中对象的大小及个数,辅助进行分析

jmap -histo pid|head -n 10 查看前10位

jmap -histo pid | sort -k 2 -g -r 查看对象数最多的对象,按降序输出

jmap -histo pid | sort -k 3 -g -r 查看内存的对象,按降序输出

$ jmap --help
Usage:
    jmap [option] <pid>
        (to connect to running process)
    jmap [option] <executable <core>
        (to connect to a core file)
    jmap [option] [server_id@]<remote server IP or hostname>
        (to connect to remote debug server)

where <option> is one of:
    <none>               to print same info as Solaris pmap
    -heap                to print java heap summary
    -histo[:live]        to print histogram of java object heap; if the "live"
                         suboption is specified, only count live objects
    -clstats             to print class loader statistics
    -finalizerinfo       to print information on objects awaiting finalization
    -dump:<dump-options> to dump java heap in hprof binary format
                         dump-options:
                           live         dump only live objects; if not specified,
                                        all objects in the heap are dumped.
                           format=b     binary format
                           file=<file>  dump heap to <file>
                         Example: jmap -dump:live,format=b,file=heap.bin <pid>
    -F                   force. Use with -dump:<dump-options> <pid> or -histo
                         to force a heap dump or histogram when <pid> does not
                         respond. The "live" suboption is not supported
                         in this mode.
    -h | -help           to print this help message
    -J<flag>             to pass <flag> directly to the runtime system

histogram 英 / ˈhɪstəɡræm 美 / [ˈhɪstəɡræm] n.(统计学的)直方图,矩形图

jstat

jstat -gcutil 1 3000 10

jstat -gcutil 垃圾收集统计信息摘要

  • 列名 描述

  • S0 幸存区Survior S0利用率占空间当前容量的百分比

  • S1 幸存区Survior S1利用率占空间当前容量的百分比

  • E Eden区利用率占空间当前容量的百分比

  • O Old老年代利用率占空间当前容量的百分比

  • M Metaspace元空间利用率占空间当前容量的百分比

  • CCS 以百分比形式压缩的类空间利用率

  • YGC 年轻代 GC 事件的数量

  • YGCT 年轻代垃圾回收时间

  • FGCT 老年代GC时间

  • GCT 总垃圾回收时间

image.png

JVM 调优

 -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=256m 
-XX:MaxMetaspaceSize=512m -XX:MaxDirectMemorySize=1g -XX:SurvivorRatio=10 
-XX:+UseConcMarkSweepGC -XX:CMSMaxAbortablePrecleanTime=5000 
-XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 
-XX:+UseCMSInitiatingOccupancyOnly -XX:+ExplicitGCInvokesConcurrent 
-Dsun.rmi.dgc.server.gcInterval=2592000000 -Dsun.rmi.dgc.client.gcInterval=2592000000 
-XX:ParallelGCThreads=2 -Dsun.net.client.defaultConnectTimeout=10000 
-Dsun.net.client.defaultReadTimeout=30000

代码优化

Controller @ResponseBody 大结果集不仅会影响解析时间,还会造成内存浪费。

假如结果集在解析成 JSON 之前,占用的内存是 10MB,那么在解析过程中,有可能会使用 20M 或者更多的内存去做这个工作。 很多案例,由于返回对象的嵌套层次太深、引用了不该引用的对象(比如非常大的 byte[] 对象),造成了内存使用的飙升。保持结果集的精简,非常有必要,这是 DTO(data transfer object)存在原因之一。

Service 层

service 层用于处理具体的业务,大部分功能需求都是在这里完成的。service 层一般是使用单例模式,很少会保存状态,而且可以被 controller 复用。

service 层的代码组织,对代码的可读性、性能影响都比较大。设计模式,大多数是针对 service 层。

对象用完置null : clear to let GC do its work


public static void main(String[] args) {
    EntClearingInfoVo entClearingInfoBo = new EntClearingInfoVo();
    entClearingInfoBo.setEnterpriseNum("test1");
    
    Map<Integer, List<StaffSettingInfoBo>> productStaffLists = new HashMap<>();\
    
    List<StaffSettingInfoBo> list = new ArrayList<>();
    StaffSettingInfoBo staffSettingInfoBo = new StaffSettingInfoBo();
    staffSettingInfoBo.setStaffID("test2");
    list.add(staffSettingInfoBo);
    
    productStaffLists.put(0,list);

    entClearingInfoBo.setProductStaffLists(productStaffLists);
    System.out.println(GsonUtil.getGson().toJson(entClearingInfoBo));
    
    System.out.println(GsonUtil.getGson().toJson(productStaffLists));
    productStaffLists = null;// clear to let GC do its work
    System.out.println(GsonUtil.getGson().toJson(productStaffLists));


}

e.printStackTrace()

1)内存占用问题

e.printStackTrace() 将异常打印到控制台时,会将产生错误堆栈信息存入字符串常量池中,如果在常量池空间较小且异常较多时,常量池空间可能会被异常信息占满, 这样其他需要使用或者正在使用此空间的线程就会产生阻塞现象,甚至最终抛出 OOM,导致整个应用挂掉。

2)性能问题

如下代码中的synchronized关键字告诉我们e.printStackTrace()执行时会有并发锁,如果异常代码频繁被调用时,e.printStackTrace()的性能会下降。

3) 实际使用场景


优化:e.printStackTrace() ,存在性能问题;
    
介绍:打印 Java 异常的调用栈,即Exception + Trace

性能问题:
1. java中,e.printStackTrace() 会产生大量字符串的方法, 占用很多非堆内存;
2. 短时间内大量请求访问异常接口,e.printStackTrace()  导致内存被占满,其他线程处于相互等待,相互等待内存,会产生线程死锁,等线程耗尽,整个服务就会被打挂。

优化方案:
方案一:不使用 e.printStackTrace()  ,干掉服务中的这行代码
方案二:通过环境标识打印e.printStackTrace(),生产环境这行代码没有意义,测试开发环境可以输出。
方案三:jvm进行了优化,如果出现非常频繁打印同一个堆栈信息的情况,后续将不会再打印堆栈信息了  (k8s JAVA_OPTS 配置JVM参数 -XX:+OmitStackTraceInFastThrow)。
 

 


{"name":"JAVA_OPTS","value":"-Xms6g
      -Xmx6g -Xmn2g -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=1g
      -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
      -XX:CMSMaxAbortablePrecleanTime=5000 
      -XX:+CMSClassUnloadingEnabled
      -XX:CMSInitiatingOccupancyFraction=80 
      -XX:+UseCMSInitiatingOccupancyOnly
      -XX:+ExplicitGCInvokesConcurrent
      -Dsun.rmi.dgc.server.gcInterval=2592000000
      -Dsun.rmi.dgc.client.gcInterval=2592000000 -XX:ParallelGCThreads=2
      -Dsun.net.client.defaultConnectTimeout=10000
      -Dsun.net.client.defaultReadTimeout=30000
      -Dsentinel.remoteAddress=sentinel.517nacos.com:343434"}

    
JAVA_OPTS="$JAVA_OPTS 
-XX:ParallelGCThreads=4 
-XX:MaxTenuringThreshold=9 
-XX:+DisableExplicitGC 
-XX:+ScavengeBeforeFullGC 
-XX:SoftRefLRUPolicyMSPerMB=0 
 
-XX:ParallelGCThreads=2       定义CMS过程并行收集的线程数。
    
-XX:+CMSClassUnloadingEnabled 相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置-XX:+CMSClassUnloadingEnabled。默认关闭
    
-XX:+ExplicitGCInvokesConcurrent 做System.gc()时会做background模式CMSGC,即并行FULLGC,可提高FULLGC效率,注意:该参数在允许systemGC且使用CMSGC时有效


-XX:+PrintGCDetails  详细的查看 GC 的回收操作,一般会将 GC 的输出,单独单到一个 log 文件当中进行查看
-XX:+HeapDumpOnOutOfMemoryError   发生OOM时,自动生成DUMP 文件

-XX:-OmitStackTraceInFastThrow  省略异常栈信息从而快速抛出
-Duser.timezone=Asia/Shanghai 
-Dclient.encoding.override=UTF-8 
-Dfile.encoding=UTF-8 
-Djava.security.egd=file:/dev/./urandom"

参考:zhuanlan.zhihu.com/p/437612041

4) 源码

private void printStackTrace(PrintStreamOrWriter s) {
        // Guard against malicious overrides of Throwable.equals by
        // using a Set with identity equality semantics.
        Set<Throwable> dejaVu =
            Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
        dejaVu.add(this);
 
        synchronized (s.lock()) {
            // Print our stack trace
            s.println(this);
            StackTraceElement[] trace = getOurStackTrace();
            for (StackTraceElement traceElement : trace)
                s.println("\tat " + traceElement);
 
            // Print suppressed exceptions, if any
            for (Throwable se : getSuppressed())
                se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
 
            // Print cause, if any
            Throwable ourCause = getCause();
            if (ourCause != null)
                ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
        }
    }

##  select * from table 性能问题定位

现象CUP 100% ,内存也很高,所有请求耗时变慢,应用网络流量接受率很高(从数据库 输出到 应用)

image.png 查询数据库慢SQL

thread -n 10


BeanUtil.copyProperties 存在性能消耗

image.png

List集合在开发过程中运用的频率相当高

List list1 = new ArrayList(); list1.add(1); list1.add(2); list1.add(3);

Integer[] array1= {1,2,3}; List list2 = Arrays.asList(array1);

springboot中,@Component@Service@Controller默认都是单例(singleton),prototype是每次调用都会new一个新的对象。

参考:blog.csdn.net/fegus/artic…

线程池

 
public String doSimplePost(String url, String params, String charset) {
    if (StringUtils.isBlank(url)) {
        return null;
    }
    CloseableHttpResponse response = null;
    try {
        HttpEntity httpEntity = null;
        if (params != null && !params.isEmpty()) {
            httpEntity = new StringEntity(params, charset);
        }
        HttpPost httpPost = new HttpPost(url);
        httpPost.setHeader("content-type", "application/json");
        if (this.headers != null && this.headers.size() > 0) {
            for (Header header : headers) {
                httpPost.addHeader(header);
            }
        }
        if (httpEntity != null && httpEntity.getContentLength() > 0) {
            httpPost.setEntity(httpEntity);
        }
        response = httpClinet.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode != 200) {
            httpPost.abort();
            if (statusCode == 401) {
                return "授权错误";
            }
            throw new RuntimeException("HttpClient,error status code :" + statusCode);
        }
        HttpEntity entity = response.getEntity();
        String result = null;
        if (entity != null) {
            result = EntityUtils.toString(entity, charset);
        }
        EntityUtils.consume(entity);
        response.close();
        return result;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (response != null) {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
     
**    if(null != client) {
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}**
 
    }
    return null;
}

## http 通信用完要关闭, CloseableHttpClient 用完就要去关闭。线程用完要shuntdown
 
    
    ```
    if(null != client) {
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

SpringBoot 服务超时配置,尽可能小于60s ,大于这个专门放行改接口

feign中的配置的readTimeout和connectTimeout会覆盖ribbon中的ConnectTimeout和ReadTimeout; ribbon.ConnectTimeout=150000 ribbon.ReadTimeout=150000 feign.client.config.default.connectTimeout=120000 feign.client.config.default.readTimeout=150000
server.tomcat.max-threads=80048g内存 核数*200,线程数8008c16g ,8*200=1600