前段时间公司的项目突然出现了异常,查看日志出现
java.lang.OutOfMemoryError: Java heap space
这不是内存溢出了?当看到这个时候,瞬间想到了JVM调优,这不是面试官最喜欢问的JVM调优?
对JVM没有了解过,刚好项目遇到了,决定学习一波。
首先就是要了解JVM,了解JVM前,需要了解内存模型,类加载机制等;才能更明白的理解JVM,内存模型,类加载机制等,由于时间原因,没有独立的做单独的文章。后续补上。
一:首先了解JVM:
JVM它是Java Virtual Machine 的缩写,主要是通过在实际计算机模仿各种计算机功能来实现的,组成部分包括堆、方法区、栈、本地方法栈、程序计算器等部分组成的,其中方法回收堆和方法区是共享区,也就是谁都可以使用,而栈和程序计算器、本地方法栈区是归JVM的。Java能够被称为“一次编译,到处运行”的原因就是Java屏蔽了很多的操作系统平台相关信息,使得Java只需要生成在JVM虚拟机运行的目标代码也就是所说的字节码,就可以在多种平台运行。
二:产生内存溢出,为什么会出现内存溢出,又何为内存泄漏?
1:内存溢出
系统已经不能再分配出你所需要的空间,比如你需要50M的空间,系统只剩40M了,这就叫内存溢出。
例如:
一辆车只能坐5个人,你却要坐feizhou火车似的6个人,掉马路牙子上了,这就是溢出。
就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错。
2:内存泄漏
强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象
意思就是你用资源的时候为他开辟了一段空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出。
大白话:就像你谈二个女朋友,当你用完时,屁股没擦干净,这时女朋友还在,一次没关系,但是等你女朋友越来越多了,原本一个星期一天陪一个,到第八个女朋友了,就溢出了。
例子:你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
一般我们所说的内存泄漏指的是堆内存的泄露,堆内存是指程序从堆中分配的,大小随机的用完后必须显示释放的内存,C++/C中有free函数可以释放内存,java中有垃圾回收机制不用程序员自己手动调用释放
如果这块内存不释放,就不能再用了,这就叫内存泄漏了。
3:内存溢出,内存泄漏的概念
内存溢出 :
out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 :
memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。那么,Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
看完概念,继续往下看原因
4:内存溢出的原因
(1)如你的代码,循环中一直产生过多重复的对象实体。— 如批量导出成千上万的数据
(2)死循环,这个情况可能不是项目上线就会出现的,可能代码中有条件判断,恰到条件等原因出现死 循环
(3)内存中加载数据库的量太大,就如同第一点的导出,一次从数据库取出过多数据。
(4)还有常见的集合,例如用集合类有对象的引用,导致使用完后未清空,使得JVM不能回收;
(5)还有启动JVM参数设内存值的大小的原因
(6)如使用各种第三方JAR包,如一些第三方JAR包工具类等。
(7)单例模式
(8)各种连接啊,如:数据库连接啊,IO连接啊,网络连接啊
(9)流没有关闭
(10)静态对象
上述某些详细解释:
第七点:单例模式
如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。
看如下例子:
class Test{
public Test(){
SingletonTest.getInstance().setTest(this);
}
....
}
// SingletonTest 类采用单例模式
class SingletonTest{
private Test test ;
private static SingletonTest instance=new SingletonTest();
public SingletonTest(){}
public static SingletonTest getInstance(){
return instance;
}
public void setTest(Test test ){
this.test =test ;
}
//getter...
}
第八点:连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面取的连接,在finally里面释放连接。
第九点:流
开发中,我们经常使用到流,如OutputStream,BufferedReader啊等等。开发中,我们难免会忘记关闭流,这样也会导致内存泄漏。因为每个流在操作系统层面都对应了打开的文件句柄,流没有关闭,会导致操作系统的文件句柄一直处于打开状态,而jvm会消耗内存来跟踪操作系统打开的文件句柄。
第十点:静态对象
静态集合类引起内存泄露(static 对象是不会被GC回收的):
集合类,如HashMap、Set、ArrayList、Vector等它们是内存泄漏的常见位置。如果将他们声明为static,那么它将拥有和主程序一样的生命周期,如果他们里面放置了大量对象(引用的关系),当这些对象不在使用时,因为HashMap,集合等是全局的static类型,那么垃圾回收将无法处理这些不在使用的对象,从而造成了内存泄漏。
如下面的例子代码:
class User{
}
public class MemoryLeak{
static List<User> list = new ArrayList<User>();
public static void main(String[] args)
throws InterruptedException{
for(int i =0;i<1000000;i++){
User user = new User();
list.add(user ); //内存泄漏,将无法回收
}
}
}
三:内存溢出的解决方式,分类
分类:
- 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
- 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
- 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
- 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到
解决方式:
1:配置JVM参数:修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
2:生成hrof文件--检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。:
-verbose:gc -Xms5M -Xmx5M -Xmn2M -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:SurvivorRatio=8
-verbose:gc 命令
-Xms 堆内存的最小大小,默认为物理内存的1/64
-Xmx 堆内存的最大大小,默认为物理内存的1/4
-Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn
-XX:+HeapDumpOnOutOfMemoryError 内存溢出导出hrof文件,默认下载到当前目录下
-XX:+PrintGCDetails 如同英文翻译的意思:打印GC日志信息
-XX:SurvivorRatio JVM参数中有一个比较重要的参数SurvivorRatio,它定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10
以IDEA作为例子–(线上后续加上):
调试工具:jvisualvm.exe
打开jdk bin目录内
选中文件类型就会出现hprof文件了
点击main方法
我们再点击类
双击进去看详细信息
3:代码审查和分析
代码审查,可理解为两种,
一种:
资深-》高级-》中级-》初级
基于代码的评审,如:阅读提交的代码,通过技术经验评审代码质量,分析代码是否会出现BUG,是否会导致内存溢出等;测试工程师对代码进行各种测试,如:功能测试,压力测试,性能测试;通过内部的协调合作对代码进行审查。
另一种是基于AWS的Amazon CodeGuru
借用官方描述:
“ Amazon CodeGuru 是一种机器学习服务,可自动执行代码审查,并提供应用程序性能建议。它可以帮助您找到影响应用程序性能的最昂贵的代码行,并全天候帮助您排查问题,然后为您提供修复或改进代码的具体建议。”
可以说,这款服务是基于Amazon数十年软件开发的知识和经验积累而成的、利用现代机器学习的技术,用于自动代码审查和应用程序性能分析的工具。CodeGuru 的机器学习模型来源于 Amazon 自有的代码库进行训练所得。这些代码库包括大约数十万个Amazon内部的项目,以及 GitHub 上的 1 万多个开源项目。数万名 Amazon 开发人员凭借数十年的代码审查和应用程序分析经验为 CodeGuru 的训练提供了最有益的帮助。这个工具因其托管于AWS云计算之上,会随者越来越多的用户在生产环境中的使用反馈而不断的搜索优化、不断的发展。从功能上来看,它会自动检查代码以查找通常难以发现的缺陷,并提供了可行的建议来解决已发现的问题 ,并帮助在运行的应用程序中找到最有希望的优化方法。与人工的代码审核比较起来,CodeGuru 像不像一位任劳任怨的全时工作的老师傅!
具体可以自行了解,暂不多介绍。
4:工具:如本地使用内存查看工具动态查看内存使用情况
如上述;线上后续加上
5:自身分析
根据前面提到的原因,需要对代码分析找出可能发生内存溢出的位置, 每一次提交都得需要对自己的代码进行审查,分析:
1、检查数据库查询,会不会查询出数据量庞大。
2、检查自己的代码是否有用到递归,或者代码是否死循环等判断依据。
3、如用到的流,有没有关闭流
4、如用第三方JAR包工具类,是否释放资源,如:com.lowagie.text.Document
我们常用的PDF第三方JAR包,是否在finnally调用了close();
5、接口有没有限流
随着系统并发量越来越高,Tomcat所占用的内存就会越来越大,如果对Tomcat的内存管理不当,则可能会引发Tomcat内存溢出的问题
需要设置tomcat的内存大小,具体配置可自行了解。
总结:万变不离其宗,程序不会无缘无故出BUG,要相信是代码的问题。