宁静·致远


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • RSS

高性能Java-缓存

发表于 2019-01-27 | 阅读次数

前言

随着互联网的普及,内容信息越来越复杂,用户数和访问量越来越大,我们的应用需要支撑更多的并发量,同时我们的应用服务器和数据库服务器所做的计算也越来越多。但是往往我们的应用服务器资源是有限的,且技术变革是缓慢的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的),如何能够有效利用有限的资源来提供尽可能大的吞吐量?一个有效的办法就是引入缓存,打破标准流程,每个环节中请求可以从缓存中直接获取目标数据并返回,从而减少计算量,有效提升响应速度,让有限的资源服务更多的用户。

缓存策略

FIFO

First in first out,先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。

LRU

Least recently used,最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。

LFU

Less frequently used,最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。

本地缓存

指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。

集中式缓存

指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。

缓存工具

Guava cache

Guava Cache是Google开源的Java重用工具集库Guava里的一款缓存工具,它的设计灵感来源于ConcurrentHashMap,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。
Guava cache主要实现的缓存功能有:

  • 自动将entry节点加载进缓存结构中;
  • 当缓存的数据超过设置的最大值时,使用LRU算法移除;
  • 具备根据entry节点上次被访问或者写入时间计算它的过期机制;
  • 缓存的key被封装在WeakReference引用内;
  • 缓存的Value被封装在WeakReference或SoftReference引用内;
  • 统计缓存使用过程中命中率、异常率、未命中率等统计数据。

Guava Cache基于ConcurrentHashMap的优秀设计借鉴,在高并发场景支持和线程安全上都有相应的改进策略,使用Reference引用命令,提升高并发下的数据访问速度并保持了GC的可回收,有效节省空间;同时,write链和access链的设计,能更灵活、高效的实现多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等;编程式的build生成器管理,让使用者有更多的自由度,能够根据不同场景设置合适的模式。
但需要注意的是,guava cache对于缓存的过期处理是在读/写的时候处理的,也就意味着它没有独立的线程去处理缓存过期,这多少会对读写性能造成影响。

Caffeine cahce

Ehcache

Ehcache是​​一个基于标准的开源缓存,可提高性能,减轻数据库负载并简化可伸缩性。它是最广泛使用的基于Java的缓存,因为它健壮、可靠、功能齐全并与其他流行的库和框架集成。Ehcache从进程内缓存扩展到混合了TB级缓存的进程内/进程外混合部署。hibernate就使用Ehcache作为其缓存组件。
主要特性:

  • 快速,针对大型高并发系统场景,Ehcache的多线程机制有相应的优化改善。
  • 简单,很小的jar包,简单配置就可直接使用,单机场景下无需过多的其他服务依赖。
  • 支持多种的缓存策略,灵活。
  • 缓存数据有两级:内存和磁盘,与一般的本地内存缓存相比,有了磁盘的* 存储空间,将可以支持更大量的数据缓存需求。
  • 具有缓存和缓存管理器的侦听接口,能更简单方便的进行缓存实例的监控管理。
  • 支持多缓存管理器实例,以及一个实例的多个缓存区域。

    OSCache

[转]Java堆外内存增长问题排查Case

发表于 2019-01-22 | 阅读次数

Java堆外内存增长问题排查Case

文章转自:https://coldwalker.com/2018/08//troubleshooter_native_memory_increase/
最近排查一个线上java服务常驻内存异常高的问题,大概现象是:java堆Xmx配置了8G,但运行一段时间后常驻内存RES从5G逐渐增长到13G #补图#,导致机器开始swap从而服务整体变慢。
由于Xmx只配置了8G但RES常驻内存达到了13G,多出了5G堆外内存,经验上判断这里超出太多不太正常。

前情提要–JVM内存模型

开始逐步对堆外内存进行排查,首先了解一下JVM内存模型。根据JVM规范,JVM运行时数据区共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
o_dc695f48-4189-4fc7-b950-ed25f6c1521708518830

  • 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值。虚拟机栈除了上述错误外,还有另一种错误,那就是当申请不到空间时,会抛出 OutOfMemoryError。

  • 本地方法栈:与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。本地方法栈也可以抛出StackOverflowError和OutOfMemoryError。

  • PC 寄存器,也叫程序计数器。可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。倘若当前线程执行的是 JAVA 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空。

  • 堆内存。堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。

  • 方法区也是所有线程共享。主要用于存储类的信息、常量池、静态变量、及时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

前情提要–PermGen(永久代)和 Metaspace(元空间)

PermGen space 和 Metaspace是HotSpot对于方法区的不同实现。在Java虚拟机(以下简称JVM)中,类包含其对应的元数据,比如类名,父类名,类的类型,访问修饰符,字段信息,方法信息,静态变量,常量,类加载器的引用,类的引用。在HotSpot JDK 1.8之前这些类元数据信息存放在一个叫永久代的区域(PermGen space),永久代一段连续的内存空间。在JDK 1.8开始,方法区实现采用Metaspace代替,这些元数据信息直接使用本地内存来分配。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

堆外内存

java 8下是指除了Xmx设置的java堆(java 8以下版本还包括MaxPermSize设定的持久代大小)外,java进程使用的其他内存。主要包括:DirectByteBuffer分配的内存,JNI里分配的内存,线程栈分配占用的系统内存,jvm本身运行过程分配的内存,codeCache,java 8里还包括metaspace元数据空间。

分析java堆

由于现象是RES比较高,先看一下java堆是否有异常。把java堆dump下来仔细排查一下,jmap -histo:live pid,发现整个堆回收完也才几百兆,远不到8G的Xmx的上限值,GC日志看着也没啥异常。基本排查java堆内存泄露的可能性。


分析DirectByteBuffer的占用

DirectByteBuffer简单了解

由于服务使用的RPC框架底层采用了Netty等NIO框架,会使用到DirectByteBuffer这种“冰山对象”,先简单排查一下。关于DirectByteBuffer先介绍一下:JDK 1.5之后ByteBuffer类提供allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现,会调用malloc方法进行内存分配。实际上,在java堆里是维护了一个记录堆外地址和大小的DirectByteBuffer的对象,所以GC是能通过操作DirectByteBuffer对象来间接操作对应的堆外内存,从而达到释放堆外内存的目的。但如果一旦这个DirectByteBuffer对象熬过了young GC到达了Old区,同时Old区一直又没做CMS GC或者Full GC的话,这些“冰山对象”会将系统物理内存慢慢消耗掉。对于这种情况JVM留了后手,Bits给DirectByteBuffer前首先需要向Bits类申请额度,Bits类维护了一个全局的totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限(堆外内存的限额默认与堆内内存Xmx设定相仿),如果已经超限,会主动执行Sytem.gc(),System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存。但如果启动时通过-DisableExplicitGC禁止了System.gc(),那么这里就会出现比较严重的问题,导致回收不了DirectByteBuffer底下的堆外内存了。所以在类似Netty的框架里对DirectByteBuffer是框架自己主动回收来避免这个问题。


DirectByteBuffer为什么要用堆外内存

DirectByteBuffer是直接通过native方法使用malloc分配内存,这块内存位于java堆之外,对GC没有影响;其次,在通信场景下,堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。所以DirectByteBuffer一般用于通信过程中作为缓冲池来减少内存拷贝。当然,由于直接用malloc在OS里申请一段内存,比在已申请好的JVM堆内内存里划一块出来要慢,所以在Netty中一般用池化的 PooledDirectByteBuf 对DirectByteBuffer进行重用进一步提升性能。

如何排查DirectByteBuffer的使用情况

JMX提供了监控direct buffer的MXBean,启动服务时开启-Dcom.sun.management.jmxremote.port=9527 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=10.79.40.147,JMC挂上后运行一段时间,此时Xmx是8G的情况下整体RES逐渐增长到13G,MBean里找到java.nio.BufferPool下的direct节点,查看direct buffer的情况,发现总共才213M。为了进一步排除,在启动时通过-XX:MaxDirectMemorySize来限制DirectByteBuffer的最大限额,调整为1G后,进程整体常驻内存的增长并没有限制住,因此这里基本排除了DirectByteBuffer的嫌疑。


使用NMT排查JVM原生内存使用

Native Memory Tracking(NMT)使用

NMT是Java7U40引入的HotSpot新特性,可用于监控JVM原生内存的使用,但比较可惜的是,目前的NMT不能监控到JVM之外或原生库分配的内存。java进程启动时指定开启NMT(有一定的性能损耗),输出级别可以设置为“summary”或“detail”级别。如:

-XX:NativeMemoryTracking=summary 或者 -XX:NativeMemoryTracking=detail

开启后,通过jcmd可以访问收集到的数据。

jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff 

如:jcmd 11 VM.native_memory,输出如下:

Native Memory Tracking:

Total: reserved=12259645KB(保留内存), committed=11036265KB (提交内存)
堆内存使用情况,保留内存和提交内存和Xms、Xmx一致,都是8G。
-                 Java Heap (reserved=8388608KB, committed=8388608KB)
                            (mmap: reserved=8388608KB, committed=8388608KB)
