• 作者简介:大家好,我是爱吃芝士的马铃薯倪,24届校招生Java选手,很高兴认识大家
  • 系列专栏:Spring原理、JUC原理、Kafka原理、分布式技能原理、数据库技能、JVM原理
  • 假如感觉博主的文章还不错的话,请三连支撑一下博主哦
  • 博主正在努力完成2023计划中:源码溯源,一探终究
  • 联系办法:nhs19990716,加我进群,大家一同学习,一同前进,一同对抗互联网隆冬

内存调优

内存走漏和内存溢出

内存走漏(memory leak):在Java中假如不再运用一个目标,可是该目标仍然在GC ROOT的引证链上,这个目标就不会被废物收回器收回,这种状况就称之为内存走漏

内存走漏绝大多数状况都是由堆内存走漏引起的,所以后续没有特别说明则评论的都是堆内存走漏。

少量的内存走漏能够容忍,可是假如发生继续的内存走漏,就像滚雪球雪球越滚越大,不论有多大的内存早晚会被消耗完,终究导致的成果便是内存溢出可是发生内存溢出并不是只有内存走漏这一种原因。

比方初始是这样的。

JVM系列-7内存调优

跟着继续走漏,变成了这样。

JVM系列-7内存调优

常见场景

第一种

内存走漏导致溢出的常见场景是大型的Java后端运用中,在处理用户的恳求之后,没有及时将用户的数据删除。跟着用户恳求数量越来越多,内存走漏的目标占满了堆内存终究导致内存溢出。

这种发生的内存溢出会直接导致用户恳求无法处理,影响用户的正常运用。重启能够恢复运用运用,可是在运转一段时刻之后仍然会呈现内存溢出。

JVM系列-7内存调优

第二种

常见场景是分布式使命调度体系如Elastic-job、Quartz等进行使命调度时,被调度的Java运用在调度使命完毕中呈现了内存走漏,终究导致多次调度之后内存溢出。

这种发生的内存溢出会导致运用履行下次的调度使命履行。相同重启能够恢复运用运用,可是在调度履行一 段时刻之后仍然会呈现内存溢出。

JVM系列-7内存调优

处理内存溢出的办法

处理内存溢出的进程一共分为四个进程,其间前两个进程是最中心的:

JVM系列-7内存调优

发现的进程特别重要,在许多公司内部,运用监控东西进行告警。

Top指令

top指令是linux下用来检查体系信息的一个指令,它供给给咱们去实时地去检查体系的资源,比方履行时的进程、线程和体系参数等信息。

JVM系列-7内存调优

其长处是操作简略,无额外的软件装置。缺点是只能检查最根底的进程信息,无法检查每个部分的内存占用(堆、办法区、堆外)。

VisualVM

VisualVM是多功用合一的Java毛病扫除东西而且他是一款可视化东西,整合了指令行 JDK 东西和轻量级剖析功用,功用非常强大。

这款软件在Oracle JDK 6~8 中发布,可是在 Oracle JDK 9 之后不在JDK装置目录下需求独自下载。下载地址:visualvm.github.io/

JVM系列-7内存调优

JDK8中的翻开办法为 bin/jvisualvm.exe

在出产环境上其实是制止运用VisualVM的,因为能够手动Full GC、Heap Dump。这些进程会将你的整个线程都中止,这样会影响到用户的运用。可是这种办法能够被运用到测验环境去定位一些问题。

其长处是功用丰厚、实时监控CPU、内存、线程等详细信息,而且支撑Idea插件、开发进程中也能够运用。缺点便是对许多集群化布置的Java进程需求手动进行办理。

Arthas

Arthas 是一款线上监控确诊产品,经过全局视角实时检查运用 load、内存、gc、线程的状态信息,并能在不修正运用代码的状况下,对事务问题进行确诊,包括检查办法调用的收支参、异常,监测办法履行耗时,类加载信息等,大大提高线上问题排查效率。

JVM系列-7内存调优

运用阿里arthas tunnel办理一切的需求监控的程序

布景:

小李的团队现已普及了arthas的运用,可是因为运用了微服务架构,出产环境上的运用数量非常多,运用arthas还得登录到每一台服务器上再去操作非常不便利。他看到官方文档上能够运用tunnel来办理一切需求监控的程序。

JVM系列-7内存调优

进程:

  1. 在Spring Boot程序中添加arthas的依赖(支撑Spring Boot2),在配置文件中添加tunnel服务端的地址,便于tunnel去监控一切的程序。
  2. 将tunnel服务端程序布置在某台服务器上并发动。
  3. 发动java程序
  4. 翻开tunnel的服务端页面,检查一切的进程列表,并选择进程进行arthas的操作。

