Crash的原因

在日常的环境中,我们偶尔也会遇到JVM Crash,与普通的错误问题不同,想要明白JVM为什么Crash是有一定难度的,所以今天我根据知识和自己的经验进行总结一下,希望也可以帮助到你。

引起Crash的直接原因可以分为两类: 代码bug内存溢出。代码bug不仅仅指应用的代码,在Oracle官网上一共分成下面几类:

有了这些原因,我怎么知道JVM是为什么Crash的呢? 不要着急,其实想要定位到直接原因非常简单,也就是下面我们要讲的定位手段。

定位手段

代码问题:

就像定位其他问题一样,JVM给我们提供了一个很直观的Crash日志,名字叫Fatal Error Log

Fatal Error Log通过参数-XX:ErrorFile=参数指定要生成的位置,如果没有指定,那么就默认在进程的目录生成。

内存问题:

如果通过JVM监控发现是内存的问题,那么建议增加-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/xxxx/Logs 两个参数,当内存溢出的时候就会生成dump文件,让我们来分析内存泄露的情况。

光说不练假把式,下面我们结合实际案例来学习一下。

实际案例

代码案例 - JVM编译代码报错

Crash现场日志

Crash日志:

# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007ff058953325, pid=46096, tid=140667078522624
#
# JRE version: Java(TM) SE Runtime Environment (8.0_20-b26) (build 1.8.0_20-b26)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.20-b23 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# V [libjvm.so+0x858325] LoadKlassNode::make(PhaseGVN&, Node*, Node*, TypePtr const*, TypeKlassPtr const*)+0x45
#
# Core dump written. Default location: /home/export/Domains/xxxxxxxx/server1/bin/core or core.46096
#
# An error report file with more information is saved as:
# /home/export/Domains/xxxxxxxx/server1/bin/hs_err_pid46096.log
#
# Compiler replay data is saved as:
# /home/export/Domains/xxxxxxx/server1/bin/replay_pid46096.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.sun.com/bugreport/crash.jsp
#

顶层堆栈:

# Problematic frame:

# V [libjvm.so+0x858325] LoadKlassNode::make(PhaseGVN&, Node*, Node*, TypePtr const*, TypeKlassPtr const*)+0x45

/home/export/Domains/xxxxxxxx/server1/bin/hs_err_pid46096.log中可以找到当时进程的具体报错详情:

Java Threads: ( => current thread )

  

--------------- P R O C E S S ---------------

  
  

C2:10611752 25130 ! 4 com.xxx.xxx.xxx.xxx::onMessage (570 bytes)

Current CompileTask:

  
  

C [libpthread.so.0+0x7e65] start_thread+0xc5

V [libjvm.so+0x8e75f8] java_start(Thread*)+0x108

V [libjvm.so+0xa29a3c] JavaThread::run()+0x11c

V [libjvm.so+0xa2990f] JavaThread::thread_main_inner()+0xdf

V [libjvm.so+0x49e420] CompileBroker::compiler_thread_loop()+0x620

V [libjvm.so+0x49ba4a] CompileBroker::invoke_compiler_on_method(CompileTask*)+0xc8a

V [libjvm.so+0x3e67e8] C2Compiler::compile_method(ciEnv*, ciMethod*, int)+0x198

V [libjvm.so+0x4931cc] Compile::Compile(ciEnv*, C2Compiler*, ciMethod*, int, bool, bool, bool)+0x126c

V [libjvm.so+0x3e7d09] ParseGenerator::generate(JVMState*, Parse*)+0x99

V [libjvm.so+0x90fd58] Parse::Parse(JVMState*, ciMethod*, float, Parse*)+0x7b8

V [libjvm.so+0x90b967] Parse::do_all_blocks()+0x127

V [libjvm.so+0x90b6f0] Parse::do_one_block()+0x180

V [libjvm.so+0x908b0a] Parse::do_exceptions()+0xba

V [libjvm.so+0x551cb6] Parse::catch_inline_exceptions(SafePointNode*)+0x946

V [libjvm.so+0x858325] LoadKlassNode::make(PhaseGVN&, Node*, Node*, TypePtr const*, TypeKlassPtr const*)+0x45

Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)

Stack: [0x00007fef9b2f4000,0x00007fef9b3f4000], sp=0x00007fef9b3f0d20, free space=1011k

Crash分析

首先是对信号的分析。下面这个是信号的字段解释说明,从信号类型(SIGSEGV)来看,我们是访问内存失败导致的Crash。可以通过下面两个链接了解更多的内容。

  1. SIGSEGV的解释
  2. 信号说明
#  SIGSEGV (0xb) at pc=0x00007ff058953325,pid=46096, tid=140667078522624
      |      |           |                    |         +--- thread id
      |      |           |                    +------------- process id
      |      |           +--------------------------- program counter
      |      |                                        (instruction pointer)
      |      +--------------------------------------- signal number
      +---------------------------------------------- signal name

接下来是对堆栈的分析。V代表了VM Frame。

# Problematic frame:

# V [libjvm.so+0x858325] LoadKlassNode::make(PhaseGVN&, Node*, Node*, TypePtr const*, TypeKlassPtr const*)+0x45
  |              +-- Same as pc, but represented as library name and offset.
  |                  For position-independent libraries (JVM and most shared
  |                  libraries), it is possible to inspect the instructions
  |                  that caused the crash without a debugger or core file
  |                  by using a disassembler to dump instructions near the
  |                  offset.
  +----------------- Frame type

Frame的Type类型如下。尝试去做一下翻译,但是总觉得还是原文比较好。

Frame Type

Description

C

Native C frame

j

Interpreted Java frame

V

VM frame

v

VM-generated stub frame

J