用于存储类元数据信息使用到的原生内存,总共12045个类,整体实际使用了79M内存。
-                     Class (reserved=1119963KB, committed=79751KB)
                            (classes #12045)
                            (malloc=1755KB #29277)
                            (mmap: reserved=1118208KB, committed=77996KB)
总共2064个线程,提交内存是2.1G左右,一个线程1M,和设置Xss1m相符。
-                    Thread (reserved=2130294KB, committed=2130294KB)
                            (thread #2064)
                            (stack: reserved=2120764KB, committed=2120764KB)
                            (malloc=6824KB #10341)
                            (arena=2706KB #4127)
JIT的代码缓存,12045个类JIT编译后代码缓存整体使用79M内存。
-                      Code (reserved=263071KB, committed=79903KB)
                            (malloc=13471KB #15191)
                            (mmap: reserved=249600KB, committed=66432KB)
GC相关使用到的一些堆外内存,比如GC算法的处理锁会使用一些堆外空间。118M左右。
-                        GC (reserved=118432KB, committed=118432KB)
                            (malloc=93848KB #453)
                            (mmap: reserved=24584KB, committed=24584KB)
JAVA编译器自身操作使用到的一些堆外内存,很少。
-                  Compiler (reserved=975KB, committed=975KB)
                            (malloc=844KB #1074)
                            (arena=131KB #3)
Internal:memory used by the command line parser, JVMTI, properties等。
-                  Internal (reserved=117158KB, committed=117158KB)
                            (malloc=117126KB #44857)
                            (mmap: reserved=32KB, committed=32KB)
Symbol:保留字符串(Interned String)的引用与符号表引用放在这里,17M左右
-                    Symbol (reserved=17133KB, committed=17133KB)
                            (malloc=13354KB #145640)
                            (arena=3780KB #1)
NMT本身占用的堆外内存,4M左右
-    Native Memory Tracking (reserved=4402KB, committed=4402KB)
                            (malloc=396KB #5287)
                            (tracking overhead=4006KB)
不知道啥,用的很少。
-               Arena Chunk (reserved=272KB, committed=272KB)
                            (malloc=272KB)
其他未分类的堆外内存占用,100M左右。
-                   Unknown (reserved=99336KB, committed=99336KB)
                            (mmap: reserved=99336KB, committed=99336KB)
  • 保留内存(reserved):reserved memory 是指JVM 通过mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries),保证了其他进程不会被占用,且保证了逻辑地址的连续性,能简化指针运算。

  • 提交内存(commited):committed memory 是JVM向操做系统实际分配的内存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,仍然会page faults,但是跟 reserved 不同,完全内核处理像什么也没发生一样。

这里需要注意的是:由于malloc/mmap的lazy allocation and paging机制,即使是commited的内存,也不一定会真正分配物理内存。

malloc/mmap is lazy unless told otherwise. Pages are only backed by physical memory once they're accessed.

Tips:由于内存是一直在缓慢增长,因此在使用NMT跟踪堆外内存时,一个比较好的办法是,先建立一个内存使用基线,一段时间后再用当时数据和基线进行差别比较,这样比较容易定位问题。

jcmd 11 VM.native_memory baseline

同时pmap看一下物理内存的分配,RSS占用了10G。

pmap -x 11 | sort -n -k3



运行一段时间后,做一下summary级别的diff,看下内存变化,同时再次pmap看下RSS增长情况。

jcmd 11 VM.native_memory summary.diff
Native Memory Tracking:

Total: reserved=13089769KB +112323KB, committed=11877285KB +117915KB

-                 Java Heap (reserved=8388608KB, committed=8388608KB)
                            (mmap: reserved=8388608KB, committed=8388608KB)

-                     Class (reserved=1126527KB +2161KB, committed=85771KB +2033KB)
                            (classes #12682 +154)
                            (malloc=2175KB +113KB #37289 +2205)
                            (mmap: reserved=1124352KB +2048KB, committed=83596KB +1920KB)

-                    Thread (reserved=2861485KB +94989KB, committed=2861485KB +94989KB)
                            (thread #2772 +92)
                            (stack: reserved=2848588KB +94576KB, committed=2848588KB +94576KB)
                            (malloc=9169KB +305KB #13881 +460)
                            (arena=3728KB +108 #5543 +184)

-                      Code (reserved=265858KB +1146KB, committed=94130KB +6866KB)
                            (malloc=16258KB +1146KB #18187 +1146)
                            (mmap: reserved=249600KB, committed=77872KB +5720KB)

-                        GC (reserved=118433KB +1KB, committed=118433KB +1KB)
                            (malloc=93849KB +1KB #487 +24)
                            (mmap: reserved=24584KB, committed=24584KB)

-                  Compiler (reserved=1956KB +253KB, committed=1956KB +253KB)
                            (malloc=1826KB +253KB #2098 +271)
                            (arena=131KB #3)

-                  Internal (reserved=203932KB +13143KB, committed=203932KB +13143KB)
                            (malloc=203900KB +13143KB #62342 +3942)
                            (mmap: reserved=32KB, committed=32KB)

-                    Symbol (reserved=17820KB +108KB, committed=17820KB +108KB)
                            (malloc=13977KB +76KB #152204 +257)
                            (arena=3844KB +32 #1)

-    Native Memory Tracking (reserved=5519KB +517KB, committed=5519KB +517KB)
                            (malloc=797KB +325KB #9992 +3789)
                            (tracking overhead=4722KB +192KB)

-               Arena Chunk (reserved=294KB +5KB, committed=294KB +5KB)
                            (malloc=294KB +5KB)

-                   Unknown (reserved=99336KB, committed=99336KB)
                            (mmap: reserved=99336KB, committed=99336KB



发现这段时间pmap看到的RSS增长了3G多,但NMT观察到的内存增长了不到120M,还有大概2G多常驻内存不知去向,因此也基本排除了由于JVM自身管理的堆外内存的嫌疑。

排查Metaspace元空间的堆外内存占用

由于线上使用的是JDK8,前面提到,JDK8里的元空间实际上使用的也是堆外内存,默认没有设置元空间大小的情况下,元空间最大堆外内存大小和Xmx是一致的。JMC连上后看下内存tab下metaspace一栏的内存占用情况,发现元空间只占用不到80M内存,也排除了它的可能性。实在不放心的话可以通过-XX:MaxMetaspaceSize设置元空间使用堆外内存的上限。


gdb分析内存块内容

上面提到使用pmap来查看进程的内存映射,pmap命令实际是读取了/proc/pid/maps和/porc/pid/smaps文件来输出。发现一个细节,pmap取出的内存映射发现很多64M大小的内存块。这种内存块逐渐变多且占用的RSS常驻内存也逐渐增长到reserved保留内存大小,内存增长的2G多基本上也是由于这些64M的内存块导致的,因此看一下这些内存块里具体内容。

strace挂上监控下内存分配和回收的系统调用:
strace -o /data1/weibo/logs/strace_output2.txt -T -tt -e mmap,munmap,mprotect -fp 12

看内存申请和释放的情况:

cat ../logs/strace_output2.txt | grep mprotect | grep -v resumed | awk '{print int($4)}' | sort -rn | head -5

cat ../logs/strace_output2.txt | grep mmap | grep -v resumed | awk '{print int($4)}' | sort -rn | head -5

cat ../logs/strace_output2.txt | grep munmap | grep -v resumed | awk '{print int($4)}' | sort -rn | head -5

配合pmap -x 10看一下实际内存分配情况:


找一块内存块进行dump:

gdb --batch --pid 11 -ex "dump memory a.dump 0x7fd488000000 0x7fd488000000+56124000"

简单分析一下内容,发现绝大部分是乱码的二进制内容,看不出什么问题。
strings a.dump | less
或者: hexdump -C a.dump | less
或者: view a.dump

没啥思路的时候,随便搜了一下发现貌似很多人碰到这种64M内存块的问题(比如这里),了解到glibc的内存分配策略在高版本有较大调整:

«从glibc 2.11(为应用系统在多核心CPU和多Sockets环境中高伸缩性提供了一个动态内存分配的特性增强)版本开始引入了per thread arena内存池,Native Heap区被打散为sub-pools ,这部分内存池叫做Arena内存池。也就是说,以前只有一个main arena,目前是一个main arena(还是位于Native Heap区) + 多个per thread arena,多个线程之间不再共用一个arena内存区域了,保证每个线程都有一个堆,这样避免内存分配时需要额外的锁来降低性能。main arena主要通过brk/sbrk系统调用去管理,per thread arena主要通过mmap系统调用去分配和管理。»

_«一个32位的应用程序进程,最大可创建 2 CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为1MB,一个64位的应用程序进程,最大可创建 8 CPU总核数个arena内存池(MALLOC_ARENA_MAX),每个arena内存池大小为64MB»_

ptmalloc2内存分配和释放

«当某一线程需要调用 malloc()分配内存空间时, 该线程先查看线程私有变量中是否已经存在一个分配区,如果存在, 尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,如果失败, 该线程搜索循环链表试图获得一个没有加锁的分配区。如果所有的分配区都已经加锁,那么 malloc()会开辟一个新的分配区,把该分配区加入到全局分配区循环链表并加锁,然后使用该分配区进行分配内存操作。在释放操作中,线程同样试图获得待释放内存块所在分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的互斥锁之后才可以进行释放操作。用户 free 掉的内存并不是都会马上归还给系统,ptmalloc2 会统一管理 heap 和 mmap 映射区域中的空闲的chunk,当用户进行下一次分配请求时, ptmalloc2 会首先试图在空闲的chunk 中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。»

ptmalloc2的内存收缩机制

«业务层调用free方法释放内存时,ptmalloc2先判断 top chunk 的大小是否大于 mmap 收缩阈值(默认为 128KB),如果是的话,对于主分配区,则会试图归还 top chunk 中的一部分给操作系统。但是最先分配的 128KB 空间是不会归还的,ptmalloc 会一直管理这部分内存,用于响应用户的分配 请求;如果为非主分配区,会进行 sub-heap 收缩,将 top chunk 的一部分返回给操 作系统,如果 top chunk 为整个 sub-heap,会把整个 sub-heap 还回给操作系统。做 完这一步之后,释放结束,从 free() 函数退出。可以看出,收缩堆的条件是当前 free 的 chunk 大小加上前后能合并 chunk 的大小大于 64k,并且要 top chunk 的大 小要达到 mmap 收缩阈值,才有可能收缩堆。»

ptmalloc2的mmap分配阈值动态调整

«M_MMAP_THRESHOLD 用于设置 mmap 分配阈值,默认值为 128KB,ptmalloc 默认开启 动态调整 mmap 分配阈值和 mmap 收缩阈值。当用户需要分配的内存大于 mmap 分配阈值,ptmalloc 的 malloc()函数其实相当于 mmap() 的简单封装,free 函数相当于 munmap()的简单封装。相当于直接通过系统调用分配内存, 回收的内存就直接返回给操作系统了。因为这些大块内存不能被 ptmalloc 缓存管理,不能重用,所以 ptmalloc 也只有在万不得已的情况下才使用该方式分配内存。»

业务特性和ptmalloc2内存分配的gap

当前业务并发较大,线程较多,内存申请时容易造成锁冲突申请多个arena,另外该服务涉及到图片的上传和处理,底层会比较频繁的通过JNI调用ImageIO的图片读取方法(com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage),经常会向glibc申请10M以上的buffer内存,考虑到ptmalloc2的lazy回收机制和mmap分配阈值动态调整默认打开,对于这些申请的大内存块,使用完后仍然会停留在arena中不会归还,同时也比较难得到收缩的机会去释放(当前回收的chunk和top chunk相邻,且合并后大于64K)。因此在这种较高并发的多线程业务场景下,RES的增长也是不可避免。

如何优化解决
三种方案:

第一种:控制分配区的总数上限。默认64位系统分配区数为:cpu核数*8,如当前环境16核系统分配区数为128个,每个64M上限的话最多可达8G,限制上限后,后续不够的申请会直接走mmap分配和munmap回收,不会进入ptmalloc2的buffer池。
所以第一种方案调整一下分配池上限个数到4:

export MALLOC_ARENA_MAX=4

第二种:之前降到ptmalloc2默认会动态调整mmap分配阈值,因此对于较大的内存请求也会进入ptmalloc2的内存buffer池里,这里可以去掉ptmalloc的动态调整功能。可以设置 M_TRIM_THRESHOLD,M_MMAP_THRESHOLD,M_TOP_PAD 和 M_MMAP_MAX 中的任意一个。这里可以固定分配阈值为128K,这样超过128K的内存分配请求都不会进入ptmalloc的buffer池而是直接走mmap分配和munmap回收(性能上会有损耗,当前环境大概10%)。:

export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536   

第三种:使用tcmalloc来替代默认的ptmalloc2。google的tcmalloc提供更优的内存分配效率,性能更好,ThreadCache会阶段性的回收内存到CentralCache里。 解决了ptmalloc2中arena之间不能迁移导致内存浪费的问题。

tcmalloc安装使用
1.实现原理

perf-tools实现原理是:在java应用程序运行时,当系统分配内存时调用malloc时换用它的libtcmalloc.so,也就是TCMalloc会自动替换掉glibc默认的malloc和free,这样就能做一些统计。使用TCMalloc(Thread-Caching Malloc)与标准的glibc库的malloc相比,TCMalloc在内存的分配上效率和速度要高,==了解更多TCMalloc

2. 安装和使用
2.1 前置工具的安装
yum -y install gcc make
yum -y install gcc gcc-c++
yum -y perl
2.2 libunwind

使用perf-tools的TCMalloc,在64bit系统上需要先安装libunwind(http://download.savannah.gnu.org/releases/libunwind/libunwind-1.2.tar.gz,只能是这个版本),这个库为基于64位CPU和操作系统的程序提供了基本的堆栈辗转开解功能,其中包括用于输出堆栈跟踪的API、用于以编程方式辗转开解堆栈的API以及支持C++异常处理机制的API,32bit系统不需安装。

tar zxvf libunwind-1.2.tar.gz
./configure
make
make install
make clean
2.3 perf-tools

从https://github.com/gperftools/gperftools下载相应的google-perftools版本。

tar zxvf google-perftools-2.7.tar.gz
./configure
make
make install
make clean
#修改lc_config,加入/usr/local/lib(libunwind的lib所在目录)
echo "/usr/local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf 
#使libunwind生效
ldconfig
2.3.1 关于etc/ld.so.conf

这个文件记录了编译时使用的动态链接库的路径。默认情况下,编译器只会使用/lib和/usr/lib这两个目录下的库文件。
如果你安装了某些库,比如在安装gtk+-2.4.13时它会需要glib-2.0 >= 2.4.0,辛苦的安装好glib后没有指定 –prefix=/usr 这样glib库就装到了/usr/local下,而又没有在/etc/ld.so.conf中添加/usr/local/lib。
库文件的路径如 /usr/lib 或 /usr/local/lib 应该在 /etc/ld.so.conf 文件中,这样 ldd 才能找到这个库。在检查了这一点后,要以 root 的身份运行 /sbin/ldconfig。
将/usr/local/lib加入到/etc/ld.so.conf中,这样安装gtk时就会去搜索/usr/local/lib,同样可以找到需要的库

2.3.2 关于ldconfig

ldconfig的作用就是将/etc/ld.so.conf列出的路径下的库文件 缓存到/etc/ld.so.cache 以供使用
因此当安装完一些库文件,(例如刚安装好glib),或者修改ld.so.conf增加新的库路径后,需要运行一下/sbin/ldconfig
使所有的库文件都被缓存到ld.so.cache中,如果没做,即使库文件明明就在/usr/lib下的,也是不会被使用的

2.4 为perf-tools添加线程目录
mkdir /data1/weibo/logs/gperftools/tcmalloc/heap
chmod 0777 /data1/weibo/logs/gperftools/tcmalloc/heap
2.5 修改tomcat启动脚本

catalina.sh里添加:

ldconfig
export LD_PRELOAD=/usr/local/lib/libtcmalloc.so
export HEAPPROFILE=/data1/weibo/logs/gperftools/tcmalloc/heap

修改后重启tomcat的容器。

2.5.1 关于LD_PRELOAD

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。更多关于LD_PRELOAD

经验证上面三种方式都能有效解决常驻内存持续增长的问题。

参考:
http://www.infoq.com/cn/articles/Java-PERMGEN-Removed
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/nmt-8.html
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr022.html#BABCBGFA
http://zhanjindong.com/2016/03/02/jvm-memory-tunning-notes
http://www.cnblogs.com/duanxz/archive/2012/08/09/2630284.html
https://stackoverflow.com/questions/31173374/why-does-a-jvm-report-more-committed-memory-than-the-linux-process-resident-set
https://siddhesh.in/posts/malloc-per-thread-arenas-in-glibc.html
https://www.sourceware.org/systemtap/SystemTap_Beginners_Guide/using-systemtap.html
http://debuginfo.centos.org/7/x86_64/

高性能Java-序列化

发表于 2019-01-19 | 阅读次数

前言

JSON

ProtoBuf

ProtoStuff

hessian

高性能Java-集合

发表于 2019-01-12 | 阅读次数

前言

集合是我们在编写代码过程中常用的数据类型。在Java中,常用的集合类型有List、Map和Set。本文将对一些常用的集合类型的特点进行分析,并针对一些会影响性能的注意事项进行说明。

1. 集合类型

1.1 List

1.1.1 ArrayList

顾名思义,基于数组实现,同时也说明容量是确定的,非线程安全。在不指定初始化容量的情况下,默认初始化的大小是10。当增加元素并且超出容量时,会以原容量50%的尺寸进行扩容,扩容时使用Arrays.copyOf(底层使用System.arraycopy)进行数组拷贝。
ArrayList适用于读多写少的场景,因为基于下标访问其中的元素,所以性能比较高。但在往指定位置add或者remove指定位置的元素时,由于涉及到指定位置后面元素的移动,会对性能造成影响。越是对前面位置元素的add/remove操作,对性能造成的影响越大。

1.2.2 LinkedList

基于双向链表实现,非线程安全。既然是链表,那么容量就可以是无限的了。
在按下标读取链表中的元素时,需要遍历部分链表进行指针移动,这样效率就比较低下了。但在add/remove指定位置的元素时,不再需要复制移动,只需修改前后节点的指针即可,所以适用于写多读少的场景。
这里扩展一下:在制定线程池的队列时,我们可以用ArrayBlockingQueue,也可以用LinkedBlockingQueue。那什么时候用LinkedBlockingQueue呢?答案是并发量比较大的时候,因为队列需要频繁入栈出栈,也就是写多读少,所以基于链表原理实现的LinkedBlockingQueue就比较适合了。

1.2.3 CopyOnWriteArrayList

线程安全的List。基于不可变对象策略,在修改时先复制出一个数组快照来修改,改好了,再让内部指针指向新数组。
因为对快照的修改对读操作来说不可见,所以读读之间不互斥,读写之间也不互斥,只有写写之间要加锁互斥。但复制快照的成本昂贵,典型的适合读多写少的场景。如果更新频率较高或数组较大时,还是得用Collections.synchronizedList(list),对所有操作用加锁来保证线程安全。

1.2 Map

1.2.1 HashMap

基于拉链法实现,非线程安全。HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap 的时候,就会初始化一个数组。
当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的元素放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

当然,每个数组里最好只有一个元素,不用去比较。所以默认当Entry数量达到数组数量的75%时,哈希冲突已比较严重,就会成倍扩容数组,并重新分配所有原来的Entry。扩容成本不低(扩容成本、内存浪费),所以也最好有个预估值,这也就是为什么我们建议在初始化时指定初始容量的原因。
HashMap不是线程安全的,在JDK7及以前的版本中,在并发情况下修改HashMap容易导致死循环,JDK8以后修改了实现,虽然不会导致死循环,但在多线程情况下写依然不安全。

1.2.2 ConcurrentHashMap

线程安全的HashMap。JDK5~JDJ7的实现是用分段锁实现。默认16把写锁(可以设置更多),有效分散了阻塞的概率。数据结构为Segment[],每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在哪个Segment里,再去算它在哪个哈希桶里。
也没有读锁,因为put/remove动作是个原子动作(比如put的整个过程是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。
但在JDK8里,抛弃了分段锁技术的实现,直接采用Node(继承自Map.Entry)作为table元素,采用CAS + synchronized保证并发更新的安全性,底层采用数组+链表+红黑树的存储结构。修改时,不再采用ReentrantLock加锁,直接用内置synchronized加锁,Java8的内置锁比之前版本优化了很多,相较ReentrantLock,性能不并差。获取size时,使用CounterCell内部类,用于并行计算每个bucket的元素数量。
使用ConcurrentHashMap时,需要注意put和putIfAbsent的差别。
put:与之hashMap相同,当key存在时,put同样的key将被覆盖。
putIfAbsent:当key不存在的时候调用put方法将key存入进map。当key存在的时候相当于return map.get(key)。

1.2.3 LinkedHashMap

扩展HashMap,每个Entry增加双向链表,适用于写多读少的场景。

1.2.4 TreeMap

基于红黑树实现的排序Map,非线程安全。TreeMap的增删改查和统计相关的操作的时间复杂度都为 O(logn)。
由于TreeMap是基于红黑树的实现的排序Map,对于增删改查以及统计的时间复杂度都控制在O(logn)的级别上,相对于HashMap和LikedHashMap的统计操作的(最大的key,最小的key,大于某一个key的所有Entry等等)时间复杂度O(n)具有较高时间效率,所以TreeMap比较适合用于需要基于排序的统计功能。

1.3 Set

1.3.1 HashSet

内部是HashMap,非线程安全。

1.3.2 LinkedHashSet

内部是LinkedHashMap,非线程安全。

1.3.3 TreeSet

内部是TreeMap的SortedSet,非线程安全。

1.3.4 CopyOnWriteArraySet

基于CopyOnWriteArrayList实现,线程安全。

2. 一些框架

2.1 Koloboke

Koloboke是一个比较年轻的集合框架,为所有原始数据类型或对象的组合提供了HashMap和HashSet。在一些评测中,Koloboke取得了不错的成绩,号称实现最快也是存储最高效的库,但是它诞生不久并没有被广泛使用,所以存在一定的风险。

2.2 fastutil

一个意大利的计算机博士开发的集合库。fastutil中的集合数据类型有10种:8种基本数据类型+Object+Reference,这意味着你能够适用int、long这种基本数据类型作为map的key,同样也意味着这能够节省很多的存储空间。

2.3 Eclipse Collections

Eclipse Collections 是一个高性能的 Java 集合框架,为原生 JDK 集合增加了丰富的功能。
Eclipse Collections起源于2004年在高盛公司内部开发的一个名为Caramel的集合框架。 自那以后,这个框架不断发展并在2012年开源成为GitHub中一个叫做 GS Collections的项目。为了最大限度地发挥开源项目的最佳特质,GS Collections已经被迁移到Eclipse Foundation,并在2015年被重新命名为Eclipse Collections。

2.4 Trove

Trove为所有的原始数据类型、对象的组合提供了链表、栈、队列、HashSet和HashMap。Trove是一个老牌的集合框架,经过几年的迭代已经比较稳定了,目前社区已经不太活跃了。

2.5 hppc

HPPC是High Performance Primitive Collections for Java的缩写,为所有原始数据类型提供了数组列表、数组队列、哈希集合和哈希映射。虽然号称高性能,但在测评中,性能只能说一般,所以这里不再详细介绍。

3. 一些注意事项

3.1 初始化容量

集合类在不容量不足的情况下会进行扩容,而扩容操作会消耗一定的资源,所以在初始化集合的时候,尽量预估集合的尺寸并指定集合的初始化容量,这样能够避免集合频繁扩容带来的资源消耗。

3.2 遍历

集合的遍历可以用for、foreach和iterator几种方式,那么这三者的速度有什么差异呢?答案是:
如果是ArrayList,用三种方式遍历的速度是for>Iterator>foreach,速度级别基本一致;这是因为ArrayList是基于索引(index)的数组,索引在数组中搜索和读取数据的时间复杂度是O(1)。
如果是LinkedList,则三种方式遍历的差距很大了,数据量大时越明显(一般是超过100000级别),用for遍历的效率远远落后于foreach和Iterator,Iterator>foreach>>>for,因为LinkedList的底层实现则是一个双向循环带头节点的链表,因此LinkedList中插入或删除的时间复杂度仅为O(1),但是获取数据的时间复杂度却是O(n)。
明白了两种List的区别之后,就知道,ArrayList用for循环随机读取的速度是很快的,因为ArrayList的下标是明确的,读取一个数据的时间复杂度仅为O(1)。但LinkedList若是用for来遍历效率很低,读取一个数据的时间复杂度就达到了为O(n)。而用Iterator的next()则是顺着链表节点顺序读取数据的效率就很高了。
综上:

  1. ArrayList用三种遍历方式都差得不算太多,一般都会用for或者foreach,因为Iterator写法相对复杂一些;
  2. LinkedList的话,推荐使用foreach或者Iterator(数据量越大,三者方法差别明显)。

    3.3 排序

    使用堆排序(对于数组)或合并排序(对于双向链表)或快速排序(对于数组,但是你需要找个好的参考值),不要使用选择排序、冒泡排序或插入排序,除非是一个非常小的数组或列表。
    对于数组,使用java.util.Arrays.sort,是一个改进过的快速排序; 它不需要额外的内存,但是它不是稳定的(不能保证相等对象的顺序)。
    对于ArrayList和LinkedList,他们都实现了接口java.util.List,可以使用 java.util.Collections.sort来排序,它是稳定的(保证相等对象的顺序)和平滑的(对于接近排好序的列表接近线性时间), 但是它使用了额外的内存。

高性能Java-String篇

发表于 2019-01-04 | 阅读次数

前言

字符串处理是程序逻辑中比重比较大的部分,由此带来的资源消耗也是比较多的。在编写代码进行字符串处理时如果能使用一些高效的方法或工具,起码能够帮我们规避一些性能上的坑,避免日后补救。

1.字符串拼接

  1. 不要用+,虽然在JDK7U40之后编译器会将”+”优化成StringBuilder的方式,但是StringBuilder初始化的时候是不会指定其初始容量的;
  2. 用StringBuilder:切记要指定其初始容量,避免扩容造成的CPU和内存浪费,这里造成的浪费还是很可观的。具体内容详见:StringBuilder你应该知道的几件事情
  3. 用Guava Joiner:对于用相同符号间隔的字符串拼接,可以使用Guava的Joiner,用起来很方便。但需要注意的是Joiner.on每次调用都会创建一个新的Joiner实例,会造成内存浪费。同时Joiner是线程安全的,所以对于相同分隔符创建的Joiner实例,公用一个单例就可以啦。
    1
    2
    3
    4
    5
    6
    /**
    * Returns a joiner which automatically places {@code separator} between consecutive elements.
    */
    public static Joiner on(char separator) {
    return new Joiner(String.valueOf(separator));
    }

2.字符串拆分

如果不需要用正则表达式,用StringUtils.split代替String.split,因为原生的split方法支持正则表达式,会导致性能偏低。

3.字符串替换

如果不需要用正则表达式,用StringUtils.replace代替String.replace,因为原生的replace方法支持正则表达式,会导致性能偏低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Replaces each substring of this string that matches the literal target
* sequence with the specified literal replacement sequence. The
* replacement proceeds from the beginning of the string to the end, for
* example, replacing "aa" with "b" in the string "aaa" will result in
* "ba" rather than "ab".
*
* @param target The sequence of char values to be replaced
* @param replacement The replacement sequence of char values
* @return The resulting string
* @since 1.5
*/
public String replace(CharSequence target, CharSequence replacement) {
return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}

4.字符串转换

避免用String.format。如果你只是想要把一堆不同类型的参数转换成字符串,从性能的角度,建议你直接用StringBuilder实现。因为String.format其实也是用StringBuilder实现的,但由于它要解析format参数中的各种格式进行转换,导致性能降低。有人做过对比,String.format要比直接使用StringBuilder要慢5-30倍……话不多少,直接上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public Formatter format(String format, Object ... args) {
return format(l, format, args);
}

public Formatter format(Locale l, String format, Object ... args) {
ensureOpen();

// index of last argument referenced
int last = -1;
// last ordinary index
int lasto = -1;

FormatString[] fsa = parse(format);
for (int i = 0; i < fsa.length; i++) {
FormatString fs = fsa[i];
int index = fs.index();
try {
switch (index) {
case -2: // fixed string, "%n", or "%%"
fs.print(null, l);
break;
case -1: // relative index
if (last < 0 || (args != null && last > args.length - 1))
throw new MissingFormatArgumentException(fs.toString());
fs.print((args == null ? null : args[last]), l);
break;
case 0: // ordinary index
lasto++;
last = lasto;
if (args != null && lasto > args.length - 1)
throw new MissingFormatArgumentException(fs.toString());
fs.print((args == null ? null : args[lasto]), l);
break;
default: // explicit index
last = index - 1;
if (args != null && last > args.length - 1)
throw new MissingFormatArgumentException(fs.toString());
fs.print((args == null ? null : args[last]), l);
break;
}
} catch (IOException x) {
lastException = x;
}
}
return this;
}

上述代码用到了FormatString.print方法,那我们再来看看FormatString的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private interface FormatString {
int index();
void print(Object arg, Locale l) throws IOException;
String toString();
}

private class FixedString implements FormatString {
private String s;
FixedString(String s) { this.s = s; }
public int index() { return -2; }
public void print(Object arg, Locale l)
throws IOException { a.append(s); }
public String toString() { return s; }
}

FormatString是Formater的内部接口类,而FixedString实现了FormatString接口,FixedString.print方法用到了a.append()方法,看到append方法,你有没有似曾相识的赶脚呢?我们再来看看这个a是个什么鬼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final class Formatter implements Closeable, Flushable {
private Appendable a;
private final Locale l;

private IOException lastException;

private final char zero;
private static double scaleUp;

// 1 (sign) + 19 (max # sig digits) + 1 ('.') + 1 ('e') + 1 (sign)
// + 3 (max # exp digits) + 4 (error) = 30
private static final int MAX_FD_CHARS = 30;

//此处省略一些代码

private static final Appendable nonNullAppendable(Appendable a) {
if (a == null)
return new StringBuilder();

return a;
}
////此处省略一万字
}

看到了吧,它还是用的StringBuilder,而且没有指定初始化容量,这样如果字符串比较长,扩容带来的资源消耗也是蛮高的。

5.toString()方法

toString方法一般是打印日志的时候使用。在这里提一下toString方法的原因是实现toString方法的方式有很多,有手写的、有用ide生成的、有用lombok生成的。这里只提2点:

  1. 不建议使用lombok的ToString注解,因为lombok生成的toString方法是用”+”做字符串拼接的,如果打印日志频繁,这里的不必要的性能开销会比较大;
    下面的代码是使用了lombok Data和ToString注解的源代码,我们来看看经过lombok处理之后的代码是什么样子。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package io.fengfu.learning.lombok;

    import lombok.Data;
    import lombok.ToString;

    import java.util.Date;

    @ToString
    @Data
    public class Wrapper {
    private String wrapperId;
    private boolean isOneWay;
    private String state;
    private int stateCode;
    private String operator;
    private Date date;
    private String operateTime;
    private String detail;
    }

下面是lombok生成的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Wrapper {
private String wrapperId;
private boolean isOneWay;
private String state;
private int stateCode;
private String operator;
private Date date;
private String operateTime;
private String detail;

public String toString() {
return "Wrapper(wrapperId=" + getWrapperId() +
", isOneWay=" + isOneWay() +
", state=" + getState() +
", stateCode=" + getStateCode() +
", operator=" + getOperator() +
", date=" + getDate() +
", operateTime=" +
getOperateTime() +
", detail=" +
getDetail() + ")";
}
}

看到了吗?是用”+”拼接的……

  1. 输出格式要统一,这样解析日志时就会方便很多,最起码不用去兼容五花八门的日志格式了。

高性能Java-目录

发表于 2019-01-01 | 阅读次数

高性能Java是一个系列文章,旨在总结编码过程中容易踩到的性能的坑以及一些规避这些坑的方法、工具。
该系列文章计划写以下内容,文章的链接也会在文章完成后添加到本文中:

  1. String
  2. 集合
  3. 序列化
  4. 缓存
  5. 压缩
  6. 日志
  7. IO
  8. 并发
  9. 异步

在编写高性能代码方面如果你有什么心得,欢迎与我沟通,共同学习,共同进步。

JVM致命错误日志

发表于 2018-09-26 | 阅读次数

翻译:曲风富

译者注:本文翻译自Oracle官方文档,有一定英文能力者建议查看原文:
https://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/felog.html

当JVM出现致命错误时,会生成一个包含有错误信息以及出错时系统状态的日志文件。

本附录包含以下内容:

  • 1 致命错误日志的位置
  • 2 致命错误日志描述
  • 3 头部格式
  • 4 线程部分格式
  • 5 进程部分格式
  • 6 系统部分格式

C.1 致命错误日志的位置

产品标志-XX:ErrorFile=file 可用于指定文件的创建位置,其中file表示文件位置的完整路径。文件变量中的子字符串%%将转换为%,子字符串%p将转换为进程的进程ID。

在下面的示例中,错误日志文件将写入目录/var/log/java,并将命名为java_error{pid}.log。

1
java -XX:ErrorFile=/var/log/java/java_error%p.log

如果未指定-XX:ErrorFile=file标志,则默认情况下文件名为hs_err_{pid}.log,其中pid是进程的进程ID。

此外,如果未指定-XX:ErrorFile=file标志,系统将尝试在进程的工作目录中创建该文件。如果无法在工作目录中创建文件(空间不足,权限问题或其他问题),则会在操作系统的临时目录中创建该文件。在Solaris OS和Linux上,临时目录是/tmp。 在Windows上,临时目录由TMP环境变量的值指定; 如果未定义该环境变量,则使用TEMP环境变量的值。

C.2 致命错误日志描述

错误日志包含致命错误发生时获取的信息,包括以下可能的信息:

  • 引发致命错误的操作异常或信号
  • 版本和配置信息
  • 引发致命错误和线程堆栈跟踪的线程的详细信息
  • 正在运行的线程列表及其状态
  • 有关堆的摘要信息
  • 已加载的本地库列表
  • 命令行参数
  • 环境变量
  • 有关操作系统和CPU的详细信息

注 - 在某些情况下,只有这些信息的子集输出到错误日志。这可能会发生在致命错误非常严重,以至于错误的处理程序无法恢复并报告所有细节。

错误日志是一个文本文件,包含以下部分:

  • 标题,提供crash的简要说明。3 Header Format
  • 包含线程信息的部分。4 Thread Section Format
  • 包含进程信息的部分。5 Process Section Format
  • 包含系统信息的部分。6 System Section Format

注 - 请注意,此处描述的致命错误日志的格式基于JDK7格式,可能与其他版本有所不同。

C.3 头部格式

每个致命错误日志文件开头的头部区域都包含问题的简要说明。这部分头部信息也打印到标准输出,可能会显示在应用程序的输出日志中。

头部信息包含指向HotSpot虚拟机错误报告页面的链接,用户可以在其中提交错误报告。

以下是crash日志的示例头部信息:

1
2
3
4
5
6
7
8
9
10
11
12
#
# An unexpected error has been detected by Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x417789d7, pid=21139, tid=1024
#
# Java VM: Java HotSpot(TM) Client VM (1.6.0-rc-b63 mixed mode, sharing)
# Problematic frame:
# C [libNativeSEGV.so+0x9d7]
#
# If you would like to submit a bug report, please visit:
# http://java.sun.com/webapps/bugreport/crash.jsp
#

此示例显示VM在一个意外信号上crash。下一行描述了信号的类型,产生信号的程序计数器(pc),进程ID和线程ID,如下所示:

1
2
3
4
5
6
7
#  SIGSEGV (0xb) at pc=0x417789d7, pid=21139, tid=1024
| | | | +--- thread id
| | | +------------- process id
| | +--------------------------- program counter
| | (instruction pointer)
| +--------------------------------------- signal number
+---------------------------------------------- signal name

下一行包含VM版本(Client VM或Server VM),指示应用程序是以混合模式还是解释模式运行,以及是否启用了类文件共享的指示。

1
# Java VM: Java HotSpot(TM) Client VM (1.6.0-rc-b63 mixed mode, sharing)

下一个信息是导致crash的函数帧,如下所示。

1
2
3
4
5
6
7
8
9
# Problematic frame:
# C [libNativeSEGV.so+0x9d7]
| +-- 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

在该示例中,C帧类型指示本机C帧。下表显示了可能的帧类型。

帧类型 描述
C 本地C帧
j 解释执行的Java帧
V 虚拟机帧
v VM生成的stub帧
J 其他的帧类型,包含被编译过的Java帧

内部错误将导致VM错误处理程序生成类似的错误转储。但是头部格式不尽相同。 示例中的内部错误是guarantee()失败,断言失败,ShouldNotReachHere()等。 以下是如何在头部信息中查找内部错误的示例。

1
2
3
4
5
6
#
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# Internal Error (4F533F4C494E55583F491418160E43505000F5), pid=10226, tid=16384
#
# Java VM: Java HotSpot(TM) Client VM (1.6.0-rc-b63 mixed mode)

在上面的头部信息中,没有信号名称或信号编号。相反,第二行现在包含文本”内部错误”和长十六进制字符串。 此十六进制字符串对检测到错误的源模块和行号进行编码。 通常,此”错误字符串”仅对在HotSpot虚拟机上工作的工程师有用。

错误字符串对行号进行编码,因此随着每次代码更改和释放而更改。一个版本中某一个带有给定错误字符串的crash(例如1.6.0)可能与更新版本中的相同crash(例如1.6.0_01)不对应,即使他们的错误字符串相匹配。

注意 - 不要认为在一个与错误字符串相关的情况下工作的解决方案或解决方案将在与相同错误字符串相关的另一个情况下工作。 注意以下事实:

•具有相同根本原因的错误可能具有不同的错误字符串。

•具有相同错误字符的错误可能具有完全不同的根本原因。

因此,在排除错误时,不应将错误字符串用作唯一的标准。

C.4 线程区域格式

本节包含有关刚刚crash的线程的信息。如果多个线程同时crash,则只打印一个线程。

C.4.1 线程信息

线程部分的第一部分显示了引发致命错误的线程,如下所示。

1
2
3
4
5
6
Current thread (0x0805ac88):  JavaThread "main" [_thread_in_native, id=21139]
| | | | +-- ID
| | | +------------- state
| | +-------------------------- name
| +------------------------------------ type
+-------------------------------------------------- pointer

线程指针是指向Java VM内部线程结构的指针,这通常没什么意义,除非你正在调试实时Java VM或核心文件。

以下列表显示了可能的线程类型。

  • JavaThread
  • VMThread
  • CompilerThread
  • GCTaskThread
  • WatcherThread
  • ConcurrentMarkSweepThread

下面的表格描述了重要的线程状态:

线程状态 描述
_thread_uninitialized 线程未创建。仅在内存损坏的情况下才会发生这种情况
_thread_new 已创建线程但尚未启动
_thread_in_native 线程正在运行本机代码。该错误可能是本机代码中的错误
_thread_in_vm 线程正在运行VM代码
_thread_in_Java 线程正在运行解释或编译的Java代码
_thread_blocked 线程被block
…_trans 如果上述任何状态后跟字符串_trans,则表示该线程正在更改为其他状态

输出中的线程ID是本地线程标识符。

如果Java线程是守护线程,则在线程状态之前打印字符串守护程序。

C.4.2 信号信息

错误日志中的下一个信息描述了导致VM终止的意外信号。在Windows系统中,输出显示如下。

1
siginfo: ExceptionCode=0xc0000005, reading address 0xd8ffecf1

在上面的示例中,异常代码为0xc0000005(ACCESS_VIOLATION),当线程尝试读取地址0xd8ffecf1时发生异常。

在Solaris OS和Linux系统中,信号编号(si_signo)和信号代码(si_code)用于标识异常,如下所示。

1
siginfo:si_signo=11, si_errno=0, si_code=1, si_addr=0x00004321

C.4.3 寄存器上下文

错误日志中的下一个信息显示致命错误时的寄存器上下文。此输出的确切格式取决于处理器。以下示例显示了Intel(IA32)处理器的输出。

1
2
3
4
Registers:
EAX=0x00004321, EBX=0x41779dc0, ECX=0x080b8d28, EDX=0x00000000
ESP=0xbfffc1e0, EBP=0xbfffc1f8, ESI=0x4a6b9278, EDI=0x0805ac88
EIP=0x417789d7, CR2=0x00004321, EFLAGS=0x00010216

与日志中的指令信息结合使用时,寄存器值可能很有用,如下所述。

C.4.4 机器指令

在寄存器值之后,错误日志包含堆栈顶部,随后是系统crash时程序计数器(PC)附近的32字节指令(操作码)。可以使用反汇编程序对这些操作码进行解码,以生成crash位置周围的指令。请注意,IA32和AMD64指令的长度可变,因此在crashPC之前无法始终可靠地解码指令。

译者注:可以使用https://onlinedisassembler.com/odaweb/这种在线工具对指令进行反汇编,当然你也可以使用udis86(http://udis86.sourceforge.net),前提是你需要安装gcc自行编译才能使用。

1
2
3
4
5
6
7
8
9
10
11
12
Top of Stack: (sp=0xbfffc1e0)
0xbfffc1e0: 00000000 00000000 0818d068 00000000
0xbfffc1f0: 00000044 4a6b9278 bfffd208 41778a10
0xbfffc200: 00004321 00000000 00000cd8 0818d328
0xbfffc210: 00000000 00000000 00000004 00000003
0xbfffc220: 00000000 4000c78c 00000004 00000000
0xbfffc230: 00000000 00000000 00180003 00000000
0xbfffc240: 42010322 417786ec 00000000 00000000
0xbfffc250: 4177864c 40045250 400131e8 00000000
Instructions: (pc=0x417789d7)
0x417789c7: ec 14 e8 72 ff ff ff 81 c3 f2 13 00 00 8b 45 08
0x417789d7: 0f b6 00 88 45 fb 8d 83 6f ee ff ff 89 04 24 e8

C.4.5 线程栈

在可能的情况下,错误日志中的下一个输出是线程堆栈。这包括基址和堆栈顶部的地址,当前堆栈指针以及线程可用的未使用堆栈的数量。在可能的情况下,遵循堆栈帧,最多打印100帧。 对于C/C++帧,也可以打印库名称。 重要的是要注意,在某些致命错误条件下,堆栈可能已损坏,在这种情况下,此详细信息可能不可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Stack: [0x00040000,0x00080000),  sp=0x0007f9f8,  free space=254k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0x83d77]
C [App.dll+0x1047]
j Test.foo()V+0
j Test.main([Ljava/lang/String;)V+0
v ~StubRoutines::call_stub
V [jvm.dll+0x80f13]
V [jvm.dll+0xd3842]
V [jvm.dll+0x80de4]
C [java.exe+0x14c0]
C [java.exe+0x64cd]
C [kernel32.dll+0x214c7]
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j Test.foo()V+0
j Test.main([Ljava/lang/String;)V+0
v ~StubRoutines::call_stub

日志包含了2个线程栈:

  • 第一个线程堆栈是Native帧,它打印显示所有函数调用的本地线程。但是,此线程堆栈不考虑运行时编译器内联的Java方法; 如果方法是内联的,它们似乎是父级堆栈框架的一部分。

本地帧的线程堆栈中的信息提供有关crash原因的重要信息。通过从上到下分析列表中的库,通常可以确定哪个库可能导致问题并将其报告给负责该库的相应组织。

  • 第二个线程栈是Java帧,它打印包含内联方法的Java帧,跳过本地帧。根据crash情况,可能无法打印本地线程栈,但可能会打印Java帧。

C.4.6 进一步的细节

如果错误发生在VM线程或编译器线程中,则可能会打印更多详细信息。例如,在VM线程的情况下,如果VM线程在致命错误时执行VM操作,则打印VM操作。 在下面的输出示例中,编译器线程引发了致命错误。 该任务是编译器任务,HotSpot Client VM正在编译方法hs101t004Thread.ackermann。

Current CompileTask:

HotSpot Client Compiler:754 b

nsk.jvmti.scenarios.hotswap.HS101.hs101t004Thread.ackermann(IJ)J (42 bytes)

对于HotSpot Server VM,编译器任务的输出略有不同,但也包括完整的类名和方法。

C.5 进程区域格式

进程部分在线程部分之后打印。它包含有关整个进程的信息,包括进程的线程列表和内存使用情况。

C.5.1 线程列表

线程列表包括VM知道的线程。 这包括所有Java线程和一些VM内部线程,但不包括未附加到VM的用户应用程序创建的任何本地线程。 输出格式如下。

1
2
3
4
5
6
7
8
=>0x0805ac88 JavaThread "main" [_thread_in_native, id=21139]
| | | | | +----- ID
| | | | +------------------- state
| | | | (JavaThread only)
| | | +--------------------------------- name
| | +------------------------------------------ type
| +---------------------------------------------------- pointer
+------------------------------------------------------ "=>" current thread

下面是一个输出示例:

1
2
3
4
5
6
7
8
9
10
Java Threads: ( => current thread )
0x080c8da0 JavaThread "Low Memory Detector" daemon [_thread_blocked, id=21147]
0x080c7988 JavaThread "CompilerThread0" daemon [_thread_blocked, id=21146]
0x080c6a48 JavaThread "Signal Dispatcher" daemon [_thread_blocked, id=21145]
0x080bb5f8 JavaThread "Finalizer" daemon [_thread_blocked, id=21144]
0x080ba940 JavaThread "Reference Handler" daemon [_thread_blocked, id=21143]
=>0x0805ac88 JavaThread "main [_thread_in_native, id=21139]
Other Threads:
0x080b6070 VMThread [id=21142]
0x080ca088 WatcherThread [id=21148]

有关线程类型和线程状态的描述请见C.4 Thread Section Format.

C.5.2 VM状态

接下来的信息是VM状态,描述了整个虚拟机的状态。下面的表格描述了VM的一般状态。

VM一般状态 描述
not at a safepoint 正常执行
at safepoint 所有线程被阻塞在VM中以等待特殊VM操作完成
synchronizing 请求了一个特殊的VM操作,VM正在等待VM中所有的线程阻塞

VM状态输出是错误日志中的单行输出,如下:

1
VM state:not at safepoint (normal execution)

C.5.3 互斥锁和监视器

错误日志中的下一个信息是当前由线程拥有的互斥锁和监视器的列表。 这些互斥锁是VM内部锁,而不是与Java对象关联的监视器。 下面的示例显示了在保持VM锁定时发生crash时输出的外观。对于每个锁,日志包含锁的名称,其所有者以及VM内部互斥结构的地址及其操作系统锁。通常,此信息仅对非常熟悉HotSpot VM的用户有用。 所有者线程可以交叉引用到线程列表。

1
2
3
4
5
VM Mutex/Monitor currently owned by a thread:

([mutex/lock_event])[0x007357b0/0x0000031c] Threads_lock - owner thread: 0x00996318

[0x00735978/0x000002e0] Heap_lock - owner thread: 0x00736218

C.5.4 堆摘要

下一个信息是堆的摘要。 输出取决于垃圾收集(GC)配置。 在此示例中,使用串行收集器,禁用类数据共享,并且tenured generation为空。 这可能表示致命错误发生在早期或启动期间,并且GC尚未将任何对象提升为终身代。 以下是此输出的示例。

1
2
3
4
5
6
7
8
9
10
Heap
def new generation total 576K, used 161K [0x46570000, 0x46610000, 0x46a50000)
eden space 512K, 31% used [0x46570000, 0x46598768, 0x465f0000)
from space 64K, 0% used [0x465f0000, 0x465f0000, 0x46600000)
to space 64K, 0% used [0x46600000, 0x46600000, 0x46610000)
tenured generation total 1408K, used 0K [0x46a50000, 0x46bb0000, 0x4a570000)
the space 1408K, 0% used [0x46a50000, 0x46a50000, 0x46a50200, 0x46bb0000)
compacting perm gen total 8192K, used 1319K [0x4a570000, 0x4ad70000, 0x4e570000)
the space 8192K, 16% used [0x4a570000, 0x4a6b9d48, 0x4a6b9e00, 0x4ad70000)
No shared spaces configured.

C.5.5 内存映射

日志中的下一个信息是crash时的虚拟内存区域列表。对于大型应用程序,此列表可能很长。调试一些crash时,内存映射非常有用,因为它可以告诉你实际使用的库,它们在内存中的位置,以及堆,堆栈和保护页的位置。

内存映射的格式是特定于操作系统的。 在Solaris操作系统上,将打印基本地址和库名称。 在Linux系统上,打印进程内存映射(/proc/pid/maps)。 在Windows系统上,将打印每个库的基址和结束地址。 在Linux/x86上生成以下示例输出。 注意,为了简洁起见,示例中省略了大多数行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Dynamic libraries:
08048000-08056000 r-xp 00000000 03:05 259171 /h/jdk6/bin/java
08056000-08058000 rw-p 0000d000 03:05 259171 /h/jdk6/bin/java
08058000-0818e000 rwxp 00000000 00:00 0
40000000-40013000 r-xp 00000000 03:0a 400046 /lib/ld-2.2.5.so
40013000-40014000 rw-p 00013000 03:0a 400046 /lib/ld-2.2.5.so
40014000-40015000 r--p 00000000 00:00 0
Lines omitted.
4123d000-4125a000 rwxp 00001000 00:00 0
4125a000-4125f000 rwxp 00000000 00:00 0
4125f000-4127b000 rwxp 00023000 00:00 0
4127b000-4127e000 ---p 00003000 00:00 0
4127e000-412fb000 rwxp 00006000 00:00 0
412fb000-412fe000 ---p 00083000 00:00 0
412fe000-4137b000 rwxp 00086000 00:00 0
Lines omitted.
44600000-46570000 rwxp 00090000 00:00 0
46570000-46610000 rwxp 00000000 00:00 0
46610000-46a50000 rwxp 020a0000 00:00 0
46a50000-46bb0000 rwxp 00000000 00:00 0
46bb0000-4a570000 rwxp 02640000 00:00 0
Lines omitted.

上述内存映射中的行的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
40049000-4035c000 r-xp 00000000 03:05 824473      /jdk1.5/jre/lib/i386/client/libjvm.so
|<------------->| ^ ^ ^ ^ |<----------------------------------->|
Memory region | | | | |
| | | | |
Permission --- + | | | |
r: read | | | |
w: write | | | |
x: execute | | | |
p: private | | | |
s: share | | | |
| | | |
File offset ----------+ | | |
| | |
Major ID and minor ID of -------+ | |
the device where the file | |
is located (i.e. /dev/hda5) | |
| |
inode number ------------------------+ |
|
File name -------------------------------------------------------+

在内存映射输出中,每个库都有两个虚拟内存区域:一个用于代码,另一个用于数据。 代码段的权限标记为r-xp(可读,可执行,私有),数据段的权限为rw-p(可读,可写,私有)。

Java堆已经包含在输出中的堆摘要中,但是验证为堆保留的实际内存区域是否与堆摘要中的值匹配以及属性是否设置为rwxp可能很有用。

线程堆栈通常在内存映射中显示为两个背对背区域,一个具有权限— p(保护页面),另一个具有权限rwxp(实际堆栈空间)。 此外,了解防护页面大小或堆栈大小很有用。 例如,在该存储器映射中,堆栈位于4127b000到412fb000。

在Windows系统上,内存映射输出是每个已加载模块的加载和结束地址,如下例所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dynamic libraries:
0x00400000 - 0x0040c000 c:\jdk6\bin\java.exe
0x77f50000 - 0x77ff7000 C:\WINDOWS\System32\ntdll.dll
0x77e60000 - 0x77f46000 C:\WINDOWS\system32\kernel32.dll
0x77dd0000 - 0x77e5d000 C:\WINDOWS\system32\ADVAPI32.dll
0x78000000 - 0x78087000 C:\WINDOWS\system32\RPCRT4.dll
0x77c10000 - 0x77c63000 C:\WINDOWS\system32\MSVCRT.dll
0x08000000 - 0x08183000 c:\jdk6\jre\bin\client\jvm.dll
0x77d40000 - 0x77dcc000 C:\WINDOWS\system32\USER32.dll
0x7e090000 - 0x7e0d1000 C:\WINDOWS\system32\GDI32.dll
0x76b40000 - 0x76b6c000 C:\WINDOWS\System32\WINMM.dll
0x6d2f0000 - 0x6d2f8000 c:\jdk6\jre\bin\hpi.dll
0x76bf0000 - 0x76bfb000 C:\WINDOWS\System32\PSAPI.DLL
0x6d680000 - 0x6d68c000 c:\jdk6\jre\bin\verify.dll
0x6d370000 - 0x6d38d000 c:\jdk6\jre\bin\java.dll
0x6d6a0000 - 0x6d6af000 c:\jdk6\jre\bin\zip.dll
0x10000000 - 0x10032000 C:\bugs\crash2\App.dll

C.5.6 VM参数和环境变量

错误日志中的下一个信息是VM参数列表,后跟环境变量列表。 例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
VM Arguments:
java_command: NativeSEGV 2
Environment Variables:
JAVA_HOME=/h/jdk
PATH=/h/jdk/bin:.:/h/bin:/usr/bin:/usr/X11R6/bin:/usr/local/bin:
/usr/dist/local/exe:/usr/dist/exe:/bin:/usr/sbin:/usr/ccs/bin:
/usr/ucb:/usr/bsd:/usr/etc:/etc:/usr/dt/bin:/usr/openwin/bin:
/usr/sbin:/sbin:/h:/net/prt-web/prt/bin
USERNAME=user
LD_LIBRARY_PATH=/h/jdk6/jre/lib/i386/client:/h/jdk6/jre/lib/i386:
/h/jdk6/jre/../lib/i386:/h/bugs/NativeSEGV
SHELL=/bin/tcsh
DISPLAY=:0.0
HOSTTYPE=i386-linux
OSTYPE=linux
ARCH=Linux
MACHTYPE=i386

请注意,环境变量列表不是完整列表,而是适用于Java VM的环境变量的子集。

C.5.7 信号处理程序

在Solaris OS和Linux上,错误日志中的下一个信息是信号处理程序列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
Signal Handlers:
SIGSEGV: [libjvm.so+0x3aea90], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGBUS: [libjvm.so+0x3aea90], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGFPE: [libjvm.so+0x304e70], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGPIPE: [libjvm.so+0x304e70], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGILL: [libjvm.so+0x304e70], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGUSR1: SIG_DFL, sa_mask[0]=0x00000000, sa_flags=0x00000000
SIGUSR2: [libjvm.so+0x306e80], sa_mask[0]=0x80000000, sa_flags=0x10000004
SIGHUP: [libjvm.so+0x3068a0], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGINT: [libjvm.so+0x3068a0], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGQUIT: [libjvm.so+0x3068a0], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGTERM: [libjvm.so+0x3068a0], sa_mask[0]=0xfffbfeff, sa_flags=0x10000004
SIGUSR2: [libjvm.so+0x306e80], sa_mask[0]=0x80000000, sa_flags=0x10000004

C.6 系统区域格式

错误日志中的最后一部分是系统信息。 输出是特定于操作系统的,但通常包括操作系统版本,CPU信息和有关内存配置的摘要信息。

以下示例显示Solaris 9 OS系统上的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---------------  S Y S T E M  ---------------
OS: Solaris 9 12/05 s9s_u5wos_08b SPARC
Copyright 2005 Sun Microsystems, Inc. All Rights Reserved.
Use is subject to license terms.
Assembled 21 November 2005
uname:SunOS 5.9 Generic_112233-10 sun4u (T2 libthread)
rlimit: STACK 8192k, CORE infinity, NOFILE 65536, AS infinity
load average:0.41 0.14 0.09
CPU:total 2 has_v8, has_v9, has_vis1, has_vis2, is_ultra3

Memory: 8k page, physical 2097152k(1394472k free)
vm_info: Java HotSpot(TM) Client VM (1.5-internal) for solaris-sparc,

built on Aug 12 2005 10:22:32 by unknown with unknown Workshop:0x550

在Solaris OS和Linux上,操作系统信息包含在文件/etc/*发行版中。此文件描述了运行应用程序的系统类型,在某些情况下,信息字符串可能包含修补程序级别。某些系统升级未反映在/etc/*发布文件中。在Linux系统上尤其如此,用户可以在其中重建系统的任何部分。

在Solaris OS上,uname系统调用用于获取内核的名称。 还会打印线程库(T1或T2)。

在Linux系统上,uname系统调用也用于获取内核名称。 还会打印libc版本和线程库类型。 一个例子如下。

1
2
3
4
5
uname:Linux 2.4.18-3smp #1 SMP Thu Apr 18 07:27:31 EDT 2002 i686

libc:glibc 2.2.5 stable linuxthreads (floating stack)

|<- glibc version ->|<-- pthread type -->|

在Linux上有三种可能的线程类型,即linuxthreads(固定栈),linuxthreads(浮动栈)和NPTL。它们通常安装在/lib,/lib/i686和/lib/tls中。

了解线程类型很有用。例如,如果crash似乎与pthread有关,那么你可以通过选择不同的pthread库来解决问题。可以通过设置LD_LIBRARY_PATH或LD_ASSUME_KERNEL来选择不同的pthread库(和libc)。

glibc版本通常不包括补丁级别。 rpm -q glibc命令可能会提供更详细的版本信息。

在Solaris OS和Linux上,下一个信息是rlimit信息。请注意,VM的默认栈大小通常小于系统限制,例子如下。

1
2
3
4
5
6
7
rlimit: STACK 8192k, CORE 0k, NPROC 4092, NOFILE 1024, AS infinity
| | | | virtual memory (-v)
| | | +--- max open files (ulimit -n)
| | +----------- max user processes (ulimit -u)
| +------------------------- core dump size (ulimit -c)
+---------------------------------------- stack size (ulimit -s)
load average:0.04 0.05 0.02

下一个信息指定VM在启动时标识的CPU体系结构和功能,如以下示例所示。

CPU:total 2 family 6, cmov, cx8, fxsr, mmx, sse | | |<----- cpu features ---->| | | | +— processor family (IA32 only): | 3 - i386 | 4 - i486 | 5 - Pentium | 6 - PentiumPro, PII, PIII | 15 - Pentium 4 +———— Total number of CPUs

下表显示了SPARC系统上可能的CPU功能。

SPARC Features

SPARC Feature Description
has_v8 Supports v8 instructions.支持第8版指令
has_v9 Supports v9 instructions.支持第9版指令
has_vis1 Supports visualization instructions.支持可视化指令
has_vis2 Supports visualization instructions.支持可视化指令
is_ultra3 UltraSparc III.
no-muldiv No hardware integer multiply and divide.没有硬件整数乘法和除法
no-fsmuld No multiply-add and multiply-subtract instructions.没有乘加和乘减指令

下表显示了Inter/IA32系统上可能的CPU功能。

Intel/IA32 Features

Intel/IA32 Feature Description
cmov Supports cmov instruction.支持cmov指令
cx8 Supports cmpxchg8b instruction.支持cmpxchg8b指令
fxsr Supports fxsave and fxrstor.支持fssave和fxrstor
mmx Supports MMX.支持MMX
sse Supports SSE extensions.支持SSE扩展
sse2 Supports SSE2 extensions.支持SSE2扩展
ht Supports Hyper-Threading Technology.支持超线程技术

下表显示了AMD64/EM64T系统上可能的CPU功能。

AMD64/EM64T Features

AMD64/EM64T Feature Description
amd64 AMD Opteron, Athlon64, and so forth.AMD Opteron, Athlon64等
em64t Intel EM64T processor.Inter EM64T处理器
3dnow Supports 3DNow extension.支持3DNow扩展
ht Supports Hyper-Threading Technology.支持超线程技术

错误日志中下一个信息是内存信息,如下:

1
2
3
4
5
6
7
                                                        unused swap space
total amount of swap space |
unused physical memory | |
total amount of physical memory | | |
page size | | | |
v v v v v
Memory: 4k page, physical 513604k(11228k free), swap 530104k(497504k free)

某些系统要求交换空间至少是实际物理内存大小的两倍,而其他系统则没有任何此类要求。作为一般规则,如果物理内存和交换空间几乎都已满,则有充分的理由怀疑crash是由于内存不足造成的。

在Linux系统上,内核可以将大多数未使用的物理内存转换为文件缓存。当需要更多内存时,Linux内核会将缓存内存返回给应用程序。这由内核透明地处理,但它确实意味着当仍有足够的可用物理内存时,致命错误处理程序报告的未使用物理内存量可能接近于零。

错误日志的SYSTEM部分中的最终信息是vm_info,它是嵌入在libjvm.so/jvm.dll中的版本字符串。 每个Java VM都有自己唯一的vm_info字符串。 如果你对特定Java VM是否生成致命错误日志有疑问,请检查版本字符串。

系统Crash问题排查

发表于 2018-09-26 | 阅读次数

翻译:曲风富

译者注:本文翻译自Oracle官方文档,有一定英文能力者建议查看原文:
https://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/crashes.html

本章节提供针对系统crash诊断的某些特定过程的信息和指南。

崩溃或致命错误会导致进程异常退出。导致crash的原因有很多,比如HotSpot VM、系统库、Java SE库或API、应用本地代码甚至操作系统中的某个bug,都有可能会导致crash。外部因素,诸如系统资源耗尽也可能会导致crash。

由HotSpot VM或Java SE库中的代码中的bug引发的 比较少见。因此本章节将针对如何检查crash提供一些建议。在某些情况下,围绕一个crash现象努力直到bug被诊断出来并修复是有可能的。

通常分析crash的第一步是定位到致命错误的日志。这个日志是一个文本文件,是HotSpot VM在crash时生成的。如何定位到这个文件以及此日志的详细描述,请参见这篇文章:Appendix C, Fatal Error Log。

1 crash采样

本节提供了一些演示如何使用error log去找到crash原因的例子。

1.1 确定crash发生的位置

Crash日志的头部指出了有问题的帧。

如果帧类型是本地帧并且不是操作系统本地帧,那么说明问题可能出自本地库并且不是Java虚拟机导致。解决这种crash的第一步是查看crash发生处的本地帧的代码。根据本地库的代码,这里有3个选项:

  1. 如果本地库由你的应用提供,那么请检查你的本地库的代码。-Xcheck:jni可以帮你很多本地bug。详情请见:2.1 -Xcheck:jni Option;
  2. 如果你应用中使用的本地库由第三方提供,建议向第三方提供bug report,并提供致命错误日志;
  3. 如果本地库是JRE的一部分,那么请向Java社区提交bug report,并确保库的名称明确无误,以便bug report被分派给正确的开发人员。

如果错误日志中的顶部帧信息显示是其他类型的帧,请向Java社区提交bug report、错误日志以及如何复现问题的相关信息。

另外,请参阅本章剩余的内容。

1.2 本地代码中Crash

如果致命错误日志显示crash发生在本地库,那么很有可能是本地代码或者JNI库代码中的bug导致。Crash当然也可能是其他的原因导致,但是对库、core文件、crash导出文件的分析是一个好的开端。例如,思考一下下面的从致命错误日志头部提取出来的信息:

1
2
3
4
5
6
7
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# SIGSEGV (0xb) at pc=0x417789d7, pid=21139, tid=1024
#
# Java VM: Java HotSpot(TM) Server VM (6-beta2-b63 mixed mode)
# Problematic frame:
# C [libApplication.so+0x9d7]

在这个例子中,一个在libApplication.so库中执行的线程发生了SIGSEGV错误。

在某些场景中某个本地库中的bug显示在Java虚拟机中crash。请看下面的crash:一个Java线程在_thread_in_vm状态下失败(即它在Java VM代码中执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x08083d77, pid=3700, tid=2896
#
# Java VM: Java HotSpot(TM) Client VM (1.5-internal mixed mode)
# Problematic frame:
# V [jvm.dll+0x83d77]

--------------- T H R E A D ---------------

Current thread (0x00036960): JavaThread &quot;main&quot; [_thread_in_vm, id=2896]
:
Stack: [0x00040000,0x00080000), sp=0x0007f9f8, free space=254k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0x83d77]
C [App.dll+0x1047] <========= C/native frame
j Test.foo()V+0
j Test.main([Ljava/lang/String;)V+0
v ~StubRoutines::call_stub
V [jvm.dll+0x80f13]
V [jvm.dll+0xd3842]
V [jvm.dll+0x80de4]
V [jvm.dll+0x87cd2]
C [java.exe+0x14c0]
C [java.exe+0x64cd]
C [kernel32.dll+0x214c7]
:

在这个case中,栈跟踪显示一个在App.dll中的本地程序调用了VM(可能通过JNI)。

如果你遇到了一个在本地库中的crash(如上面所述的例子),如果可能的话,你可以使用本地的调试程序链接到core文件或者crash导出文件。本地调试程序有dbx、gdb或windbg,因操作系统而异。

另一个方法是在启动时将-Xcheck:jni添加到启动命令行中(参见B.2.1 -Xcheck:jni Option)。这个选项不保证发现所有的JNI代码中的问题,但它可以帮你定位到很多的问题。

如果crash的本地库是Java运行环境的一部分(如awt.dll, net.dll等),那么很可能你遇到了一个Java库或API的bug。如果经过进一步分析之后你断定这是个Java库或API的bug,请收集尽可能多的数据并提交bug或寻求Java技术支持。详情参见Chapter 7, Submitting Bug Reports。

1.3 栈溢出导致Crash

Java语言代码中的栈溢出(也叫爆栈)会导致线程抛出java.lang.StackOverflowError。换句话说,C和C++写入时达到了栈的末尾并且引发了栈溢出。这个致命错误导致了进程终止。

在HotSpot实现中,Java方法与C/C++本地代码共享栈帧,也就是用户本地代码和虚拟机本身。Java方法生成代码来检查栈空间有可用的到栈尾的固定距离,来确保本地代码在不超出栈空间的情况下能够被执行。这个到栈尾的距离被称为”Shadow Pages”。这个距离是可调节的,这样应用需要比默认值更大的距离时就可以增加Shadow page的尺寸。增大Shadow pages的选项是-XX:StackShadowpages=_n_,n的值要大于shadow pages在当前平台下的默认值。

如果你的应用出现了分段错误,而且没有生成core文件或error日志(见Appendix C, Fatal Error Log)或在Windows系统中发现STACK_OVERFLOW_ERROR或”An irrecoverable stack overflow has occurred”消息,这说明StackShadowPages已经耗尽需要扩容。

如果你想要增大StackShadowPages 的值,你也需要使用-Xss参数增加默认线程栈大小。增加线程栈大小可能会导致能够创建的线程数的减少。默认线程栈大小根据平台差异从256K到1024K不等。

下面的代码片段来自于Windows系统的一个致命错误日志,其中的一个线程引发了本地代码中的栈溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# EXCEPTION_STACK_OVERFLOW (0xc00000fd) at pc=0x10001011, pid=296, tid=2940
#
# Java VM: Java HotSpot(TM) Client VM (1.6-internal mixed mode, sharing)
# Problematic frame:
# C [App.dll+0x1011]
#

--------------- T H R E A D ---------------

Current thread (0x000367c0): JavaThread &quot;main&quot; [_thread_in_native, id=2940]
:
Stack: [0x00040000,0x00080000), sp=0x00041000, free space=4k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C [App.dll+0x1011]
C [App.dll+0x1020]
C [App.dll+0x1020]
:
C [App.dll+0x1020]
C [App.dll+0x1020]
...<more frames>...

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j Test.foo()V+0
j Test.main([Ljava/lang/String;)V+0
v ~StubRoutines::call_stub

下面是上面的日志包含的信息:

  • 异常是EXCEPTION_STACK_OVERFLOW;
  • 线程的状态是_thread_in_native,说明线程正在执行本地或者JNI代码;
  • 栈信息显示,栈的可用空间是4K(Windows中单页的大小)。另外,栈指针(Stack pointer, sp)在0x00041000的位置,已经很接近栈尾了(0x00040000)(译者注: java线程栈是从高地址往低地址方向走的);
  • 本地帧的输出显示一个递归的本地函数是这个case的问题;
  • 标注……表明还有更多的帧存在但没有被输出。最多可输出100帧。

译者注:关于栈溢出的问题,感兴趣的同学可以进一步阅读这篇文章:https://www.jianshu.com/p/debef4f69a90。

1.4 HotSpot编译线程中Crash

如果错误日志中显示crash时当前Java线程的名称是CompilerThread0, CompilerThread1, 或AdapterCompiler,那么可能你遇到了一个编译bug。这种情况下你有必要临时性地切换编译器(例如把HotSpot Server VM替换成HotSpot Client VM,反之亦然),或者将引发crash的方法从编译过程中排除。详情见:4.2.1 Crash in HotSpot Compiler Thread or Compiled Code。

1.5 编译的代码中Crash

如果编译的代码中发生crash,那么你可能遇到了编译器bug导致了错误的代码生成的问题。你可以识别出一个在被编译的代码中出现的crash,如果有问题的帧被标注为代码J(代表被编译的Java帧)。下面是一个这样的crash例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# SIGSEGV (0xb) at pc=0x0000002a99eb0c10, pid=6106, tid=278546
#
# Java VM: Java HotSpot(TM) 64-Bit Server VM (1.6.0-beta-b51 mixed mode)
# Problematic frame:
# J org.foobar.Scanner.body()V
#
:
Stack: [0x0000002aea560000,0x0000002aea660000), sp=0x0000002aea65ddf0,
free space=1015k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
J org.foobar.Scanner.body()V

[error occurred during error reporting, step 120, id 0xb]

需要注意的是一个完整的线程栈无法提供。输出行”error occurred during error reporting”表示在尝试获得栈跟踪的时候出错了(本示例中可能是栈损坏)。

这种情况下你有必要临时性地切换编译器(例如把HotSpot Server VM替换成HotSpot Client VM,反之亦然),或者将引发crash的方法从编译过程中排除。在本示例中,不太可能将编译器将64位Server VM切换,因为将其切换至32位Client VM不太可行。

1.6 VMThread中Crash

如果log输出显示当前线程是VMThread,那么请在日志中的THREAD区域寻找包含VM_Operation的行。VMThread是HotSpot虚拟机中一个特殊的线程。它向虚拟机中提交诸如GC这种特殊的任务。如果VM_Operation显示其操作是垃圾回收,那么很有可能你遇到了诸如堆损坏的问题。

Crash也有可能有GC问题引起,但这可以等价于一些其他问题(比如编译器或运行期bug)导致堆中的对象引用处于不连续或者不正确的状态。在这个场景中,尽可能多地收集与环境相关的信息,并尝试可能的替代方案。如果这个问题是GC相关,你可能需要临时性地修改GC配置作为替代方案。这方面的内容在4.2.2 Crash During Garbage Collection中有讨论。

2 寻求替代方案

如果是重要的应用发生了crash,并且是由HotSpot VM中的bug引发,那么你应该快速寻找一个替代方案。本节的目标是给出一些可能的替代方案。如果crash的应用是部署在JDK最近的发布版本上,那么这个crash事件应该报告给Oracle。

如果关键应用程序发生崩溃,并且崩溃似乎是由HotSpot VM中的错误引起的,那么可能需要快速找到临时解决方法。 本节的目的是提出一些可能的解决方法。 如果使用最新版本的JDK部署的应用程序发生崩溃,则应始终向Oracle报告崩溃

注意 - 即使本节中的相关内容成功消除了崩溃,但是问题的解决方案不是固定的,而是临时解决方案。 寻求电话支持或提交包含了能说明问题的原始配置的BUG报告。

2.1 在HotSpot编译线程或编译代码中crash

如果致命错误日志显示crash发生在编译器线程中, 则可能是遇到了编译器 bug (但并非总是如此)。同样, 如果在编译后的代码中crash, 则可能是因为编译器生成了不正确的代码所致。

在HotSpot VM (-client选项) 的情况下, 编译器线程以 CompilerThread0 的形式出现在错误日志中。在HotSpot Server VM中, 则有多个编译器线程, 它们在错误日志文件中显示为 CompilerThread0、CompilerThread1 和 AdapterThread。

下面是 J2SE 5.0 开发过程中遇到和修复的编译器bug的错误日志片段。日志文件显示使用了HotSpot Server VM, 并且崩溃发生在 CompilerThread1 中。此外, 日志文件显示当前 CompileTask 是 java.lang.Thread.setPriority方法的编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# An unexpected error has been detected by HotSpot Virtual Machine:
#
:
# Java VM: Java HotSpot(TM) Server VM (1.5-internal-debug mixed mode)
:
--------------- T H R E A D ---------------

Current thread (0x001e9350): JavaThread "CompilerThread1" daemon [_thread_in_vm, id=20]

Stack: [0xb2500000,0xb2580000), sp=0xb257e500, free space=505k

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

V [libjvm.so+0xc3b13c]
:

Current CompileTask:
opto: 11 java.lang.Thread.setPriority(I)V (53 bytes)

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

Java Threads: ( => current thread )
0x00229930 JavaThread "Low Memory Detector" daemon [_thread_blocked, id=21]
=>0x001e9350 JavaThread "CompilerThread1" daemon [_thread_in_vm, id=20]
:

在这种情况下, 有两种潜在的变通方法:

  • 蛮力方法: 更改jvm配置, 以便使用-client选项运行应用程序以指定HotSpot Client VM;
  • 假定bug仅在setPriority方法的编译过程中发生, 并将此方法从编译中排除。

在某些环境中配置第一种方法(使用-client选项)可能很简单。在其他情况下,如果配置复杂或者无法轻松访问配置VM的命令行,则可能会更加困难。通常,从HotSpot Server VM切换到HotSpot Client VM也会降低应用程序的峰值性能。 根据环境的不同,在诊断和修复实际问题之前,这可能是可以接受的。

第二种方法(从编译中排除方法)需要在应用程序的工作目录中创建文件.hotspot_compiler。 以下是此文件的示例

1
exclude    java/lang/Thread    setPriority

通常,此文件的格式为exclude CLASS METHOD,其中CLASS是类(使用包名称完全限定),METHOD是方法的名称。构造方法指定为,静态初始化程序指定为。

_注意 - .HOTSPOT_COMPILER文件是一个不受支持的接口。 此处仅为了故障排除和查找临时解决方案而对其进行了文档记录。_

重新启动应用程序后,编译器将不会尝试编译.hotspot_compiler文件中列出的任何排除方法。在某些情况下,这可以提供临时缓解,直到诊断出崩溃的根本原因并修复错误

为了验证HotSpot VM是否正确定位并处理了上述示例中显示的.hotspot_compiler文件,请在运行时查找以下日志信息。请注意,文件名分隔符是一个点,而不是斜杠。

1
### Excluding compile:    java.lang.Thread::setPriority

2.2 GC时crash

如果在垃圾回收(GC)期间发生崩溃,则致命错误日志会报告VM_Operation正在进行中。出于本讨论的目的,假设大多数并发GC(-XX:+ UseConcMarkSweep)未使用。VM_Operation显示在日志的THREAD部分中,表示以下情况之一:

  • 用于分配的生成集合
  • 全集代收藏
  • 并行gc分配失败
  • 并行gc未能永久分配
  • 并行gc系统gc

很可能日志中报告的当前线程是VMThread。 这是用于在HotSpot VM中执行特殊任务的特殊线程。 致命错误日志的以下片段显示了串行垃圾收集器中崩溃的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
---------------  T H R E A D  ---------------

Current thread (0x002cb720): VMThread [id=3252]

siginfo: ExceptionCode=0xc0000005, reading address 0x00000000

Registers:
EAX=0x0000000a, EBX=0x00000001, ECX=0x00289530, EDX=0x00000000
ESP=0x02aefc2c, EBP=0x02aefc44, ESI=0x00289530, EDI=0x00289530
EIP=0x0806d17a, EFLAGS=0x00010246

Top of Stack: (sp=0x02aefc2c)
0x02aefc2c: 00289530 081641e8 00000001 0806e4b8
0x02aefc3c: 00000001 00000000 02aefc9c 0806e4c5
0x02aefc4c: 081641e8 081641c8 00000001 00289530
0x02aefc5c: 00000000 00000000 00000001 00000001
0x02aefc6c: 00000000 00000000 00000000 08072a9e
0x02aefc7c: 00000000 00000000 00000000 00035378
0x02aefc8c: 00035378 00280d88 00280d88 147fee00
0x02aefc9c: 02aefce8 0806e0f5 00000001 00289530
Instructions: (pc=0x0806d17a)
0x0806d16a: 15 08 83 3d c0 be 15 08 05 53 56 57 8b f1 75 0f
0x0806d17a: 0f be 05 00 00 00 00 83 c0 05 a3 c0 be 15 08 8b

Stack: [0x02ab0000,0x02af0000), sp=0x02aefc2c, free space=255k

Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0x6d17a]
V [jvm.dll+0x6e4c5]
V [jvm.dll+0x6e0f5]
V [jvm.dll+0x71771]
V [jvm.dll+0xfd1d3]
V [jvm.dll+0x6cd99]
V [jvm.dll+0x504bf]
V [jvm.dll+0x6cf4b]
V [jvm.dll+0x1175d5]
V [jvm.dll+0x1170a0]
V [jvm.dll+0x11728f]
V [jvm.dll+0x116fd5]
C [MSVCRT.dll+0x27fb8]
C [kernel32.dll+0x1d33b]

VM_Operation (0x0373f71c): generation collection for allocation, mode:

safepoint, requested by thread 0x02db7108

注意 - 垃圾收集期间的crash并不意味着这是垃圾收集过程中的一个BUG。它还可能指向编译器或运行时错误或其他一些问题。

如果在垃圾回收期间遇到重复崩溃,您可以尝试以下变通方法:

  • 切换GC配置。 例如,如果您使用的是串行收集器,请尝试使用吞吐量收集器,反之亦然;
  • 如果您使用的是HotSpot Server VM,请尝试使用HotSpot Client VM。

如果您不确定正在使用哪个垃圾收集器,则可以使用Solaris OS和Linux上的jmap实用程序(请参阅2.7 jmap Utility)从核心文件获取堆信息(如果核心文件可用)。 通常,如果未在命令行中指定GC配置,则将在Windows上使用串行收集器。 在Solaris OS和Linux上,它取决于计算机配置。 如果机器至少有2GB内存且至少有2个处理器,则将使用吞吐量收集器(并行GC)。 对于较小的机器,串行收集器是默认的。选择串行收集器的选项是-XX:+UseSerialGC和选择吞吐量收集器的选项是-XX:+UseParallelGC。 如果作为解决方法,您从吞吐量收集器切换到串行收集器,那么您可能会在多处理器系统上遇到性能下降。 在诊断和解决根问题之前,这可能是可以接受的。

2.3 Class数据共享时crash

类数据共享是J2SE 5.0中的一项新功能。 使用Sun提供的安装程序在32位平台上安装JRE时,安装程序会将一组类从系统JAR文件加载到专用内部表示形式,并将该表示形式转储到名为共享存档的文件中。 启动VM时,共享存档将进行内存映射。这样可以节省类加载,并允许在多个VM实例之间共享与类关联的大部分元数据。 在J2SE 5.0中,仅在使用HotSpot Client VM时才启用类数据共享。 此外,只有串行垃圾收集器才支持共享。

致命错误日志在日志标题中打印版本字符串。 如果启用了共享,则由文本共享指示,如以下示例所示:

1
2
3
4
5
6
7
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x08083d77, pid=3572, tid=784
#
# Java VM: Java HotSpot(TM) Client VM (1.5-internal mixed mode, sharing)
# Problematic frame:
# V [jvm.dll+0x83d77]

可以通过在命令行上提供-Xshare:off选项来禁用共享。 如果在禁用共享的情况下无法复制崩溃但可以在启用共享的情况下复制崩溃,则可能是您遇到此功能中的错误。在这种情况下,尽可能多地收集信息并提交错误报告。

3 微软C++版本的考虑

JDK 7软件是在Windows上使用Microsoft Visual Studio 2010专业版为32位和64位平台构建的。如果遇到Java 应用程序崩溃, 并且你有使用不同版本的编译器编译的本地或JNI库, 则必须考虑运行时之间的兼容性问题。具体地说, 只有在处理多个运行时时遵循Microsoft准则时, 才会支持你的环境。例如, 如果你使用一个运行时分配内存, 则必须使用同一运行时释放它。如果使用不同的库释放资源, 而不是分配资源时所使用的库, 则可能会出现不可预知的行为或崩溃。

JToolkit:Java问题排查工具箱

发表于 2018-08-20 | 阅读次数

关于JToolkit

JToolkit
JToolkit是一个Java问题排查的工具集,通过集成各种Linux命令、Shell脚本、Java命令,以及各种第三方好用的工具,通过傻瓜式的界面和方便快捷的操作来帮助我们快速定位问题。目前已经集成了唯品会的 VjTop、Async-Profiler。

包含的功能

1.CPU/LOAD问题分析

  1. 显示Java线程栈的CPU时间占比
  2. 线程CPU时间占比排行(VJTop)
  3. 生成火焰图(10分钟)
  4. 生成飞行记录JFR(10分钟)

2.GC问题分析

  1. FullGC时间久
  2. FullGC不断
  3. FullGC频繁
  4. YoungGC时间久
  5. 远程分析gc.log

3.Swap问题

  1. 统计各进程Swap使用情况
  2. 关闭Swap

4.内存问题

  1. 堆内存使用情况
  2. 对象实例统计Top10
  3. dump堆内存

5.JVM参数

  1. JVM参数检查
  2. JVM参数生成

使用说明

1.copy脚本

在有写入权限的目录下执行以下脚本即可:

1
mkdir jtoolkit && cd jtoolkit && wget --no-cache --no-check-certificate https://raw.githubusercontent.com/fengfu/jtoolkit/master/jtoolkit.sh && source jtoolkit.sh

2.运行SecureCRT Script

使用SecureCRT的同学可以下载run_jtoolkit.py文件到自己的机器上,通过[Script]-[Run]功能运行此下载的文件。

另外,某些脚本需要sudo权限,所以请确保您在运行的服务器上已经申请到了sudo权限。

反馈

欢迎移步https://github.com/fengfu/jtoolkit/issues 拍砖~

重计算型应用的性能优化实践

发表于 2018-06-27 | 阅读次数

系统介绍

ATPCO系统是Qunar国际机票的运价系统,它通过美国ATPCO公司接收全球近500家航空公司的运价、规则数据进行计算,为上层的机票搜索系统提供运价搜索服务。系统的基础数据主要分为运价、规则、航路等数据,其中运价数据有9000万条,规则数据有1.2亿条,平均每条运价关联20条规则数据。规则数据分为Record 0/1/2/3/6/8六大类,其中Record3数据又包含Category1~Category50几十种子规则,以及Table 900系列的若干表、Category 10下面的各种子表。所以业务规则复杂,数据量大是这个系统的一大特点。

ATPCO系统的另外一个特点是计算量大。我们以北京-香港,2018年7月1日的单程搜索为例来粗略估算一下搜索发生时系统的计算量:

  1. 航路数量(中转点,如上海、厦门等):5
  2. 运价数量:500
  3. 航班数量:20
  4. 舱位数量:10
  5. 舱位规则:2

上述因素之间是叉乘的关系,所以总的计算量=550020102=1000000。也就是单次搜索产生的计算量是100万,而且这只是单程的部分计算,还没有考虑addon、两次中转……。在这种情况的,即便我们使用了24核的物理机并发计算,系统的压力依然很大。下图是我们用vmstat命令查看到的CPU运行队列的情况,也可以看到在处理请求时,CPU的运行队列都超过了CPU Processor数量。
计算压力大

所以,系统上线初期,系统响应比较慢,单程平均响应时间500ms,往返多达1500ms,部分往返搜索甚至超时。

问题分析及应对策略

在积累了一段时间的运行数据的基础上,结合业务的特点,我们对系统性能较低的原因进行分析,总结有以下几点:

  1. 代码存在瓶颈:如锁等待(如早期QMonitor中的synchronized)、代码效率低(如频繁从InfoCenter中获取机场信息)等;
  2. 计算量太大:一次搜索的计算量太大,线程超时严重,尤其是往返的计算;
  3. 部分计算流程不合理:如有些互相没有依赖的逻辑串行处理、CPU核心忙闲不均拖慢整体响应时间;
  4. 其他因素:GC停顿、外部依赖性能低

    针对上述的问题,我们采用了不同的方案来解决。当然从事后诸葛亮的角度来总结,我们借鉴了2个法则的思想来进行系统性能的提升:针对问题1和问题2,我们可以使用利特尔法则解决;针对问题3,可以利用阿姆达尔定律来解决。

利特尔法则实践

利特尔法则

利特尔法则由麻省理工大学斯隆商学院(MIT Sloan School of Management)的教授John Little于1961年所提出与证明,其英文名称为:Little’s Law。利特尔法则是一个有关提前期与在制品关系的简单数学公式,这一法则为精益生产的改善方向指明了道路。如何有效地缩短生产周期呢?道理很简单:一个方向是提高单位效率,另一个方向就是降低任务数量。

所以基于利特尔法则,我们对系统优化的工作就沿着2个方向前进:

  1. 提高代码性能,这里是我们努力的主要方向;
  2. 减少计算量:也就是剪枝。

提高代码性能

要提高代码性能,首要的工作就是找到代码中耗时比较久的地方,也就是“热点”。这里我们使用了JMC(Java Mission Control)来查找热点。通过热点分析,我们做了以下4个方面的改进:

减少字符串使用

因为需要降低内存占用的缘故,系统中将部分的字符串转成了int/long在堆内存储,这同时也带来了一个好处就是提高了字符串的操作效率,毕竟相比String,int/long的执行效率要高一些。当然String转int/long这种方式比较适合大量存储且不需要频繁转换的场景,所以要慎用。

减少/避免低性能的使用方式

为提升性能,代码中尽量避免低性能的使用方式。比如String.split方法,因为支持正则的缘故而性能偏低,而我们的场景中很少使用正则,所以我们就换成了Guava Splitter;比如尽量避免使用BigDecimal这种为保证精度而牺牲一定性能的类,如果必须使用,那么也优先使用BigDecimal.valueOf方法而不是new BigDecimal()这种方式;再比如要求集合、StringBuilder在使用前必须指定初始化容量,避免扩容造成的内存浪费,从而减少GC时间;

减少IO阻塞、锁等待

IO阻塞、锁等待是系统性能提升的大敌。对于减少IO阻塞,一个典型的例子就是减少日志的打印以及调整日志组件的配置。虽然我们的系统中使用了异步方式打印日志,但当日志量比较大的时候,依然可能引起日志的阻塞。所以我们的策略是减少不必要的日志打印。同时,为避免logback阻塞主线程,我们将其neverBlock属性设置为true,允许丢失一部分非关键日志,但换来的好处就是不会因为日志打印而阻塞主线程。

另外,通过热点查找,我们还定位到了当时使用的监控组件存在锁等待的问题,并推动相关团队进行了改进。

提高缓存效率

由于系统大量使用了缓存,在进行优化的过程中,我们一度发现使用的Guava Cache也成了热点,于是我们也寻求了解决方案。通过搜索引擎和github,我们发现Caffeine宣称拥有比Guava Cache高几倍的读写效率,于是我们进行了测试和试用,发现在我们的应用场景下(读多写少),Caffeine的性能确实要好于Guava Cache。所以我们最终采用Caffeine作为了本地缓存方案。对于Caffeine Cache,感兴趣的同学可以通过文末给出的链接进行了解。

写性能比较

读性能比较

剪枝

在对代码性能进行充分压榨后,我们依然无法解决往返计算时间太长甚至超时的问题,原因在于往返的去程、回程航班组合存在笛卡尔积的缘故,导致往返的计算量比单程要多2个数量级。

在无法对程序性能做进一步榨取的情况下,我们开始考虑在庞大的计算量中进行取舍。前面我们提到了,初期的系统对往返去程、回程的航班组合比较粗放,采取笛卡尔积的方式,如下图:

笛卡尔积

这样带来的结果就是航班组合数量非常庞大,系统处理不过来。所以我们将去程、回程航班以价格进行排序,在去程航班的基础上选取回程航班,如下图所示:

航班组合优化

通过这样的优化,往返搜索需要处理的航班组合数量减少了50%以上,搜索超时的量也减少了30%以上。

当然,在剪枝的层面上的优化,我们还是比较保守的,毕竟这需要强大的业务及分析能力做后盾才有能力取得比较好的成果。在剪枝策略上,携程的引擎团队做得比我们要激进得多,但效果也好很多。他们将航班组合剪枝到了我们的20%,但对业务的影响小到可以忽略。因此在这一点上,我们需要向他们好好学习。

阿姆达尔定律实践

阿姆达尔定律(英语:Amdahl’s law,Amdahl’s argument),一个计算机科学界的经验法则,因吉恩·阿姆达尔(Gene Amdahl)而得名。它代表了处理器平行运算之后效率提升的能力。譬如说,你的程序50%是串行的,其他一半可以并行,那么,最大的加速比就是2。不管你用多少处理器并行,这个加速比不可能提高。在这种情况下,改进串行算法可能比多核处理器并行更有效。
撇开上面那些文绉绉的话,在这个方向上,我们做的优化就是:

  1. 提高并发效率
  2. 并行处理

提高并发效率

在提高并发效率方面,值得一提的一点就是:针对计算量比较大的逻辑,将普通的线程池改成了ForkJoinPool。事情的起因还是在提升往返计算性能的过程中,我们通过mpstat命令查看线上服务器CPU Processor的使用率,发现各CPU Processor忙闲不均,如下图:

CPU处理器使用率不均匀

这种现象引起了我们的警觉,我们意识到在忙闲不均的情况下,运行压力最大的线程将拖慢整个请求的处理速度。因此我们将计算量比较大的线程池改成了ForkJoinPool,依赖它的Work-Stealing机制来确保各线程都有任务可以执行,避免出现忙闲不均的情况。
通过这个优化,往返搜索的超时率进一步降低,同时服务器CPU Processor使用率也变得更加均衡,下面这张图就是优化后再通过mpstat查看到CPU核心使用率的情况。
优化后的CPU使用率
不管ForkJoinPool也好,还是Java8中出现的stream(默认基于ForkJoinPool)也好,都是有自己适合的场景,不是什么情况都适合的。Work-Stealing的适用场景是不同的任务的耗时相差比较大,即某些任务需要运行较长时间,而某些任务会很快的运行完成,这种情况下用 Work-Stealing很合适;但是如果任务的耗时很平均,则此时 Work-Stealing 并不适合,因为窃取任务时不同线程需要抢占锁,这可能会造成额外的时间消耗,而且每个线程维护双端队列也会造成更大的内存消耗。所以 ForkJoinPool 并不是 ThreadPoolExecutor 的替代品,而是作为对 ThreadPoolExecutor 的补充。

并行处理

在并行处理方面,我们将互相独立的计算类型(Specified fare/Constructed fare/End-on-end)拆分到不同的机器上进行处理,以减轻单台机器的计算压力,同时也能通过并行来进一步提高处理系统的处理能力。当然,这方面我们要面对的挑战就是系统的逻辑将变得更加复杂,毕竟任务调度的效率也将影响整个搜索的效率。

总结

上述内容就是我们在对系统优化过程中提炼的一些值得一提的点,因为篇幅的限制,还有一些较为底层的优化在这里无法展开来分享,这里讲考虑后续通过小专题的方式继续分享。针对本文的内容或相关的领域,也欢迎大家提出宝贵的意见和建议,谢谢。

相关链接:

  1. Caffeine github:https://github.com/ben-manes/caffeine
  2. Caffeine原理:https://segmentfault.com/a/1190000008751999
12…6
宁静·致远

宁静·致远

60 日志
9 标签
友情链接
  • 卡拉搜索
© 2020 宁静·致远
由 Hexo 强力驱动
主题 - NexT.Gemini