添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

dump heap

如果Java应用程序出现了内存泄露,千万别着急着把应用杀掉,而是要保存现场。如果是互联网应用,可以把流量切到其他服务器。保存现场的目的就是为了把运行中JVM的heap dump下来。

JDK自带的jmap工具,可以做这件事情。它的执行方法是:

Java代码

file-heap.bin的含义是,dump出来的文件名是heap.bin。

<pid>就是JVM的进程号。

(在 linux 下)先执行ps aux | grep java,找到JVM的pid;然后再执行jmap -dump:format=b,file=heap.bin <pid>,得到heap dump文件。

analyze heap

将二进制的heap dump文件解析成human-readable的信息,自然是需要专业工具的帮助,这里推荐 Memory Analyzer

Memory Analyzer,简称MAT,是Eclipse基金会的开源项目,由SAP和IBM捐助。巨头公司出品的软件还是很中用的,MAT可以分析包含数亿级对象的heap、快速计算每个对象占用的内存大小、对象之间的引用关系、自动检测内存泄露的嫌疑对象,功能强大,而且界面友好易用。

MAT的界面基于Eclipse开发,以两种形式发布:Eclipse插件和Eclipe RCP。MAT的分析结果以图片和报表的形式提供,一目了然。总之个人还是非常喜欢这个工具的。下面先贴两张官方的screenshots:

言归正传,我用MAT打开了heap.bin,很容易看出, char[]的数量出其意料的多,占用90%以上的内存 。一般来说,char[]在JVM确实会占用很多内存,数量也非常多,因为String对象以char[]作为内部存储。但是这次的char[]太贪婪了,仔细一观察, 发现有数万计的char[],每个都占用数百K的内存 。这个现象说明, Java程序保存了数以万计的大String对象 。结合程序的逻辑,这个是不应该的,肯定在某个地方出了问题。

在可疑的char[]中,任意挑了一个,使用Path To GC Root功能,找到该char[]的引用路径,发现String对象是被一个HashMap中引用的 。这个也是意料中的事情,Java的内存泄露多半是因为对象被遗留在全局的HashMap中得不到释放。不过,该HashMap被用作一个缓存,设置了缓存条目的阈值,导达到阈值后会自动淘汰。从这个逻辑分析,应该不会出现内存泄露的。虽然缓存中的String对象已经达到数万计,但仍然没有达到预先设置的阈值(阈值设置地比较大,因为当时预估String对象都比较小)。

但是,另一个问题引起了我的注意:为什么缓存的String对象如此巨大?内部char[]的长度达数百K。 虽然缓存中的String对象数量还没有达到阈值,但是String对象大小远远超出了我们的预期,最终导致内存被大量消耗,形成内存泄露的迹象(准确说应该是内存消耗过多)

就这个问题进一步顺藤摸瓜,看看String大对象是如何被放到HashMap中的。通过查看程序的源代码,我发现,确实有String大对象, 不过并没有把String大对象放到HashMap中,而是把String大对象进行split(调用String.split方法),然后将split出来的String小对象放到HashMap中 了。

这就奇怪了,放到HashMap中明明是split之后的String小对象,怎么会占用那么大空间呢?难道是String类的split方法有问题?

带着上述疑问,我查阅了Sun JDK6中String类的代码,主要是是split方法的实现:

