蚂蚁金服寒泉子:JVM源码分析之临门一脚的OutOfMemoryError完全解读
概述
OutOfMemoryError,说的是java.lang.OutOfMemoryError,是JDK里自带的异常,顾名思义,说的就是内存溢出,当我们的系统内存严重不足的时候就会抛出这个异常(PS:注意这是一个Error,不是一个Exception,所以当我们要catch异常的时候要注意哦),这个异常说常见也常见,说不常见其实也见得不多,不过作为Java程序员至少应该都听过吧,如果你对jvm不是很熟,或者对OutOfMemoryError这个异常了解不是很深的话,这篇文章肯定还是可以给你带来一些惊喜的,通过这篇文章你至少可以了解到如下几点:
OutOfMemoryError一定会被加载吗
什么时候抛出OutOfMemoryError
会创建无数OutOfMemoryError实例吗
为什么大部分OutOfMemoryError异常是无堆栈的
我们如何去分析这样的异常
OutOfMemoryError类加载
既然要说OutOfMemoryError,那就得从这个类的加载说起来,那这个类什么时候被加载呢?你或许会不假思索地说,根据java类的延迟加载机制,这个类一般情况下不会被加载,除非当我们抛出OutOfMemoryError这个异常的时候才会第一次被加载,如果我们的系统一直不抛出这个异常,那这个类将一直不会被加载。说起来好像挺对,不过我这里首先要纠正这个说法,要明确的告诉你这个类在jvm启动的时候就已经被加载了,不信你就执行`java -verbose:class -version`打印JDK版本看看,看是否有OutOfMemoryError这个类被加载,再输出里你将能找到下面的内容:
```
[Loaded java.lang.OutOfMemoryError from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
```
这意味着这个类其实在vm启动的时候就已经被加载了,那JVM里到底在哪里进行加载的呢,且看下面的方法:
```
bool universe_post_init() {
...
// Setup preallocated OutOfMemoryError errors
k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_OutOfMemoryError(), true, CHECK_false);
k_h = instanceKlassHandle(THREAD, k);
Universe::_out_of_memory_error_java_heap = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_class_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_array_size = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_gc_overhead_limit =
k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_realloc_objects = k_h->allocate_instance(CHECK_false);
...if (!DumpSharedSpaces) {
// These are the only Java fields that are currently set during shared space dumping.
// We prefer to not handle this generally, so we always reinitialize these detail messages.
Handle msg = java_lang_String::create_from_str("Java heap space", CHECK_false);
java_lang_Throwable::set_message(Universe::_out_of_memory_error_java_heap, msg());
msg = java_lang_String::create_from_str("Metaspace", CHECK_false);
java_lang_Throwable::set_message(Universe::_out_of_memory_error_metaspace, msg());
msg = java_lang_String::create_from_str("Compressed class space", CHECK_false);
java_lang_Throwable::set_message(Universe::_out_of_memory_error_class_metaspace, msg());
msg = java_lang_String::create_from_str("Requested array size exceeds VM limit", CHECK_false);
java_lang_Throwable::set_message(Universe::_out_of_memory_error_array_size, msg());
msg = java_lang_String::create_from_str("GC overhead limit exceeded", CHECK_false);
java_lang_Throwable::set_message(Universe::_out_of_memory_error_gc_overhead_limit, msg());
msg = java_lang_String::create_from_str("Java heap space: failed reallocation of scalar replaced objects", CHECK_false);
java_lang_Throwable::set_message(Universe::_out_of_memory_error_realloc_objects, msg());
msg = java_lang_String::create_from_str("/ by zero", CHECK_false);
java_lang_Throwable::set_message(Universe::_arithmetic_exception_instance, msg());
// Setup the array of errors that have preallocated backtrace
k = Universe::_out_of_memory_error_java_heap->klass();
assert(k->name() == vmSymbols::java_lang_OutOfMemoryError(), "should be out of memory error");
k_h = instanceKlassHandle(THREAD, k);int len = (StackTraceInThrowable) ? (int)PreallocatedOutOfMemoryErrorCount : 0;
Universe::_preallocated_out_of_memory_error_array = oopFactory::new_objArray(k_h(), len, CHECK_false);
for (int i=0; i<len; i++) {
oop err = k_h->allocate_instance(CHECK_false);
Handle err_h = Handle(THREAD, err);
java_lang_Throwable::allocate_backtrace(err_h, CHECK_false);
Universe::preallocated_out_of_memory_errors()->obj_at_put(i, err_h());
}
Universe::_preallocated_out_of_memory_error_avail_count = (jint)len;
}
}
```
上面的代码其实就是在vm启动过程中加载了OutOfMemoryError这个类,并且创建了好几个OutOfMemoryError对象,每个OutOfMemoryError对象代表了一种内存溢出的场景,比如说`Java heap space`不足导致的OutOfMemoryError,抑或`Metaspace`不足导致的OutOfMemoryError,上面的代码来源于JDK8,所以能看到metaspace的内容,如果是JDK8之前,你将看到Perm的OutOfMemoryError,不过本文metaspace不是重点,所以不展开讨论,如果大家有兴趣,可以专门写一篇文章来介绍metsapce来龙去脉,说来这个坑填起来还挺大的。
能通过agent拦截到这个类加载吗
熟悉字节码增强的人,可能会条件反射地想到是否可以拦截到这个类的加载呢,这样我们就可以做一些譬如内存溢出的监控啥的,哈哈,我要告诉你的是`NO WAY`,因为通过agent的方式来监听类加载过程是在vm初始化完成之后才开始的,而这个类的加载是在vm初始化过程中,因此不可能拦截到这个类的加载,于此类似的还有`java.lang.Object`,`java.lang.Class`等。
为什么要在vm启动过程中加载这个类
这个问题或许看了后面的内容你会有所体会,先卖个关子。包括为什么要预先创建这几个实例对象后面也会解释。
何时抛出OutOfMemoryError
要抛出OutOfMemoryError,那肯定是有地方需要进行内存分配,可能是heap里,也可能是metsapce里(如果是在JDK8之前的会是Perm里),不同地方的分配,其策略也不一样,简单来说就是尝试分配,实在没办法就gc,gc还是不能分配就抛出异常。
不过还是以Heap里的分配为例说一下具体的过程:
正确情况下对象创建需要分配的内存是来自于Heap的Eden区域里,当Eden内存不够用的时候,某些情况下会尝试到Old里进行分配(比如说要分配的内存很大),如果还是没有分配成功,于是会触发一次ygc的动作,而ygc完成之后我们会再次尝试分配,如果仍不足以分配此时的内存,那会接着做一次full gc(不过此时的soft reference不会被强制回收),将老生代也回收一下,接着再做一次分配,仍然不够分配那会做一次强制将soft reference也回收的full gc,如果还是不能分配,那这个时候就不得不抛出OutOfMemoryError了。这就是Heap里分配内存抛出OutOfMemoryError的具体过程了。
OutOfMemoryError对象可能会很多吗
想象有这么一种场景,我们的代码写得足够烂,并且存在内存泄漏,这意味着系统跑到一定程度之后,只要我们创建对象要分配内存的时候就会进行gc,但是gc没啥效果,进而抛出OutOfMemoryError的异常,那意味着每发生此类情况就应该创建一个OutOfMemoryError对象,并且抛出来,也就是说我们会看到一个带有堆栈的OutOfMemoryError异常被抛出,那事实是如此吗?如果真是如此,那为什么在VM启动的时候会创建那几个OutOfMemoryError对象呢?
抛出异常的java代码位置需要我们关心吗
这个问题或许你仔细想想就清楚了,如果没想清楚,请在这里停留一分钟仔细想想再往后面看。
抛出OutOfMemoryError异常的java方法其实只是临门一脚而已,导致内存泄漏的不一定就是这个方法,当然也不排除可能是这个方法,不过这种情况的可能性真的非常小。所以你大可不必去关心抛出这个异常的堆栈。
既然可以不关心其异常堆栈,那意味着这个异常其实没必要每次都创建一个不一样的了,因为不需要堆栈的话,其他的东西都可以完全相同,这样一来回到我们前面提到的那个问题,`为什么要在vm启动过程中加载这个类`,或许你已经有答案了,在vm启动过程中我们把类加载起来,并创建几个没有堆栈的对象缓存起来,只需要设置下不同的提示信息即可,当需要抛出特定类型的OutOfMemoryError异常的时候,就直接拿出缓存里的这几个对象就可以了。
所以OutOfMemoryError的对象其实并不会太多,哪怕你代码写得再烂,当然,如果你代码里要不断`new OutOfMemoryError()`,那我就无话可说啦。
为什么我们有时候还是可以看到有堆栈的OutOfMemoryError
如果都是用jvm启动的时候创建的那几个OutOfMemoryError对象,那不应该再出现有堆栈的OutOfMemoryError异常,但是实际上我们偶尔还是能看到有堆栈的异常,如果你细心点的话,可能会总结出一个规律,发现最多出现4次有堆栈的OutOfMemoryError异常,当4次过后,你都将看到无堆栈的OutOfMemoryError异常。
这个其实在我们上面贴的代码里也有体现,最后有一个for循环,这个循环里会创建几个OutOfMemoryError对象,如果我们将`StackTraceInThrowable`设置为true的话(默认就是true的),意味着我们抛出来的异常正确情况下都将是有堆栈的,那根据`PreallocatedOutOfMemoryErrorCount`这个参数来决定预先创建几个OutOfMemoryError异常对象,但是这个参数除非在debug版本下可以被设置之外,正常release出来的版本其实是无法设置这个参数的,它会是一个常量,值为4,因此在jvm启动的时候会预先创建4个OutOfMemoryError异常对象,但是这几个异常对象的堆栈,是可以动态设置的,比如说某个地方要抛出OutOfMemoryError异常了,于是先从预存的OutOfMemoryError里取出一个(其他是预存的对象还有),将此时的堆栈填上,然后抛出来,并且这个对象的使用是一次性的,也就是这个对象被抛出之后将不会再次被利用,直到预设的这几个OutOfMemoryError对象被用完了,那接下来抛出的异常都将是一开始缓存的那几个无栈的OutOfMemoryError对象。
这就是我们看到的最多出现4次有堆栈的OutOfMemoryError异常及大部分情况下都将看到没有堆栈的OutOfMemoryError对象的原因。
如何分析OutOfMemoryError异常
既然看堆栈也没什么意义,那只能从提示上入手了,我们看到这类异常,首先要确定的到底是哪块内存何种情况导致的内存溢出,比如说是Perm导致的,那抛出来的异常信息里会带有`Perm`的关键信息,那我们应该重点看Perm的大小,以及Perm里的内容;如果是Heap的,那我们就必须做内存Dump,然后分析为什么会发生这样的情况,内存里到底存了什么对象,至于内存分析的最佳的分析工具自然是MAT啦,不了解的请google之。
原文链接:http://blog.tingyun.com/web/article/detail/1210
【寒泉子】目前在阿里从事JVM相关工作,为各业务系统做性能优化,性能问题分析,之前主要从事支付宝框架容器的开发
蚂蚁金服寒泉子:JVM源码分析之临门一脚的OutOfMemoryError完全解读的更多相关文章
- JVM源码分析之临门一脚的OutOfMemoryError完全解读
概述 OutOfMemoryError,说的是java.lang.OutOfMemoryError,是JDK里自带的异常,顾名思义,说的就是内存溢出,当我们的系统内存严重不足的时候就会抛出这个异常(P ...
- JVM源码分析之一个Java进程究竟能创建多少线程
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
- JVM源码分析之堆外内存完全解读
JVM源码分析之堆外内存完全解读 寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...
- JVM源码分析-类加载场景实例分析
A类调用B类的静态方法,除了加载B类,但是B类的一个未被调用的方法间接使用到的C类却也被加载了,这个有意思的场景来自一个提问:方法中使用的类型为何在未调用时尝试加载?. 场景如下: public cl ...
- JVM源码分析之SystemGC完全解读
JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...
- JVM源码分析之Metaspace解密
概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...
- JVM源码分析-JVM源码编译与调试
要分析JVM的源码,结合资料直接阅读是一种方式,但是遇到一些想不通的场景,必须要结合调试,查看执行路径以及参数具体的值,才能搞得明白.所以我们先来把JVM的源码进行编译,并能够使用GDB进行调试. 编 ...
- JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...
- JVM源码分析之堆内存的初始化
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十五篇. 今天呢!灯塔君跟大家讲: JVM源码分析之堆内存的初始化 堆初始化 Java堆的初始化入口位于Univ ...
随机推荐
- N个数依次入栈,出栈顺序有多少种?
对于每一个数来说,必须进栈一次.出栈一次.我们把进栈设为状态‘1’,出栈设为状态‘0’.n个数的所有状态对应n个1和n个0组成的2n位二进制数.由于等待入栈的操作数按照1‥n的顺序排列.入栈的操作数b ...
- 【资源】108个大数据文档PDF开放下载-整理后打包下载
本博客所有文章分类的总目录:http://www.cnblogs.com/asxinyu/p/4288836.html 本博客其他.NET开源项目文章目录:h ...
- 升级到Windows10
1.Windows10的优点 2.需要安装的软件 实用软件: Firefox浏览器 Chrome浏览器 有道云笔记 Adobe Reader Adobe Flash Adobe PhotoShop 编 ...
- Lasso回归算法: 坐标轴下降法与最小角回归法小结
前面的文章对线性回归做了一个小结,文章在这: 线性回归原理小结.里面对线程回归的正则化也做了一个初步的介绍.提到了线程回归的L2正则化-Ridge回归,以及线程回归的L1正则化-Lasso回归.但是对 ...
- 自己实现简单的string类
1.前言 最近看了下<C++Primer>,觉得受益匪浅.不过纸上得来终觉浅,觉知此事须躬行.今天看了类类型,书中简单实现了String类,自己以前也学过C++,不过说来惭愧,以前都是用C ...
- Servlet在启动时加载的tomcat源码(原创)
tomcat 8.0.36 知识点: 通过配置loadOnStartup可以设置Servlet是否在Tomcat启动时加载,以及按值大小进行有序加载,其最小有效值为0,最大有效值为Integer.MA ...
- 相克军_Oracle体系_随堂笔记003-体系概述
1.进程结构图 对Oracle生产库来讲,服务器进程(可以简单理解是前台进程)的数量远远大于后台进程.因为一个用户进程对应了一个服务器进程. 而且后台进程一般出问题几率不大,所以学习重点也是服务器进程 ...
- 【原创】轻量级移动端即时通讯技术 MobileIMSDK 发布了
申明:MobileIMSDK目前为个人原创开源工程,投入了大量的时间和精力,希望对需要的人有所帮助.如需与作者交流,见文章底部个人签名处,互相学习.Q群:215891622,欢迎共同志趣者学习和交流. ...
- 让你分分钟学会Javascript中的闭包
Javascript中的闭包 前面的话: 闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它 ...
- Python解析非标准JSON(Key值非字符串)
采集数据的时候经常碰到一些JSON数据的Key值不是字符串,这些数据在JavaScript的上下文中是可以解析的,但在Python中,没有该部分数据的上下文,无法采用json.loads(JSON)的 ...