这种服务就比较适合于出产环境或许测验环境去办理许多的集群。

Prometheus + Grafana

Prometheus+Grafana是企业中运维常用的监控计划,其间Prometheus用来收集体系或许运用的相关数据,一起具有告警功用。Grafana能够将Prometheus收集到的数据以可视化的办法进行展现。

JVM系列-7内存调优

其长处支撑体系级别和运用级别的监控,比方linux操作体系、Redis、MySQL、Java进程。支撑告警并允许自界说告警指标,经过邮件、短信等办法尽早告诉相关人员进行处理。

堆内存状况的比照

JVM系列-7内存调优

发生内存走漏的原因

代码中的内存走漏

事例1:equals()和hashCode()导致的内存走漏

问题:

在界说新类时没有重写正确的equals()和hashCode()办法。在运用HashMap的场景下,假如运用这个类目标作为key,HashMap在判别key是否现已存在时会运用这些办法,假如重写办法不正确,会导致相同的数据被保存多份。

正常状况:

1、以JDK8为例,首要调用hash办法核算key的哈希值,hash办法中会运用到key的hashcode办法。依据hash办法的成果决议寄存的数组中方位。

2、假如没有元素,直接放入。假如有元素,先判别key是否相等,会用到equals办法,假如key相等,直接替换value;key不相等,走链表或许红黑树查找逻辑,其间也会运用equals比对是否相同。

JVM系列-7内存调优

异常状况:

1、hashCode办法完成不正确,会导致相同id的学生目标核算出来的hash值不同,或许会被分到不同的槽中。

JVM系列-7内存调优

2、equals办法完成不正确,会导致key在比对时,即使学生目标的id是相同的,也被以为是不同的key。

JVM系列-7内存调优

3、长时刻运转之后HashMap中会保存许多相同id的学生数据。

JVM系列-7内存调优

处理计划:

1、在界说新实体时,一直重写equals()和hashCode()办法。

2、重写时必定要确认运用了仅有标识去区别不同的目标,比方用户的id等。

3、hashmap运用时尽量运用编号id等数据作为key,不要将整个实体类目标作为key寄存。

事例2:内部类引证外部类

问题:

1、非静态的内部类默许会持有外部类,虽然代码上不再运用外部类,所以假如有地方引证了这个非静态内部类,会导致外部类也被引证,废物收回时无法收回这个外部类。

