Java开发大型互联网高阶框架内存泄漏之JVM监控实战原理
引言
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机器是实现这一特点的关键。一般的高阶语言如果要在不同的平台上执行,至少需要编译成不同的目的码。而引入Java语言虚拟机器后,Java语言在不同平台上执行时不需要重新编译。Java语言使用Java虚拟机器遮蔽了与具体平台相关的资讯,使得Java语言编译程式只需生成在Java虚拟机器上执行的目的码(字节码),就可以在多种平台上不加修改地执行。Java虚拟机器在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处执行”的原因。
jconsole – jconsole是基于Java Management Extensions (JMX)的实时图形化监测工具,这个工具利用了内建到JVM里面的JMX指令来提供实时的效能和资源的监控,包括了Java 程式的内存使用,Heap size, 执行绪的状态,类的分配状态和空间使用等等。
jinfo – jinfo可以从core档案里面知道崩溃的Java应用程序的配置资讯,目前只有在Solaris和Linux的JDK版本里面才有。
jmap – jmap 可以从core档案或程序中获得内存的具体匹配情况,包括Heap size, Perm size等等,目前只有在Solaris和Linux的JDK版本里面才有。
jdb – jdb 用来对core档案和正在执行的Java程序进行实时地除错,里面包含了丰富的命令帮助您进行除错,它的功能和Sun studio里面所带的dbx非常相似,但 jdb是专门用来针对Java应用程序的。
jstat – jstat利用了JVM内建的指令对Java应用程序的资源和效能进行实时的命令列的监控,包括了对Heap size和垃圾回收状况的监控等等。
jps – jps是用来检视JVM里面所有程序的具体状态, 包括程序ID,程序启动的路径等等。
jstatd
启动jvm监控服务。它是一个基于rmi的应用,向远端机器提供本机jvm应用程序的资讯。预设埠1099。
例项:jstatd -J-Djava.security.policy=my.policy
my.policy档案需要自己建立,内如如下:
grant codebase "file:$JAVA_HOME/lib/tools.jar" { permission java.security.AllPermission; };
这是安全策略档案,因为jdk对jvm做了jaas的安全检测,所以我们必须设定一些策略,使得jstatd被允许作网络操作
上面的操作没有通过,出现:
Could not create remote objectaccess denied (java.util.PropertyPermission java.rmi.server.ignoreSubClasses write)java.security.AccessControlException: access denied (java.util.PropertyPermission java.rmi.server.ignoreSubClasses write)at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323)at java.security.AccessController.checkPermission(AccessController.java:546)at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)at java.lang.System.setProperty(System.java:727)at sun.tools.jstatd.Jstatd.main(Jstatd.java:122)
create in your usr/java/bin the jstatd.all.policy file, with the content must be
grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; };
JPS
列出所有的jvm例项
例项:
jps
列出本机所有的jvm例项
jps 192.168.0.77
列出远端服务器192.168.0.77机器所有的jvm例项,采用rmi协议,预设连线埠为1099(前提是远端服务器提供jstatd服务)
输出内容如下:
[email protected]:~/data/ebook/java/j2se/jdk_gc$ jps
6286 Jps
6174 Jstat
jconsole
一个图形化界面,可以观察到java程序的gc,class,内存等资讯。虽然比较直观,但是个人还是比较倾向于使用jstat命令(在最后一部分会对jstat作详细的介绍)。
jinfo (linux下特有)
观察执行中的java程式的执行环境引数:引数包括Java System属性和JVM命令列引数
例项:jinfo 2083
其中2083就是java程序id号,可以用jps得到这个id号。
输出内容太多了,不在这里一一列举,大家可以自己尝试这个命令。
jstack (linux下特有)
可以观察到jvm中当前所有执行绪的执行情况和执行绪当前状态
jstack 2083
输出内容如下:
jmap (linux下特有,也是很常用的一个命令)
观察执行中的jvm实体内存的占用情况。
引数如下:
-heap :打印jvm heap的情况-histo: 打印jvm heap的直方图。其输出资讯包括类名,物件数量,物件占用大小。-histo:live : 同上,但是只答应存活物件的情况-permstat: 打印permanent generation heap情况
命令使用:
jmap -heap 2083
可以观察到New Generation(Eden Space,From Space,To Space),tenured generation,Perm Generation的内存使用情况
输出内容:
jmap -histo 2083 | jmap -histo:live 2083
可以观察heap中所有物件的情况(heap中所有生存的物件的情况)。包括物件数量和所占空间大小。
输出内容:
写个指令码,可以很快把占用heap最大的物件找出来,对付内存泄漏特别有效。
jstat
最后要重点介绍下这个命令。
这是jdk命令中比较重要,也是相当实用的一个命令,可以观察到classloader,compiler,gc相关资讯
具体引数如下:
-class:统计class loader行为资讯
-compile:统计编译行为资讯
-gc:统计jdk gc时heap资讯
-gccapacity:统计不同的generations(不知道怎么翻译好,包括新生区,老年区,permanent区)相应的heap容量情况
-gccause:统计gc的情况,(同-gcutil)和引起gc的事件
-gcnew:统计gc时,新生代的情况
-gcnewcapacity:统计gc时,新生代heap容量
-gcold:统计gc时,老年区的情况
-gcoldcapacity:统计gc时,老年区heap容量
-gcpermcapacity:统计gc时,permanent区heap容量
-gcutil:统计gc时,heap情况
-printcompilation:不知道干什么的,一直没用过。
一般比较常用的几个引数是:
jstat -class 2083 1000 10 (每隔1秒监控一次,一共做10次)
输出内容含义如下:
LoadedNumber of classes loaded.BytesNumber of Kbytes loaded.UnloadedNumber of classes unloaded.BytesNumber of Kbytes unloaded.TimeTime spent performing class load and unload operations.
jstat -gc 2083 2000 20(每隔2秒监控一次,共做10)
输出内容含义如下:
S0CCurrent survivor space 0 capacity (KB).ECCurrent eden space capacity (KB).EUEden space utilization (KB).OCCurrent old space capacity (KB).OUOld space utilization (KB).PCCurrent permanent space capacity (KB).PUPermanent space utilization (KB).YGCNumber of young generation GC Events.YGCTYoung generation garbage collection time.FGCNumber of full GC events.FGCTFull garbage collection time.GCTTotal garbage collection time.
输出内容:
如果能熟练运用这些命令,尤其是在linux下,那么完全可以代替jprofile等监控工具了,谁让它收费呢。呵呵。用命令的好处就是速度快,并且辅助于其他命令,比如grep gawk sed等,可以组装多种符合自己需求的工具。
jps 的用法
用来检视 JVM 里面所有程序的具体状态 , 包括程序 ID ,程序启动的路径等等。 与 unix 上的 ps 类似,用来显示本地的 java 程序,可以检视本地执行着几个 java 程式,并显示他们的程序号。
[[email protected] ~]# jps
25517 Jps
25444 Bootstrap
jstack 的用法
如果 java 程式崩溃生成 core 档案, jstack 工具可以用来获得 core 档案的 java stack 和 native stack 的资讯,从而可以轻松地知道 java 程式是如何崩溃和在程式何处发生问题。另外, jstack 工具还可以附属到正在执行的 java 程式中,看到当时执行的 java 程式的 java stack 和 native stack 的资讯 , 如果现在执行的 java 程式呈现 hung 的状态, jstack 是非常有用的。目前只有在 Solaris 和 Linux 的 JDK 版本里面才有。
[[email protected] bin]# jstack 25444
Attaching to process ID 25917, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 1.5.0_08-b03
Thread 25964: (state = BLOCKED)
Error occurred during stack walking:
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: get_thread_regs failed for a lwp
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:134)
at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.getThreadIntegerRegisterSet(LinuxDebuggerLocal.java:437)
at sun.jvm.hotspot.debugger.linux.LinuxThread.getContext(LinuxThread.java:48)
at
jstat 的用法
用以判断JVM 是否存在内存问题呢?如何判断JVM 垃圾回收是否正常?一般的top 指令基本上满足不了这样的需求,因为它主要监控的是总体的系统资源,很难定位到java 应用程序。
Jstat 是JDK 自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool” ,它位于java 的bin 目录下,主要利用JVM 内建的指令对Java 应用程序的资源和效能进行实时的命令列的监控,包括了对Heap size 和垃圾回收状况的监控。可见,Jstat 是轻量级的、专门针对JVM 的工具,非常适用。由于JVM 内存设定较大,图中百分比变化不太明显
一个极强的监视 VM 内存工具。可以用来监视 VM 内存内的各种堆和非堆的大小及其内存使用量。
jstat 工具特别强大,有众多的可选项,详细检视堆内各个部分的使用量,以及载入类的数量。使用时,需加上检视程序的程序 id ,和所选引数。
语法结构:
Usage: jstat -help|-options
jstat - [-t] [-h] [ []]
引数解释:
Options — 选项,我们一般使用 -gcutil 检视gc 情况
vmid — VM 的程序号,即当前执行的java 程序号
interval– 间隔时间,单位为秒或者毫秒
count — 打印次数,如果预设则打印无数次
S0 — Heap 上的 Survivor space 0 区已使用空间的百分比
S1 — Heap 上的 Survivor space 1 区已使用空间的百分比
E — Heap 上的 Eden space 区已使用空间的百分比
O — Heap 上的 Old space 区已使用空间的百分比
P — Perm space 区已使用空间的百分比
YGC — 从应用程序启动到取样时发生 Young GC 的次数
YGCT– 从应用程序启动到取样时 Young GC 所用的时间( 单位秒 )
FGC — 从应用程序启动到取样时发生 Full GC 的次数
FGCT– 从应用程序启动到取样时 Full GC 所用的时间( 单位秒 )
GCT — 从应用程序启动到取样时用于垃圾回收的总时间( 单位秒)
jstat –gccapacity : 可以显示, VM 内存中三代( young,old,perm )物件的使用和占用大小,如: PGCMN 显示的是最小 perm 的内存使用量, PGCMX 显示的是 perm 的内存最大使用量, PGC 是当前新生成的 perm 内存占用量, PC 是但前 perm 内存占用量。其他的可以根据这个类推, OC 是 old 内纯的占用量。
[[email protected] bin]# jstat -gccapacity 25917
NGCMN 640.0
NGCMX 4992.0
NGC 832.0
S0C 64.0
S1C 64.0
EC 704.0
OGCMN 1408.0
OGCMX 60544.0
OGC 9504.0
OC 9504.0 OC 是 old 内纯的占用量
PGCMN 8192.0 PGCMN 显示的是最小 perm 的内存使用量
PGCMX 65536.0 PGCMX 显示的是 perm 的内存最大使用量
PGC 12800.0 PGC 是当前新生成的 perm 内存占用量
PC 12800.0 PC 是但前 perm 内存占用量
YGC 164
FGC 6
jmap 的用法
打印出某个 java 程序(使用 pid )内存内的,所有 ‘ 物件 ’ 的情况(如:产生那些物件,及其数量)。
可以输出所有内存中物件的工具,甚至可以将 VM 中的 heap ,以二进位制输出成文字。使用方法 jmap -histo pid 。如果连用 SHELL jmap -histo pid>a.log 可以将其储存到文字中去,在一段时间后,使用文字对比工具,可以对比出 GC 回收了哪些物件。
jinfo 的用法
可以输出并修改执行时的 java 程序的 opts 。用处比较简单,就是能输出并修改执行时的 java 程序的执行引数。用法是 jinfo -opt pid 如:检视 2788 的 MaxPerm 大小可以用 jinfo -flag MaxPermSize 2788 。
jconsole 的用法
jconsole: 一个 java GUI 监视工具,可以以图表化的形式显示各种资料。并可通过远端连线监视远端的服务器 VM 。
用 java 写的 GUI 程式,用来监控 VM ,并可监控远端的 VM ,非常易用,而且功能非常强。命令列里打 jconsole ,选则程序就可以了
jmap 的用法
打印出某个 java 程序(使用 pid )内存内的,所有 ‘ 物件 ’ 的情况(如:产生那些物件,及其数量)。
可以输出所有内存中物件的工具,甚至可以将 VM 中的 heap ,以二进位制输出成文字。使用方法 jmap -histo pid 。如果连用 SHELL jmap -histo pid>a.log 可以将其储存到文字中去,在一段时间后,使用文字对比工具,可以对比出 GC 回收了哪些物件。
原理
内存分配策略
首先我们来了解程式执行时,所需内存的分配策略:
按照编译原理的观点,程式执行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的,对应的,三种储存策略使用的内存空间主要分别是静态储存区(也称方法区)、堆区和栈区。他们的功能不同,对他们使用方式也就不同。
静态储存区(方法区):内存在程式编译的时候就已经分配好,这块内存在程式整个执行期间都存在。它主要存放静态资料、全域性static资料和常量。
栈区:在执行函式时,函式内区域性变数的储存单元都可以在栈上建立,函式执行结束时这些储存单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区:亦称动态内存分配。程式在执行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存(Java则依赖垃圾回收器)。动态内存的生存期可以由我们决定,如果我们不释放内存,程式将在最后才释放掉动态内存。 但是,良好的程式设计习惯是:如果某动态内存不再使用,需要将其释放掉。
在函式中(说明是区域性变数)定义的一些基本型别的变数和物件的引用变数都是在函式的栈内存中分配。当在一段程式码块中定义一个变数时,java就在栈中为这个变数分配内存空间,当超过变数的作用域后,java会自动释放掉为该变数分配的内存空间,该内存空间可以立刻被另作他用。
堆内存用于存放所有由new建立的物件(内容包括该物件其中的所有成员变数)和阵列。在堆中分配的内存,由java虚拟机器自动垃圾回收器来管理。在堆中产生了一个数组或者物件后,还可以在栈中定义一个特殊的变数,这个变数的取值等于阵列或者物件在堆内存中的首地址,在栈中的这个特殊的变数就变成了阵列或者物件的引用变数,以后就可以在程式中使用栈内存中的引用变数来访问堆中的阵列或者物件,引用变数相当于为阵列或者物件起的一个别名,或者代号。
堆是不连续的内存区域(因为系统是用连结串列来储存空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大。栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设定)。
为什么内存泄漏
为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理(堆)内存的。Java的内存管理就是物件的分配和释放问题。在Java中,内存的分配是由程式完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过呼叫函式来释放内存,但它只能回收无用并且不再被其它物件引用的那些物件所占用的空间。
Java的内存垃圾回收机制是从程式的主要执行物件(如静态物件/暂存器/栈上指向的堆内存物件等)开始检查引用链,当遍历一遍后得到上述这些无法回收的物件和他们所引用的物件链,组成无法回收的物件集合,而其他孤立物件(集)就作为垃圾回收。GC为了能够正确释放物件,必须监控每一个物件的执行状态,包括物件的申请、引用、被引用、赋值等,GC都需要进行监控。监视物件状态是为了更加准确地、及时地释放物件,而释放物件的根本原则就是该物件不再被引用。
在Java中,这些无用的物件都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。虽然,我们有几个函式可以访问GC,例如执行GC的函式System.gc(),但是根据Java语言规范定义,该函式不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC。通常GC的执行绪的优先级别较低。JVM呼叫GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。
堆内存中的长生命周期的物件持有短生命周期物件的强/软引用,尽管短生命周期物件已经不再需要,但是因为长生命周期物件持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因`。
内存泄漏常见原因
集合类
集合类如果仅仅有新增元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全域性性的变数 (比如类中的静态属性,全域性性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。
Android 元件或特殊集合物件的使用
BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。
不要直接对 Activity 进行直接引用作为成员变数,如果不得不这么做,请用 private WeakReference mActivity 来做,相同的,对于Service 等其他有自己宣告周期的物件来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。
Handler
要知道,只要 Handler 传送的 Message 尚未被处理,则该 Message 及传送它的 Handler 物件将被执行绪 MessageQueue 一直持有。由于 Handler 属于 TLS(Thread Local Storage) 变数, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler 的使用要尤为小心,否则将很容易导致内存泄露的发生。
Thread 内存泄露
执行绪也是造成内存泄露的一个重要的源头。执行绪产生内存泄露的主要原因在于执行绪生命周期的不可控。比如执行绪是 Activity 的内部类,则执行绪物件中储存了 Activity 的一个引用,当执行绪的 run 函式耗时较长没有结束时,执行绪物件是不会被销毁的,因此它所引用的老的 Activity 也不会被销毁,因此就出现了内存泄露的问题。
异步执行绪未完成前退出 Activity 等元件,可能会导致界面资源无法释放。
这种情况是典型的执行绪物件导致的内存泄露。原因也很简单,执行绪 Thread 物件的 run 任务未执行完之前,物件本身是不会释放的。因此 Activity 等元件物件内的执行绪物件成员如果有耗时任务(一般也都是耗时任务),就会导致一直持有元件本身的引用内存泄露!
本文部分内容和经验摘自网络,结合本次内存泄露的排查总结予以归纳
工具
AS的Memory视窗
可以图表的方式显示内存情况 - 空闲内存 & 已使用内存.
adb shell dumpsys meminfo com.mogujie
非常好的工具 可以显示java heap 、 native heap、共享内存、堆叠资讯。
MAT
Eclipse Memory Analysis Tools(点我下载)是一个专门分析Java堆资料内存引用的工具,我们可以使用它方便的定位内存泄露原因,核心任务就是找到GC ROOT位置即可,哎呀,关于这个工具的使用我是真的不想说了,自己搜寻吧,实在简单、传统的不行了。
PS:这是开发中使用频率非常高的一个工具之一,麻烦务必掌握其核心使用技巧
DDMS-Heap
DDMS 自带的内存分析工具 比较简单 使用频度不高
leakcanary
leakcanary是一个开源专案,一个内存泄露自动检测工具,是著名的GitHub开源组织Square贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十情况是OK的,其核心原理与MAT工具类似。
总结
以 上就是我对Java开发大型互联网高阶框架内存泄漏之JVM监控实战原理 问题及其优化总结,分享给大家,觉得收获的话可以点个关注收藏转发一波喔,谢谢大佬们支援!最后,每一位读到这里的网友,感谢你们能耐心地看完。希望在成为一名更优秀的Java程序员的道路上,我们可以一起学习、一起进步!都能赢取白富美,走向架构师的人生巅峰!
想了解学习Java方面的技术内容以及Java技术视讯的内容可加群:722040762 验证码:头条(06 必过)欢迎大家的加入哟!
