JVM调优
JVM调优
JDK、JRE、JVM
说明:
1)Jdk包括了Jre和Jvm,Jre包括了Jvm,Jdk是我们编写代码使用的开发工具包。
2)Jre是Java的运行时环境,他大部分都是C和C++语言编写的,他是我们在编译Java时所需要的基础的类库。
3)Jvm俗称Java虚拟机,他是Java运行环境的一部分,它虚构出来的一台计算机,再通过在实际的计算机上仿真模拟各种计算机功能来实现Java应用程序。
JVM调优目的
当程序卡顿、请求吞吐量(QPS)变慢、stop the word(STW)停顿时间过长、内存溢出(OOM)时,如误写死循环或本地开发内存不足,这时我们首先导出JVM内存使用情况日志进行分析(可以使用mat工具进行分析),主要分析堆内存的使用情况。然后我们输出GC日志,通过在线工具GC Easy或jconsole、jvisualvm分析吞吐量、停顿时间和垃圾回收频率,最后我们通过分析的情况调整堆内存的大小和分配比例,不断调式使GC的吞吐量和停顿时间到达稳定高速的状态。
优质文章推荐:
面试回答:https://juejin.cn/post/7034669867286396958
调优流程:https://juejin.cn/post/6844903506093015053
JVM调优参数配置
标准参数(一般默认)
-help:查看JVM帮助,使用java -help可以查看所有的标准参数
-version:输出产品版本并退出
-server:Server VM的初始堆空间大一些,使用并行垃圾回收器,JVM启动慢运行快。
-client:Client VM的初始堆空间比较小,使用串行垃圾回收器,JVM启动快运行慢。
JVM在启动的时候会根据硬件和操作系统自动选择使用Server还是Client类型的JVM。
32位操作系统:如果是Windows系统,不论硬件配置如何,都默认使用Client类型的JVM。 如果是其他操作系统上,机器配置有2GB以上的内存同时有2个以上CPU的话默认使用server模式,否则使用client模式。
64位操作系统:只有server类型,不支持client类型。
-X参数(非标准参数,随相应程序需要做改变)
使用java -X可以查看所有的非标准参数,如改变模式命令:
java -showversion -Xint JvmTest(可以查看java版本并继续)
-Xint:解释模式,会执行所有的字节码文件,会降低运行速度。
-Xcomp:编译模式,JVM会在第一次执行时,将所有字节码编译成本地代码,带来最大程度的优化。
-Xint与-Xcomp比较:都会有性能上的损失(Xint>Xcomp),-Xcomp没有让JVM启用JIT编译器(JIT可以对是否编译做判断)的所有功能;如果全部再次编译,对于只执行一次的代码,毫无意义。
-Xmixed:混合模式,解释模式与编译模式混合使用,由JVM自己决定,这是默认模式,也是推荐模式。
-XX参数(非标准参数,使用率比较高,用于JVM调优)
-XX:newSize
-XX:+UserSerialGC
参数使用的2种方式(boolean类型和非boolean类型),格式:
boolean类型:-XX:[+-]UserSerialGC:加表示启用,减表示禁用;如:
-XX:+DisableExplicitGC,表示禁用手动调用gc操作,也就是说调用System.gc()无效。
非boolean类型:-XX:NewRatio=1,表示新生代和老年代的比值,如:
java ‐showversion ‐XX:+DisableExplicitGC JvmTest
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。
适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快。命令:
java ‐Xms512m ‐Xmx2048m JvmTest(JvmTest:java类编译后的class文件)
运行时参数查看(jinfo)
查看JVM的运行参数:
java -XX:+PrintFlagsFinal -version JvmTest[编译后的class文件名]
输出结果中如果值的操作符为=,表示设置的是默认值,如果为:=,表示设置的是修改后的值;
修改参数值:
java -XX:+PrintFlagsFinal -XX:+ZeroTLAB JvmTest[编译后的class文件名]
查看正在运行的java进程(比如一个tomcat):jps -l(这是小写的L)
tomcat进程名称:org.apache.catalina.startup.Bootstrap
查看正在运行的JVM参数:jinfo -flags <进程ID>
查看正在运行的指定参数:jinfo -flags <参数名(如MaxHeapSize)> <进程ID>
根据字节转换可以知道一些参数的值到底有多大
堆调优参数
# 常用参数
-Xms:初始堆大小,JVM启动的时候,给定堆空间大小。
-Xmx:最大堆大小,JVM运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。
-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。
-XX:NewSize=n 设置年轻代初始化大小大小
-XX:MaxNewSize=n 设置年轻代最大值
-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代+年老代和的1/4
-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10,比值默认就为8
-Xss:设置每个线程的堆栈大小。JDK5后每个线程Java栈大小为1M,以前每个线程堆栈大小为 256K。
-XX:ThreadStackSize=n 线程堆栈大小
-XX:PermSize=n 设置持久代初始值
-XX:MaxPermSize=n 设置持久代大小
-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
# 不常用参数
-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小
-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能
-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用
-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动
-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用
-Xnoclassgc 是否禁用垃圾回收
-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用
设置参数的方式:
1)可以在IDEA,Eclipse,JVM工具里设置。
2)如果上线了是WAR包的话可以在Tomcat设置。
3)如果是Jar包直接执行命令进行设置,如:
java -Xms1024m -Xmx1024m -jar springboot_app.jar
GC调优参数
-XX:+UseSerialGC 设置串行收集器,年轻带收集器。
-XX:+UseParNewGC 设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
-XX:+UseParallelGC 设置并行收集器,目标是目标是达到可控制的吞吐量。
-XX:+UseParallelOldGC 设置并行年老代收集器,JDK6.0支持对年老代并行收集。
-XX:+UseConcMarkSweepGC 设置年老代并发收集器。
-XX:+UseG1GC 设置G1收集器,JDK1.9默认垃圾收集器
-XX:MaxGCPauseMillis 设置目标停顿时间
JVM内存使用情况分析
查看堆内存使用情况(jstat)
查看堆内存使用情况:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]
查看class加载统计:
jstat -class <进程ID>
一些参数的含义:
Loaded:加载class的数量
Bytes:所占用空间大小
Unloaded:未加载数量
Bytes:未加载占用空间
Time:时间
查看编译统计:
jstat -compiler <进程ID>
一些参数的含义:
Compiled:编译数量
Failed:失败数量
Invalid:不可用数量
Time:时间
FailedType:失败类型
FailedMethod:失败的方法
垃圾回收统计:
jstat -gc <进程ID> 1000 10(这个表示每一秒钟打印一次,一共打印10次)
一些参数的含义:
S0C:第一个Survivor区的大小(KB)
S1C:第二个Survivor区的大小(KB)
S0U:第一个Survivor区的使用大小(KB)
S1U:第二个Survivor区的使用大小(KB)
EC:Eden区的大小(KB)
EU:Eden区的使用大小(KB)
OC:Old区大小(KB)
OU:Old使用大小(KB)
MC:方法区大小(KB)
MU:方法区使用大小(KB)
CCSC:压缩类空间大小(KB)
CCSU:压缩类空间使用大小(KB)
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
查看更详细内存情况(jmap)
jmap(JVM Memory Map):作用一方面是获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。
jmap语法:
jmap [option] <pid>
jmap [option] <executable> <core>
jmap [option] [server_id@]<remote server IP or hostname>
options选项:
-dump 生成dump文件
-finalizerinfo 以ClassLoader为统计口径输出永久代的内存状态信息
-heap 输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等
-histo 输出堆空间中对象的统计信息,包括类、实例数量和合计容量
-histo[:live] 输出堆空间中对象的统计信息,包括类、实例数量和合计容量,如果live子参数加上后,只统计活的对象数量。比如pid为1111,使用时:jmap -histo:live 1111。
-permstat 以ClassLoader为统计口径输出永久代的内存状态信息
-F 当虚拟机进程对-dump选项没有任何响应时,强制执行生成dump文件
查看内存使用情况:
jmap -heap <进程ID>
查看内存中对象的数量及大小:
jmap -histo <进程ID> | more
查看内存中活跃对象的数量及大小:
jmap -histo:live <进程ID> | more
将内存使用情况导出到文件中:
导出的意义是可以查看JVM中对象的长度等信息,然后做出更好的调整,导出命令格式:
jmap -dump:format=b,file=dumpFileName <进程ID>
如:jmap -dump:format=b,file=D:/dump.dat 1400
jmap -histo返回对象(class name)说明:
B byte
C char
D double
F float
I int
J long
Z boolean
[ 数组,如[I表示int[]
[L+类名 其他对象
jmap -heap输出jvm参数解析:
通过jhat对dump文件进行分析
jhat -port <port> <file>
如:jhat -port 9999 D:/dump.dat
查看该文件方式:http://localhost:9999
执行文件情况查询语句:
通过mat工具对都dump文件进行分析
MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。 官网地址:https://www.eclipse.org/mat/ ,下载之后直接解压缩安装即可,双击启动程序:
点击file>>>open heap dump打开一个dump文件
第一个红框:列出内存中的对象,对象的数量和大小
第二个红框:列出最大的对象以及其依赖存活的对象
报表展示可能存在内存溢出的地方
模拟内存溢出
package com.study;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* @author: XDZY
* @date: 2020/08/30 13:11
* @description: 测试内存溢出
*/
public class JvmTest2 {
/**
* 向集合中输入100万个字符串,每个字符串由1000个uuid组成
*
* @param args
*/
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
String str = "";
for (int j = 0; j < 1000; j++) {
str = UUID.randomUUID().toString();
}
stringList.add(str);
}
System.out.println("ok");
}
}
设置内存大小,当内存溢出时在本地生成hprof文件,通过mat软件打开该文件即可分析问题
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
JVM程序线程查看
jstack的使用(查看JVM中程序线程)
有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等。由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要看下jvm的内部线程的执行情况,然后再进行分析查找出原因。这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进行快照,并且打印出来。查看JVM中某个程序的所有线程执行情况:
jstack <pid>
线程的状态:
产生死锁时查看:
jps -l #查看java进程
jstack <pid> #查看问题产生原因
日志输出如下:
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.study.JvmTest3.lambda$main$1(JvmTest3.java:39)
- waiting to lock <0x00000000d64b6848> (a java.lang.Object)
- locked <0x00000000d64b6858> (a java.lang.Object)
at com.study.JvmTest3$$Lambda$2/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at com.study.JvmTest3.lambda$main$0(JvmTest3.java:24)
- waiting to lock <0x00000000d64b6858> (a java.lang.Object)
- locked <0x00000000d64b6848> (a java.lang.Object)
at com.study.JvmTest3$$Lambda$1/295530567.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
JVM调优分析工具
JDK自带了很多监控工具,都位于JDK的bin目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具。jconsole用于对JVM中的内存、线程和类等进行监控。jvisualvm是JDK自带的全能分析工具,可以分析内存快照、线程快照、程序死锁、监控内存的变化、gc变化等。
VisualVM工具的使用
VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。
1、内存信息
2、线程信息
3、Dump堆(本地进程)
4、Dump线程(本地进程)
5、打开堆Dump,堆Dump可以用jmap来生成。
6、打开线程Dump
7、生成应用快照(包含内存信息、线程信息等等)
8、性能分析。CPU分析(各个方法调用时间,检查哪些方法耗时多),内存分析(各类对象占用的内存,检查哪些类占用内存多)
在jdk的安装目录的bin目录下,找到jvisualvm.exe,双击打开即可:
页面展示:
JVM调优案例
问题呈现:项目页面刷新无法加载页面,接口访问等待超时。
问题分析:最开始认为接口查询条件引发错误导致nginx挂起,故先调节nginx为15秒失效2次则认为项目实例失效,保证项目正常运行再解决接口问题,但是发现并没有解决该问题。注:nginx轮询2个后端实例,默认10秒内访问一次失败则认为实例无效,多次访问失败则nginx挂起,访问后端超时。
问题解决:发现项目部署的服务器内存为4核8G,并且多个(3个)项目部署在服务器上,故开始查看CPU使用率。主要如下三个步骤:
1、找到最耗CPU的进程
通过top命令查看进程的cpu占用情况,运行top命令后再键入P(大写p),进程会按照CPU使用率排序。也可以通过top -c命令进行实时查看。确实发现项目java进程CPU飙升至300%,这时肯定项目OOM内存溢出了。
2、找到这个进程中最耗CPU的线程
根据上面获取的PID进行查看最耗CPU的线程:
top -Hp ${进程的PID} 或 ps -mp ${进程的PID} -o THREAD,tid,time
运行以上命令后再键入P(大写p),线程会按照CPU使用率排序
3、查看堆栈信息,定位线程的什么操作消耗了大量CPU,定位对应代码
堆栈里,线程id是用16进制表示的,所以需要将上面获取的线程PID转化为16进制:
printf "%x\n" 2611 输出:a33
打印进程堆栈信息(注意2601是进程的PID),通过线程id,过滤得到线程堆栈:
jstack 2601 | grep a33 -A 20
通过不断查看各个消耗CPU线程的堆栈信息确认是否存在代码死锁的情况,最后发现JVM多次进行full gc,并且繁忙。
根据上面获取的信息,开始查看JVM内存使用情况:
1、通过jps -l查看java进程ID
2、执行jmap -heap pid查看JVM内存消耗,pid替换为自己的pid即可,如:jmap -heap 20677,发现新生代和老年代使用率90%以上。
3、通过命令jmap -histo:live 20677 | more查看当前存活对象数量和大小,发现保存在本地缓存的对象数量偏多(航线点位数据),缓存失效时间1小时。点位数量最大能到190万个对象,1小时过期,一直访问不会过期,无法回收。
最终解决方案:增加项目服务器内存,4核8G改为8核16G。其实不应该把大量数据保存至本地缓存,但是由于查询点位信息缓慢,所以暂时不修改保存至本地缓存的方案。后期可以适当调整-Xmx和-Xms参数保证JVM更加有效的运行。
jmap -heap 20677展示案例图:
jmap -histo:live 20677 | more展示案例图:
缓存代码展示:
后来查看系统之前的日志发现OOM异常:
OOM异常说明:
OutOfMemoryError是java.lang.VirtualMachineError的子类,当JVM资源利用出现问题时抛出,更具体地说,这个错误是由于JVM花费太长时间执行GC且只能回收很少的堆内存时抛出的。根据Oracle官方文档,默认情况下,如果Java进程花费98%以上的时间执行GC,并且每次只有不到2%的堆被恢复,则JVM抛出此错误。换句话说,这意味着我们的应用程序几乎耗尽了所有可用内存,垃圾收集器花了太长时间试图清理它,并多次失败。在这种情况下,用户会体验到应用程序响应非常缓慢,通常只需要几毫秒就能完成的某些操作,此时则需要更长的时间来完成,这是因为所有的CPU正在进行垃圾收集,因此无法执行其他任务。理想的解决方案是通过检查可能存在内存泄漏的代码来发现应用程序所存在的问题,这时需要考虑:
1)应用程序中哪些对象占据了堆的大部分空间
2)这些对象在源码中的哪些部分被使用
我们可以使用自动化图形工具,比如JVisualVM、JConsole,它可以帮助检测代码中的性能问题,包括java.lang.OutOfMemoryError。最后一种方法是通过更改JVM启动配置来增加堆大小,或者在JVM启动配置里增加-XX:-UseGCOverheadLimit选项来关闭GC Overhead limit exceeded。例如,以下JVM参数为Java应用程序提供了1GB堆空间:
java -Xmx1024m com.xyz.TheClassName
以下JVM参数不仅为Java应用程序提供了1GB堆空间,也增加-XX:-UseGCOverheadLimit选项来关闭GC Overhead limit exceeded:
java -Xmx1024m -XX:-UseGCOverheadLimit com.xyz.TheClassName
但增加-XX:-UseGCOverheadLimit选项的方式治标不治本,JVM最终会抛出java.lang.OutOfMemoryError: Java heap space错误。总之,如果实际的应用程序代码中存在内存泄漏,那么以上列举的方法并不能解决问题,相反,我们将推迟这个错误。因此,更明智的做法是彻底重新评估应用程序的内存使用情况。
后话:最后使用SonarQube对项目代码进行了全文扫描,发现存在很多代码隐患和漏洞,只能逐步修复代码。
OOM异常分析
1)第一类内存溢出,也是大家认为最多的,第一反应认为是的内存溢出,也就是堆栈溢出:
java.lang.OutOfMemoryError: ......java heap space.....
原因:当你看到heap相关的时候就肯定是堆栈溢出了,此时如果代码没有问题的情况下,适当调整-Xmx和-Xms是可以避免的,不过一定是代码没有问题的前提,为什么会溢出呢,要么代码有问题,要么访问量太多并且每个访问的时间太长或者数据太多,导致数据释放不掉,因为垃圾回收器是要找到哪些是垃圾才能回收,这里它不会认为这些东西是垃圾,自然不会去回收了;注意这个溢出之前,可能系统会提前先报错关键字为:
java.lang.OutOfMemoryError:GC over head limit exceeded
原因:这种情况是当系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC。
2)第二类内存溢出,PermGen的溢出或者PermGen满了的提示,你会看到这样的关键字:
java.lang.OutOfMemoryError: PermGen space
原因:系统的代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、或者通过动态代码加载等方法,导致常量池的膨胀,虽然JDK 1.5以后可以通过设置对永久带进行回收,但是我们希望的是这个地方是不做GC的,它够用就行,所以一般情况下今年少做类似的操作,所以在面对这种情况常用的手段是:增加-XX:PermSize和-XX:MaxPermSize的大小。
3)第三类内存溢出,在使用ByteBuffer中的allocateDirect()的时候会用到,很多javaNIO的框架中被封装为其他的方法。溢出关键字:
java.lang.OutOfMemoryError: Direct buffer memory
原因:如果你在直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而不做clear的时候就会出现类似的问题,常规的引用程序IO输出存在一个内核态与用户态的转换过程,也就是对应直接内存与非直接内存,如果常规的应用程序你要将一个文件的内容输出到客户端需要通过OS的直接内存转换拷贝到程序的非直接内存(也就是heap中),然后再输出到直接内存由操作系统发送出去,而直接内存就是由OS和应用程序共同管理的,而非直接内存可以直接由应用程序自己控制的内存,JVM垃圾回收不会回收掉直接内存这部分的内存,所以要注意了哦。如果经常有类似的操作,可以考虑设置参数:-XX:MaxDirectMemorySize
4)第四类内存溢出错误。溢出关键字:
java.lang.StackOverflowError
原因:这个参数直接说明一个内容,就是-Xss太小了,我们申请很多局部调用的栈针等内容是存放在用户当前所持有的线程中的,线程在jdk 1.4以前默认是256K,1.5以后是1M,如果报这个错,只能说明-Xss设置得太小,当然有些厂商的JVM不是这个参数,本文仅仅针对Hotspot VM而已;不过在有必要的情况下可以对系统做一些优化,使得-Xss的值是可用的。
5)第五类内存溢出错误。溢出关键字:
java.lang.OutOfMemoryError: unable to create new native thread
原因:上面第四种溢出错误,已经说明了线程的内存空间,其实线程基本只占用heap以外的内存区域,也就是这个错误说明除了heap以外的区域,无法为线程分配一块内存区域了,这个要么是内存本身就不够,要么heap的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了。
6)第六类内存溢出。溢出关键字
java.lang.OutOfMemoryError: request {} byte for {}out of swap
原因:这类错误一般是由于地址空间不够而导致。
总结:六大类常见溢出已经说明JVM中99%的溢出情况,要逃出这些溢出情况非常困难,除非一些很怪异的故障问题会发生,比如由于物理内存的硬件问题,导致了code cache的错误(在由byte code转换为native code的过程中出现,但是概率极低),这种情况内存会被直接crash掉,类似还有swap的频繁交互在部分系统中会导致系统直接被crash掉,OS地址空间不够的话,系统根本无法启动;JNI的滥用也会导致一些本地内存无法释放的问题,所以尽量避开JNI;socket连接数据打开过多的socket也会报类似:IOException: Too many open files等错误信息。JNI就不用多说了,尽量少用,这种内存如果没有在被调用的语言内部将内存释放掉(如C语言),那么在进程结束前这些内存永远释放不掉,解决办法只有一个就是将进程kill掉。另外GC本身是需要内存空间的,因为在运算和中间数据转换过程中都需要有内存,所以你要保证GC的时候有足够的内存,如果没有的话GC的过程将会非常的缓慢。很多参数没啥建议值,建议值是自己在现场根据实际情况科学计算和测试得到的综合效果,建议值没有绝对好的,而且默认值很多也是有问题的,因为不同的版本和厂商都有很大的区别,默认值没有永久都是一样的,就像-Xss参数的变化一样。
Map集合双花括号初始化导致的内存泄漏问题
Map<String, String> map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
可能内存泄漏分析:
用双括号的代码其实是创建了匿名内部类,然后再进行初始化代码块。这一点我们可以使用命令 javac将代码编译成字节码之后发现,我们发现之前的一个类被编译成两个字节码(.class)文件。在Java语言中非静态内部类会持有外部类的引用,从而导致GC无法回收这部分代码的引用,以至于造成内存溢出。
为什么要持有外部类:
这个就要从匿名内部类的设计说起了,在Java语言中,非静态匿名内部类的主要作用有两个:
1、当匿名内部类只在外部类(主类)中使用时,匿名内部类可以让外部不知道它的存在,从而减少了代码的维护工作。
2、当匿名内部类持有外部类时,它就可以直接使用外部类中的变量了,这样可以很方便的完成调用,如下代码所示,在HashMap的方法内部,可以直接使用外部类的变量userName:
public class DoubleBracket {
private static String userName = "hello world";
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Map<String, String> map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
put(userName, userName);
}};
}
}
为什么说可能内存溢出:
这是因为当此map被赋值为其他类属性时,可能会导致GC收集时不清理此对象,这时候才会导致内存泄漏。要想保证双花扣号不泄漏,办法也很简单,只需要将map对象声明为static静态类型的就可以了,代码如下:
public static Map createMap() {
Map map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
}};
return map;
}
为什么静态内部类不会持有外部类的引用:
原因其实很简单,因为匿名内部类是静态的之后,它所引用的对象或属性也必须是静态的了,因此就可以直接从JVM的Method Area(方法区)获取到引用而无需持久外部对象了。
即使声明为静态的变量可以避免内存泄漏,但依旧不建议这样使用,为什么呢:
原因很简单,项目一般都是需要团队协作的,假如那位老兄在不知情的情况下把你的static给删掉了,这就相当于设置了一个隐形的“坑”,其他不知道的人,一不小心就跳进去了,所以我们可以尝试一些其他的方案,比如Java8中的Stream API和Java9中的集合工厂等。即使不泄漏,也不够直观,不方便他人进行代码维护。