public class Outer{
    private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
    private String name  = "测验";
    class Inner{
        private String name;
        public Inner() {
            this.name = Outer.name;
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
//        System.in.read();
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while (true){
            if(count++ % 100 == 0){
                Thread.sleep(10);
            }
            inners.add(new Outer().new Inner());
        }
    }
}

1、这个事例中,运用内部类的原因是能够直接获取到外部类中的成员变量值,简化开发。假如不想持有外部类目标,应该运用静态内部类。

2、运用静态办法,能够避免匿名内部类持有调用者目标。

public class Outer{
    private byte[] bytes = new byte[1024 * 1024]; //外部类持有数据
    private static String name  = "测验";
    static class Inner{
        private String name;
        public Inner() {
            this.name = Outer.name;
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
//        System.in.read();
        int count = 0;
        ArrayList<Inner> inners = new ArrayList<>();
        while (true){
            if(count++ % 100 == 0){
                Thread.sleep(10);
            }
            inners.add(new Inner());
        }
    }
}

2、匿名内部类目标假如在非静态办法中被创立,会持有调用者目标,废物收回时无法收回调用者。(和非静态内部类同理)

public class Outer {
    private byte[] bytes = new byte[1024];
    public List<String> newList() {
        List<String> list = new ArrayList<String>() {{
            add("1");
            add("2");
        }};
        return list;
    }
    public static void main(String[] args) throws IOException {
        System.in.read();
        int count = 0;
        ArrayList<Object> objects = new ArrayList<>();
        while (true){
            System.out.println(++count);
            objects.add(new Outer().newList());
        }
    }
}

修正往后

public class Outer {
    private byte[] bytes = new byte[1024];
    public static List<String> newList() {
        List<String> list = new ArrayList<String>() {{
            add("1");
            add("2");
        }};
        return list;
    }
    public static void main(String[] args) throws IOException {
        System.in.read();
        int count = 0;
        ArrayList<Object> objects = new ArrayList<>();
        while (true){
            System.out.println(++count);
            objects.add(newList());
        }
    }
}

事例3:ThreadLocal的运用

问题:

假如仅仅运用手动创立的线程,就算没有调用ThreadLocal的remove办法整理数据,也不会发生内存走漏。因为当线程被收回时,ThreadLocal也相同被收回。可是假如运用线程池就不必定了。

处理计划:

线程办法履行完,必定要调用ThreadLocal中的remove办法整理目标。

事例4:经过静态字段保存目标

问题:

假如许多的数据在静态变量中被长期引证,数据就不会被开释,假如这些数据不再运用,就成为了内存走漏。

处理计划:

1、尽量减少将目标长时刻的保存在静态变量中,假如不再运用,有必要将目标删除(比方在调集中)或许将静态变量设置为null。

2、运用单例形式时,尽量运用懒加载,而不是立即加载。

3、Spring的Bean中不要长期寄存大目标,假如是缓存用于提高性能,尽量设置过期时刻定时失效。

事例5:资源没有正常封闭

问题:

连接和流这些资源会占用内存,假如运用完之后没有封闭,这部分内存不必定会呈现内存走漏,可是会导致close办法不被履行。

处理计划:

1、为了防止呈现这类的资源目标走漏问题,有必要在finally块中封闭不再运用的资源。

2、从 Java 7 开端,运用try-with-resources语法能够用于主动封闭资源。

并发恳求问题

并发恳求问题指的是用户经过发送恳求向Java运用获取数据,正常状况下Java运用将数据回来之后,这部分数据就能够在内存中被开释掉。可是因为用户的并发恳求量有或许很大,一起处理数据的时刻很长,导致许多的数据存在于内存中,终究超越了内存的上限,导致内存溢出。这类问题的处理思路和内存走漏相似,首要要定位到目标发生的本源。

JVM系列-7内存调优

关于咱们常用的SpringBoot运用,同一时刻最许多是有限的,因为默许状况下,tomcat最大线程数为200.所以同一时刻只能处理200个恳求。假如要导致内存溢出,**一般会有三个条件,第一个是一起并发的恳求量比较大,第二个每一次恳求内存中加载的数据比较多,第三个是每一笔恳求处理时刻比较长。**满足了这三个条件,许多的数据在内存中积压,终究导致内存溢出。

模仿并发恳求

运用Apache Jmeter软件能够进行并发恳求测验。

Apache Jmeter是一款开源的测验软件,运用Java语言编写,开始是为了测验Web程序,现在现已发展成支撑数据库、音讯行列、邮件协议等不同类型内容的测验东西。

JVM系列-7内存调优

Apache Jmeter支撑插件扩展,生成多样化的测验成果。

JVM系列-7内存调优

运用Jmeter进行并发测验,发现内存溢出问题

布景:

小李的团队发现有一个微服务在晚上8点左右用户运用的顶峰期会呈现内存溢出的问题,所以他们希望在自己的开发环境能重现相似的问题。

进程:

  1. 装置Jmeter软件,添加线程组。
  2. 在线程组中添加Http恳求,添加随机参数。
  3. 在线程组中添加监听器 – 聚合陈述,用来展现终究成果。
  4. 发动程序,运转线程组并观察程序是否呈现内存溢出。

确诊

内存快照

当堆内存溢出时,需求在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件。

运用MAT翻开hprof文件,并选择内存走漏检测功用,MAT会自行依据内存快照中保存的数据剖析内存走漏的本源。

JVM系列-7内存调优

生成内存快照的Java虚拟机参数:(添加两个jvm参数)

-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,主动生成hprof内存快照文件。

-XX:HeapDumpPath=:指定hprof文件的输出途径。

依据MAT的陈述能够很快的找到内存走漏的原因。

MAT内存走漏检测的原理 – 分配树

MAT供给了称为**分配树(Dominator Tree)**的目标图。分配树展现的是目标实例间的分配联系。在目标引证图中,一切指向目标B的途径都经过目标A,则以为目标A分配目标B。

JVM系列-7内存调优

分配树中目标自身占用的空间称之为浅堆(Shallow Heap)。

分配树中目标的子树便是一切被该目标分配的内容,这些内容组成了目标的深堆(Retained Heap),也称之为保留集( Retained Set ) 。深堆的巨细表明该目标假如能够被收回,能开释多大的内存空间。

JVM系列-7内存调优

运用如下代码生成内存快照,并剖析TestClass目标的深堆和浅堆。

如何在不内存溢出状况下生成堆内存快照?-XX:+HeapDumpBeforeFullGC能够在FullGC之前就生成内存快照。

JVM系列-7内存调优

JVM系列-7内存调优

MAT便是依据分配树,从叶子节点向根节点遍历,假如发现深堆的巨细超越整个堆内存的必定份额阈值,就会将其标记成内存走漏的“嫌疑目标”

JVM系列-7内存调优

导出运转中体系的内存快照并进行剖析

布景:

小李的团队经过监控体系发现有一个服务内存在继续添加,希望尽快经过内存快照剖析添加的原因,因为并未发生内存溢出所以不能经过HeapDumpOnOutOfMemoryError参数生成内存快照。

思路:

导出运转中体系的内存快照,比较简略的办法有两种,留意只需求导出标记为存活的目标:

  1. 经过JDK自带的jmap指令导出,格式为:

jmap -dump:live,format=b,file=文件途径和文件名 进程ID

  1. 经过arthas的heapdump指令导出,格式为:

heapdump –live 文件途径和文件名

剖析超大堆的内存快照

在程序员开发用的机器内存范围之内的快照文件,直接运用MAT翻开剖析即可。可是经常会遇到服务器上的程序占用的内存达到10G以上,开发机无法正常翻开此类内存快照,此时需求下载服务器操作体系对应的MAT。下载地址:eclipse.dev/mat/downloa…

留意:默许MAT剖析时只运用了1G的堆内存,假如快照文件超越1G,需求修正MAT目录下的MemoryAnalyzer.ini配置文件调整最大堆内存。

JVM系列-7内存调优

事例实战

修正内存溢出问题的要详细问题详细剖析,问题一共能够分成三类:

JVM系列-7内存调优

事例1 – 分页查询文章接口的内存溢出

布景:

小李担任的新闻资讯类项目采用了微服务架构,其间有一个文章微服务,这个微服务在事务顶峰期呈现了内存溢出的现象。

JVM系列-7内存调优

JVM系列-7内存调优

处理思路:

1、服务呈现OOM内存溢出时,生成内存快照。

2、运用MAT剖析内存快照,找到内存溢出的目标。

3、测验在开发环境中重现问题,剖析代码中问题发生的原因。

4、修正代码。

5、测验并验证成果。

运用MAT最想做的便是定位到那个接口造成了内存溢出,那么只需求经过并发测验单个接口,就必定能找到对应的问题,然后拟定出对应的修正计划。

JVM系列-7内存调优

运用mat进行剖析发现有两个置疑目标,这个数据的来源便是从分配树上获取到的。

第一个置疑目标是线程池里边的许多线程。这个线程是处理HTTP恳求的。底层运用了NIO,端口号是8881

JVM系列-7内存调优

第二个置疑目标是ResultSet,假如了解JDBC,其实这个便是其包装目标

JVM系列-7内存调优

接下来在分配树上依照深堆进行倒序排序。

JVM系列-7内存调优

咱们也能够依据深堆里边的内容点开来看看,比方这个你就能够知道详细来自那张表。

JVM系列-7内存调优

也能够基于线程下手,点开线程的分配树,里边有许多的局部变量,咱们得精准的知道,这个线程履行的办法是哪一个。在这儿有一个目标叫做HandlerMethod,是当时springmvc的处理器,对应的当时的controller办法,可是在分配树上看不到任何的东西。

JVM系列-7内存调优

然后咱们就能够非常快速的找到当时线程在履行那个办法。

JVM系列-7内存调优

假如是这个接口,那么就去直方图看看这个接口的目标是不是有许多没有收回呢?也便是看其深堆巨细。发现当时接口的目标的深堆巨细远远大于其他的。所以咱们有理由置疑是这个目标导致了内存溢出。

JVM系列-7内存调优

问题本源:

文章微服务中的分页接口没有约束最大单次访问条数,而且单个文章目标占用的内存量较大,在事务顶峰期并发量较大时这部分从数据库获取到内存之后会占用许多的内存空间。

处理思路:

1、与产品规划人员交流,约束最大的单次访问条数。

2、分页接口假如只是为了展现文章列表,不需求获取文章内容,能够大大减少目标的巨细。

3、在顶峰期对微服务进行限流保护。

事例2 – Mybatis导致的内存溢出

布景:

小李担任的文章微服务进行了升级,新添加了一个判别id是否存在的接口,第二天事务顶峰期再次呈现了内存溢出,小李觉得应该和新添加的接口有联系。

JVM系列-7内存调优

处理思路:

1、服务呈现OOM内存溢出时,生成内存快照。

2、运用MAT剖析内存快照,找到内存溢出的目标。

3、测验在开发环境中重现问题,剖析代码中问题发生的原因。

4、修正代码。

5、测验并验证成果。

首要剖析其mat陈述,看到这个图,或许就猜出来了,这仅有的置疑目标里边或许包含了元凶巨恶。

JVM系列-7内存调优

依照之前说的办法,找到对应的接口。这个接口的功用主要是为了判别当时的id在数据库中存不存在。

终究中心sql定位到这儿:

JVM系列-7内存调优

问题本源:

Mybatis在运用foreach进行sql拼接时,会在内存中创立目标,假如foreach处理的数组或许调集元素个数过多,会占用许多的内存空间。

处理思路:

1、约束参数中最大的id个数。

2、将id缓存到redis或许内存缓存中,经过缓存进行校验。

事例3 – 导出大文件内存溢出

布景:

小李担任了一个办理体系,这个办理体系支撑几十万条数据的excel文件导出。他发现体系在运转时假如有几十个人一起进行大数据量的导出,会呈现内存溢出。

JVM系列-7内存调优

问题本源:

Excel文件导出假如运用POI的XSSFWorkbook,在大数据量(几十万)的状况下会占用许多的内存。

处理思路:

1、运用poi的SXSSFWorkbook。

2、hutool供给的BigExcelWriter减少内存开支。

3、运用easy excel,对内存进行了许多的优化。将数十万分批导出,使得内存尽或许的小,会导致导出的时刻比较长一些。

事例4 – ThreadLocal运用时占用许多内存

布景:

小李担任了一个微服务,可是他发现体系在没有任何用户运用时,也占用了许多的内存。导致能够运用的内存大大减少。

JVM系列-7内存调优

经过mat剖析发现,threadlocal这个深堆很大,原因是因为许多微服务会选择在拦截器preHandle办法中去解析恳求头中的数据,并放入一些数据到 ThreadLocal中便利后续运用。

JVM系列-7内存调优

而实际的处理办法便是当这个恳求处理完之后呢,正常应该在拦截器中将这个threadlocal整理掉,这样threadlocal的深堆里的空间就被开释掉了。所以需求在拦截器的afterCompletion办法中,有必要要将ThreadLocal中的数据整理掉。

之所以会呈现这个问题是因为运用了线程池,假如线程被收回了,那么不会呈现这个问题,可是假如线程没有收回的话,threadlocal的深堆会一直大。

事例5 – 文章内容审阅接口的内存问题

布景:

文章微服务中供给了文章审阅接口,会调用阿里云的内容安全接口进行文章中文字和图片的审阅,在自测进程中呈现内存占用较大的问题。

JVM系列-7内存调优

规划1:

运用SpringBoot中的@Async注解进行异步的审阅。

JVM系列-7内存调优

存在问题:

1、线程池参数设置不妥,会导致许多线程的创立或许行列中保存许多的数据。

2、使命没有耐久化,一旦走线程池的回绝策略或许服务宕机、服务器掉电等状况很有或许会丢失使命。

规划2:

运用出产者和顾客形式进行处理,行列数据能够完成耐久化到数据库。

JVM系列-7内存调优

存在问题:

1、行列参数设置不正确,会保存许多的数据。

2、完成杂乱,需求自行完成耐久化的机制,否则数据会丢失。

规划3:

运用mq音讯行列进行处理,由mq来保存文章的数据。发送音讯的服务和拉取音讯的服务能够是同一个,也能够不是同一个。

JVM系列-7内存调优

问题本源和处理思路:

在项目中假如要运用异步进行事务处理,或许完成出产者 – 顾客的模型,假如在Java代码中完成,会占用许多的内存去保存中间数据。

尽量运用Mq音讯行列,能够很好地将中间数据独自进行保存,不会占用Java的内存。一起也能够将出产者和顾客拆分成不同的微服务。

确诊和处理问题 – 两种计划

JVM系列-7内存调优

在线定位问题 – 进程

1、运用jmap -histo:live 进程ID > 文件名 指令将内存中存活目标以直方图的形式保存到文件中,这个进程会影响用户的时刻,可是时刻比较短暂。

2、剖析内存占用最多的目标,一般这些目标便是造成内存走漏的原因。

JVM系列-7内存调优

3、运用arthas的stack指令,追寻目标创立的办法被调用的调用途径,找到目标创立的本源。

JVM系列-7内存调优