Java代码
  • public String[] split(String regex, int limit) {
  • return Pattern.compile(regex).split( this , limit);
  • 可以看出,Stirng.split方法调用了Pattern.split方法。继续看Pattern.split方法的代码:

    Java代码
  • public String[] split(CharSequence input, int limit) {
  • int index = 0 ;
  • boolean matchLimited = limit > 0 ;
  • ArrayList<String> matchList = new ArrayList<String>();
  • Matcher m = matcher(input);
  • // Add segments before each match found
  • while (m.find()) {
  • if (!matchLimited || matchList.size() < limit – 1 ) {
  • String match = input.subSequence(index, m.start()).toString();
  • matchList.add(match);
  • index = m.end();
  • } else if (matchList.size() == limit – 1 ) { // last one
  • String match = input.subSequence(index,
  • input.length()).toString();
  • matchList.add(match);
  • index = m.end();
  • // If no match was found, return this
  • if (index == 0 )
  • return new String[] {input.toString()};
  • // Add remaining segment
  • if (!matchLimited || matchList.size() < limit)
  • matchList.add(input.subSequence(index, input.length()).toString());
  • // Construct result
  • int resultSize = matchList.size();
  • if (limit == 0 )
  • while (resultSize > 0 && matchList.get(resultSize- 1 ).equals( “” ))
  • resultSize–;
  • String[] result = new String[resultSize];
  • return matchList.subList( 0 , resultSize).toArray(result);
  • 注意看第9行:Stirng match = input.subSequence(intdex, m.start()).toString();

    这里的match就是split出来的String小对象,它其实是String大对象subSequence的结果。继续看String.subSequence的代码:

    Java代码
  • public CharSequence subSequence( int beginIndex, int endIndex) {
  • return this .substring(beginIndex, endIndex);
  • String.subSequence有调用了String.subString,继续看:

    Java代码
  • public String substring( int beginIndex, int endIndex) {
  • if (beginIndex < 0 ) {
  • throw new StringIndexOutOfBoundsException(beginIndex);
  • if (endIndex > count) {
  • throw new StringIndexOutOfBoundsException(endIndex);
  • if (beginIndex > endIndex) {
  • throw new StringIndexOutOfBoundsException(endIndex – beginIndex);
  • return ((beginIndex == 0 ) && (endIndex == count)) ? this :
  • new String(offset + beginIndex, endIndex – beginIndex, value);
  • 看第11、12行,我们终于看出眉目,如果subString的内容就是完整的原字符串,那么返回原String对象;否则,就会创建一个新的String对象,但是这个String对象貌似使用了原String对象的char[]。我们通过String的构造函数确认这一点:

    Java代码
  • // Package private constructor which shares value array for speed.
  • String( int offset, int count, char value[]) {
  • this .value = value;
  • this .offset = offset;
  • this .count = count;
  • 为了避免内存拷贝、加快速度,Sun JDK直接复用了原String对象的char[],偏移量和长度来标识不同的字符串内容。也就是说, subString出的来String小对象仍然会指向原String大对象的char[],split也是同样的情况 。这就解释了,为什么HashMap中String对象的char[]都那么大。

    其实上一节已经分析出了原因,这一节再整理一下:

  • 程序从每个请求中得到一个String大对象,该对象内部char[]的长度达数百K。
  • 程序对String大对象做split,将split得到的String小对象放到HashMap中,用作缓存。
  • Sun JDK6对String.split方法做了优化,split出来的Stirng对象直接使用原String对象的char[]
  • HashMap中的每个String对象其实都指向了一个巨大的char[]
  • HashMap的上限是万级的,因此被缓存的Sting对象的总大小=万*百K=G级。
  • G级的内存被缓存占用了,大量的内存被浪费,造成内存泄露的迹象。
  • 原因找到了,解决方案也就有了。split是要用的,但是我们不要把split出来的String对象直接放到HashMap中,而是调用一下String的拷贝构造函数String(String original),这个构造函数是安全的,具体可以看代码:

    Java代码
  • * Initializes a newly created {@code String} object so that it represents
  • * the same sequence of characters as the argument; in other words, the
  • * newly created string is a copy of the argument string. Unless an
  • * explicit copy of {@code original} is needed, use of this constructor is
  • * unnecessary since Strings are immutable.
  • * @param  original
  • *         A {@code String}
  • public String(String original) {
  • int size = original.count;
  • char [] originalValue = original.value;
  • char [] v;
  • if (originalValue.length > size) {
  • // The array representing the String is bigger than the new
  • // String itself.  Perhaps this constructor is being called
  • // in order to trim the baggage, so make a copy of the array.
  • int off = original.offset;
  • v = Arrays.copyOfRange(originalValue, off, off+size);
  • } else {
  • // The array representing the String is the same
  • // size as the String, so no point in making a copy.
  • v = originalValue;
  • this .offset = 0 ;
  • this .count = size;
  • this .value = v;
  • 只是,new String(string)的代码很怪异,囧。或许,subString和split应该提供一个选项,让程序员控制是否复用String对象的char[]。

    是否Bug

    虽然,subString和split的实现造成了现在的问题,但是这能否算String类的bug呢?个人觉得不好说。因为这样的优化是比较合理 的,subString和spit的结果肯定是原字符串的连续子序列。只能说,String不仅仅是一个核心类,它对于JVM来说是与原始类型同等重要的类型。

    JDK实现对String做各种可能的优化都是可以理解的。但是优化带来了忧患,我们程序员足够了解他们,才能用好他们。

    1 内存分析

    1.1 jmap -histo 命令

    pid=`jps | awk ‘{if ($2 == “Jps”) print $1}’`

    jmap -histo $pid >>1.txt 查看pid中类的内存占用

    num     #instances(实例数)         #bytes(占用字节)  class name

    class name 解读

    B代表byte

    C代表char

    D代表double

    F代表float

    I代表int

    J代表long

    Z代表boolean

    前边有[代表数组,[I 就相当于int[]

    对象用[L+类名表示



    如果某个类的个数特别多, 就得检查是否内存溢出了。



    1.2 命令 jmap -heap



    jmap -heap 22792

    Attaching to process ID 22792, please wait…

    Debugger attached successfully.

    Server compiler detected.

    JVM version is 19.0-b09



    using thread-local object allocation.

    Parallel GC with 8 thread(s)



    Heap Configuration:

    MinHeapFreeRatio = 40                            # 对应jvm启动参数 -XX:MinHeapFreeRatio 设置JVM堆最小空闲比率 (默认40)

    MaxHeapFreeRatio = 70                            # 对应jvm启动参数 -XX:MaxHeapFreeRatio 设置JVM堆最大空闲比率 (默认70)

    MaxHeapSize      = 10737418240 (10240.0MB)       # 对应jvm启动参数 -XX:MaxHeapSize 设置JVM堆的最大大小

    NewSize          = 5368709120 (5120.0MB)         # 对应jvm启动参数 -XX:NewSize 设置JVM堆的年轻代的默认大小

    MaxNewSize       = 5368709120 (5120.0MB)         # 对应jvm启动参数 -XX:MaxNewSize 设置JVM堆的年轻带的最大大小

    OldSize          = 5439488 (5.1875MB)            # 对应jvm启动参数 -XX:OldSize 设置JVM堆的老年代的大小

    NewRatio         = 2                             # 对应jvm启动参数 -XX:NewRatio 老年代与年轻代的大小比率

    SurvivorRatio    = 8                             # 对应jvm启动参数 -XX:SurvivorRatio 设置年轻代中Eden区与Survivor区的大小比值

    PermSize         = 21757952 (20.75MB)            # 对应jvm启动参数 -XX:PermSize 设置JVM堆的持久带的初始大小

    MaxPermSize      = 1073741824 (1024.0MB)         # 对应jvm启动参数 -XX:MaxPermSize 设置JVM堆的永生代的最大大小



    Heap Usage:

    PS Young Generation

    Eden Space:                                         # Eden区内存分布 总量 已使用 空闲 使用比率

    capacity = 5357305856 (5109.125MB)

    used     = 1647437208 (1571.118553161621MB)

    free     = 3709868648 (3538.006446838379MB)

    30.751225565270396% used

    From Space:                                         # 其中一个Survivor(sərˈvaɪvər)区内存分布 总量 已使用 空闲 使用比率

    capacity = 5898240 (5.625MB)

    used     = 2375696 (2.2656402587890625MB)

    free     = 3522544 (3.3593597412109375MB)

    40.278049045138886% used

    To Space:                                           # 另一个Survivor区内存分布 总量 已使用 空闲 使用比率

    capacity = 5505024 (5.25MB)

    used     = 0 (0.0MB)

    free     = 5505024 (5.25MB)

    0.0% used

    PS Old Generation                                   # 当前老年代内存分布 总量 已使用 空闲 使用比率

    capacity = 5368709120 (5120.0MB)

    used     = 181392168 (172.98905181884766MB)

    free     = 5187316952 (4947.010948181152MB)

    3.3786924183368683% used

    PS Perm Generation                                  # 当前持久代内存分布 总量 已使用 空闲 使用比率

    capacity = 72286208 (68.9375MB)

    used     = 72213176 (68.86785125732422MB)

    free     = 73032 (0.06964874267578125MB)

    99.89896827898346% used



    1.3

    jstat -gcutil [pid] [internal]  很实用

    S0: Survivor space 0 区已使用空间的百分比

    S1: Survivor space 1 区已使用空间的百分比

    E: Eden space 区已使用空间的百分比

    O: Old space 区已使用空间的百分比

    P: Perm space 区已使用空间的百分比

    YGC: Young GC 的次数

    YGCT: Young GC 所用的时间 单位秒

    FGC: Full GC 的次数

    FGCT: Full GC 所用的时间 单位秒

    GCT: 用于垃圾回收的总时间 单位秒



    1.4

    尽量减少Full GC的次数, 因为Full GC的消耗要比Monitor GC要大

    年轻代大小: 尽可能设大, 降低年轻代GC次数, 同时也减少达到老年代的对象?

    分配堆栈的最小值最好等于最大值, 因为动态分配也是需要耗费时间的. 如年轻代, 老年代, 持久代的最小最大值可设为一致



    2 参数调优

    http://blog.csdn.net/historyasamirror/article/details/6233007

    jmap (linux下特有,也是很常用的一个命令)

    观察运行中的jvm物理内存的占用情况。

    参数如下:

    -heap :打印jvm heap的情况

    -histo: 打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。

    -histo:live : 同上,但是只答应存活对象的情况

    -permstat: 打印permanent generation heap情况

    命令使用:

    jmap -heap 3409

    可以观察到New Generation(Eden Space,From Space,To Space),tenured generation,Perm Generation的 内存使用情况

    输出内容:

    jmap -histo 3409 | jmap -histo:live 3409

    可以观察heap中所有对象的情况(heap中所有生存的对象的情况)。包括对象数量和所占空间大小。

    输出内容:

    写个脚本,可以很快把占用heap最大的对象找出来,对付内存泄漏特别有效。

    如果结果很多,可以用以下命令输出到文本文件。

    jmap -histo 3409 | jmap -histo:live 3409 > a.txt

    jinfo:可以输出并修改运行时的java 进程的opts。

    jps:与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号。

    jstat:一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。

    jmap:打印出某个java进程(使用pid)内存内的所有’对象’的情况(如:产生那些对象,及其数量)。

    jconsole:一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM。

    详细:在使用这些工具前,先用JPS命令获取当前的每个JVM进程号,然后选择要查看的JVM。

    jstat工具特别强大,有众多的可选项,详细查看堆内各个部分的使用量,以及加载类的数量。使用时,需加上查看进程的进程id,和所选参数。以下详细介绍各个参数的意义。

    jstat -class pid:显示加载class的数量,及所占空间等信息。

    jstat -compiler pid:显示VM实时编译的数量等信息。

    jstat -gc pid:可以显示gc的信息,查看gc的次数,及时间。其中最后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。

    jstat -gccapacity:可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小,如:PGCMN显示的是最小perm的内存使用量,PGCMX显示的是perm的内存最大使用量,PGC是当前新生成的perm内存占用量,PC是但前perm内存占用量。其他的可以根据这个类推, OC是old 内纯的占用量

    jstat -gcnew pid:new对象的信息。

    jstat -gcnewcapacity pid:new对象的信息及其占用量。

    jstat -gcold pid:old对象的信息。

    jstat -gcoldcapacity pid:old对象的信息及其占用量。

    jstat -gcpermcapacity pid: perm对象的信息及其占用量。

    jstat -util pid:统计gc信息统计。

    jstat -printcompilation pid:当前VM执行的信息。

    除了以上一个参数外,还可以同时加上 两个数字,如:jstat -printcompilation 3024 250 6是每250毫秒打印一次,一共打印6次,还可以加上-h3每三行显示一下标题。

    jmap是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。

    命令:jmap -dump:format=b,file=heap.bin

    file:保存路径及文件名

    pid:进程编号

    ?jmap -histo:live pid| less :堆中活动的对象以及大小

    ?jmap -heap pid : 查看堆的使用状况信息

    jinfo:的用处比较简单,就是能输出并修改运行时的java进程的运行参数。用法是jinfo -opt pid 如:查看2788的MaxPerm大小可以用 jinfo -flag MaxPermSize 2788。

    jconsole是一个用java写的GUI程序,用来监控VM,并可监控远程的VM,非常易用,而且功能非常强。使用方法:命令行里打 jconsole,选则进程就可以了。

    JConsole中关于内存分区的说明。

    Eden Space (heap): 内存最初从这个线程池分配给大部分对象。

    Survivor Space (heap):用于保存在eden space内存池中经过垃圾回收后没有被 回收的对象。

    Tenured Generation (heap):用于保持已经在 survivor space内存池中存在了一段时间的对象。

    Permanent Generation (non-heap): 保存虚拟机自己的静态(refective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的,

    Code Cache (non-heap):HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)

    ?jstack ( 查看jvm线程运行状态,是否有死锁现象等等信息) : jstack pid : thread dump

    ?jstat -gcutil pid 1000 100 : 1000ms统计一次gc情况统计100次;

    另外推荐一款查看jmap dump 的内存对象工具 MemoryAnalyzer

    版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

    文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/14281.html

    (0)

    在人生的道路上,不管是潇洒走一回,或者是千山独行,皆须是自己想走的路,虽然,有的人并不是很快就能找到自己的方向和道路,不过,只要坚持到底,我相信,就一定可以找到自己的路,只要找到路,就不必怕路途遥远了。

    导读:本篇文章讲解 Linux学习-12-Vim文本编辑器使用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源: 原文

    Java自学入门 2023年3月5日 0 0 1

    在上一篇文章《Redis列表实现原理之ziplist结构》,我们分析了ziplist结构如何使用一块完整的内存存储列表数据。同时也提出了一个问题:如果链表很长,ziplist中每次…

    2022年6月16日 0 0 1

    在人生的道路上,不管是潇洒走一回,或者是千山独行,皆须是自己想走的路,虽然,有的人并不是很快就能找到自己的方向和道路,不过,只要坚持到底,我相信,就一定可以找到自己的路,只要找到路,就不必怕路途遥远了。

    导读:本篇文章讲解 Linux学习-05-Linux文件目录结构,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源: 原文

    2023年3月5日 0 0 3

    追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

    导读:本篇文章讲解 (十三)【Java程序员必背知识点】RabbitMQ,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源: 原文

    2023年4月6日 0 0 2

    追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

    导读:本篇文章讲解 CentOS安装Docker,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源: 原文

    2023年4月6日 0 0 3