Other frame types, including compiled Java Frames

最后是Crash的线程信息:

可以看到基本都是和C2有关。

解决手段

通过对上述信息的搜索,怀疑是JVM在JIT编译的时候出现了问题。具体的BUG链接JDK-6675699,经过优化后的代码,在循环的时候会访问到非法的地址。

修复的方法也很简单:

  1. 禁用JIT编译
  2. 升级JDK到8u40以上

我们采用的是升级JDK,而随后的表现也被证明的确解决了该问题。

内存泄露 - 超级大的年轻代

现象
在JVM启动后运行一段时间,就会自动挂掉。

分析
从JVM监控上看,没有YGC和FullGC的出现,内存是一直在涨,导致最后申请不到内存而挂掉。通过没有GC,就可以知道是内存设置的问题,所以就去检查了xms和Xmx的设置。

原因
因为容器本身内存比较大,16G内存,所以在设置Xmx的时候误将14G写成140G,所以导致内存使用一直在上涨,直到最终失败。

解决手段
把内存设置为14G,解决问题。

虽然这个内存泄露看起来很简单,不太正经,下面这个就是正经的内存泄露了。

延伸阅读

140G的年轻代有多大呢? 按照默认的是1:2,然后计算下来大约是46G。命名是16G内存的机器,为什么能够启动成功呢?

在staickoverflow上找到一个回答:

至少在Sun 64位的Window版本上,ObjectStartArray::initialize将512字节使用1个字节来表示占用。使用35TB的堆将导致VM需要立即分配70GB,所以这个启动失败了。
但在32位 VM上,在计算最大堆内存的时候,并不会关心物理内存有多少,在Windows和Linux上它被限制在2GB左右,在Soloaris上被限制在4GB左右,如果超过这个数值,那么在启动的时候就会立即失败。
如果你开始考虑设置对的最大可用内存这件事情,你会发现去考虑物理内存并不会有多大的意义。 X GB的物理内存并不会代表有X GB的内存可以给VM去使用,物理内存是被其他进程共享的,因此VM需要找到一个方法来解决堆内存的需求要比实际可用的内存多这个情况。如果VM没有崩溃,但是在分配时无法申请到更多的物理内存,那么就会抛出OutOfMemoryErrors异常。

总结下来是在说:

  1. 因为物理内存的最大可用是不稳定的,所以VM允许申请更大的内存,如果内存分配失败,那么就抛出来OutOfMemoryErrors。
  2. 但这个更大又不是绝对无意义上的大,在本机(macos)的实验上,超过512倍并不会失败,但是乘以10就会失败,也证明了是有上限的。

原链接: [Why am I able to set -Xmx to a value greater than physical and virtual memory on the machine on both Windows and Solaris?](https://stackoverflow.com/questions/1949904/why-am-i-able-to-set-xmx-to-a-value-greater-than-physical-and-virtual-memory-on)

内存泄露 - 三方图片导致内存泄露

现象
解决了超级大的年轻代问题后,但是发现JVM在运行过一段比较长的时间又会Crash。

分析

通过JVM监控发现内存这次使用的很正常,在缓缓上涨然后YGC和FullGC都有,不正常的是在最后Crash的时候,发现JVM一直在FullGC,但是内存却下不去。

去配置的目录中,找到生成内存溢出生成的dump文件,可以判断是内存溢出了。遇见内存溢出不可怕,好的工具能够事半功倍,这里推荐用JProfiler,的确好用很多。

使用JProfiler打开生成的dump文件,首先按Class的大小排序,可以看到是byte[]数组占用最大。

java crash原理 jvm crash原因_java

接下来,可以通过byte[]数据的引用是谁来发现问题。通过incoming reference可以发现基本上都会有java.awt.image.BufferedImage

java crash原理 jvm crash原因_JVM_02


发现了可疑对象,但这个类是sun的类,按道理不会出问题,所以我们要怀疑它的持有者,通过merged-incoming-reference可以看到都是org.fit.cssbox.layout.ImageCache,这个类是我们为了图片转换引入的类。

java crash原理 jvm crash原因_开发语言_03

通过看它的源码,印证了这个结论。这个三方包在图片转换中,会有一个全局的map,只进不出,所以在转换的数量越多,它占用的空间就越大。

解决手段

  1. 在src包下新建相同路径相同名称的类,这样因为tomcat的classloader顺序,就会使用这个class
  2. 因为没有在文档和代码发现明显的不缓存配置选项,需要fork之后自己修改一下。

延伸拓展

通常我们说内存泄露,就是指堆的异常,但并不是只有堆上出了问题才会导致内存溢出。下面还有几种类型:

  1. GC占用时间异常。 Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded。
  2. array这种内存连续的申请过大。Exception in thread thread_name: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  3. 元数据空间。 Exception in thread thread_name: java.lang.OutOfMemoryError: Metaspace
  4. 系统申请内存错误。Exception in thread thread_name: java.lang.OutOfMemoryError: request size bytes for reason. Out of swap space?
  5. 这个是指在64位机器上,可以使用32位的方式来代表Class元数据,这个空间的大小是被CompressedClassSpaceSize来控制的。Exception in thread thread_name: java.lang.OutOfMemoryError: Compressed class space。
  6. 这个是指JNI方法申请内存失败。Exception in thread thread_name: java.lang.OutOfMemoryError: reason stack_trace_with_native_method

一点心得

对于内存泄露来说,我们可以从网上看到很多文章,比较多的是在说可以直接找到三方或者自己应用的类,但真实的情况比这个要复杂很多。可能看不到三方的类,也可能计算出来的大小并不对,heap是否经过gc后的大小等等问题,都让假设验证这样的模式成为最佳的解决手段。