您的位置:首页 > 其它

干货|JVM内存模型和常规问题定位手段

2017-11-08 00:00 375 查看
点击上方“中兴开发者社区”,关注我们

每天读一篇一线开发者原创好文


1.JVM是什么

JVM是一种规范,它屏蔽了不同操作系统的差异,使得运行在其上的代码”一次编写,到处执行”成为可能。通常所说的和使用的最多的是sun公司的一个JVM的实现版本(HotSpot),当然我们也可以自行实现JVM中的某些部件如“class loader”。JVM规范规定了JVM必须由如下部件组成:



(1)、 Class loader(类装载器) 子系统;
根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。Javsa程序员可以extends java.lang.ClassLoader类来写自己的Class loader。
(2)、Execution engine(执行引擎) 子系统;
执行classes中的指令。任何JVM specification实现(JDK)的核心是Execution engine, 换句话说:Sun 的JDK 和IBM的JDK好坏主要取决于他们各自实现的Execution engine的好坏。每个运行中的线程都有一个Execution engine的实例。
(3)、Native interface(本地接口)组件;
      Native interface与native libraries交互,是其它编程语言交互的接口。
(4)、Runtime data area (运行时数据区域)组件①
    运行时数据区域组件包含:Heap (堆)、 Method Area(方法区域)、Java Stack(java的栈)、Program Counter(程序计数器)、Native method stack(本地方法栈);
接下来再来看下“堆”的内部细节。

2.堆内存模型





这两个图表达的意思差不多,堆内存由 年轻代和老年代组成,年轻代有分为Eden,S0(From),S1(To)
Eden: 伊甸园,万物出生的地方, 程序运行时,所有被创建的对象都在这里出生。
S0,S1: S表示Survivor幸存者, 这里存放的是当Eden空间满进过一次年轻代内存回收(YGC)所没有被回收的对象,YGC通常采用“复制算法”来进行垃圾回收,所以需要两个Survivor空间来进行倒换。
Tenured(Old): 老年代,  对象不会直接进入老年代,老年代中的对象都是在GC的时候被JVM移入的。老年代满的时候会触发Full GC,Full GC通常采用“标记-整理算法”来进行垃圾回收。这个过程会“stop-the-word”,需要扫描整个堆内存空间,所以也非常消耗系统资源。通常有以下几种情况JVM会把对象移入老年代:
    (1)、某个对象在经历了多次YGC之后还幸存。
    (2)、通常S0、S1都被设置得非常小,因为大多数的对象都是临时变量,方法执行完之后就没用了,只要少量对象会幸存。 当YGC发生时发现S0/S1存放不下幸存的对象的时候会将溢出的部分移入老年代。

3.问题定位思路和方法:

疑似内存泄露问题一般表现为两种情况:(1). 进程还没挂,但是cpu占用非常高(通常200%+)。top命令查看内存占用也已经逼近最大值 (2).进程已经挂了,生成了dump文件。
思路:

(1)、确认问题

第二种情况99%的情况都是内存耗尽导致的,直接分析dump文件,对于第一种情况要首先确认是否是内存即将耗尽的表现。
对于第一种情况首先确认cpu冲高是否是FGC导致的,用jstat -gcutil pid 1000 10 命令查看当前gc的情况②



如果FGC这一列的数值不断增大,则说明进程在不断的执行Full GC,那就说明内存即将耗尽。正常来说一次Full GC之后老年代O这一列的数值会降下去。
如果FGC这一列的数值不变,或者很久才增加一次,则说明进程的cpu冲高不是Full GC导致的,那么就要考虑其他的情况了。有时候你会发现使用top命令看出内存占用逼近最大值,但却是Full GC执行的也不频繁。这个时候就可以通过jstat -gc pid 1000 10命令来查看更加详细的内存占用,通常都是OC(老年代总空间)比较大。 PS:linux的top命令查看的是进程所占用的内存,这里可以理解为jvm从操作系统哪里要来了很多内存,但是并没有都利用起来,所以看起来内存占用很高,但是Full
GC并不频繁。





RES 为进程占用内存, 即为jvm占用内存,基本等同于堆内存占用和非堆内存占用,它的最大值=Xmx + XX:MaxPermSize
如果不是Full GC导致的cpu冲高,那么又是什么原因导致的呢? 这时可以使用jstack pid 命令查看当前进程中的所有线程都在干啥。


(2)、定位问题
如果已经确认是内存耗尽疯狂执行FullGC导致的问题,那接下来就要来定位为什么内存一直被占用着不能被释放。
如果进程还在,可以快速的查看堆中到底是什么东西,用jmap -histo:live pid > a.txt 命令来查看。ps:jmap -histo展示的是堆中所有的对象,包含垃圾和非垃圾,如果系统在不断的执行FullGC,那么就可以认为jmap输出的都是非垃圾,jmap -histo:live和jmap -histo的区别是:在快照内存之前会进行一次FullGC.



根据所列的对象,联系自己的业务来定位问题。
如果进程已经挂了,或者根据jmap的输出依然无法定位,那么还可以使用第三方工具MAT来分析dump文件。MAT本身也是基于jvm平台开发的,所以运行它需要在你的电脑上装好java。下载地址:http://www.eclipse.org/mat/downloads.php?
dump文件有两个手段获取:

启动jvm实例时增加参数:-XX:HeapDumpPath=xxxxxx,jvm进程在内存耗尽的时候会主动生成dump文件



2.手动获取dump文件: 命令:jmap -dump:file=DumpFileName.hprof,format=b pid
MAT的界面:



通常分析Leak suspects页签就可以得到问题的答案。



点击detail查看详情:



这里清晰的指出
akka.dispatch.Dispatcher$$anon$1  下的
akka.dispatch.UnboundedMailbox$MessageQueue  占用了95.49的内存。
再接着分析这个对象被谁持有着,点击MessageQueue这一行,选择List objects -》with incomming references。(with outgoing references可以查看当前这个对象里面有哪些属性)



这里展示引用MessageQueue的对象,同时也包含了除了MessageQueue之外的其他属性。然后在最上层对象点击鼠标右键选择Path To GC Roots-》exclude all phantom/week/soft etc.references



然后逐层展开,去找你认识的对象类型:


找到了“相关”的对象,再根据代码分析为什么会一直持有。
 
同时还可从Histogram页签入手分析:



这个列表和jmap命令输出的内容非常相似,只不过jmap输出的是全量的,而这个工具列出来的是无法被回收的。
这个例子中的问题正是由于我们使用akka时没有限制邮箱长度,同时消息由于其他问题无法被消费,消息不断的积压造成的内存泄露。

(3)、性能调优

如果是在家里做性能测试的时候想要实时观测jvm实例的状态来调优可以使用安装java时自带的VisualVM工具。(因为实时观测远程jvm实例需要打开jvm的相关服务,所以生产环境用不起来。)
开发虚机上就有:执行/usr/local/java/bin/jvisualvm    即可打开可视化界面(检测到我本机有7个jvm实例在运行):



VisualVM还可以添加插件来满足各种不同的需求场景。
一些参考资料:https://visualvm.java.net/zh_CN/gettingstarted.html https://www.ibm.com/developerworks/cn/java/j-lo-visualvm/  
 
注释:
①:Runtime data area (运行时数据区域)组件:Heap (堆)、 Method Area(方法区域)、Java Stack(java的栈)、Program Counter(程序计数器)、Native method stack(本地方法栈)
Heap 和Method Area是被所有线程的共享使用的;而Java stack, Program counter 和Native method stack是以线程为粒度的,每个线程独自拥有。
Heap Java程序在运行时创建的所有类实或数组都放在同一个堆中。而一个Java虚拟实例中只存在一个堆空间,因此所有线程都将共享这个堆。每一个java程序独占一个JVM实例,因而每个java程序都有它自己的堆空间,它们不会彼此干扰。但是同一java程序的多个线程都共享着同一个堆空间,就得考虑多线程访问对象(堆数据)的同步问题。 (这里可能出现的异常java.lang.OutOfMemoryError:
Java heap space)
Method Area 在Java虚拟机中,被装载的class的信息存储在Method area的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。该类型中的类(静态)变量同样也存储在方法区中。与Heap 一样,method area是多线程共享的,因此要考虑多线程访问的同步问题。比如,假设同时两个线程都企图访问一个名为Lava的类,而这个类还没有内装载入虚拟机,那么,这时应该只有一个线程去装载它,而另一个线程则只能等待。 (这里可能出现的异常java.lang.OutOfMemoryError:
PermGen full)
Java栈(Java Stack)是线程私有的,它的生命周期与线程相同。Java stack以帧为单位保存线程的运行状态。虚拟机只会直接对Java stack执行两种操作:以帧为单位的压栈或出栈。每当线程调用一个方法的时候,就对当前状态作为一个帧保存到java stack中(压栈);当一个方法调用返回时,从java stack弹出一个帧(出栈)。栈的大小是有一定的限制,这个可能出现StackOverFlow问题。
程序计数器(Program counter)是一块较小的内存空间,它的作用可以看做是当前线程所在会想的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理线程恢复等基础功能都需要依赖这个计数器来完成。 每个运行中的Java程序,每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
Native method stack 对于一个运行中的Java程序而言,它还能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,不止与此,它还可以做任何它想做的事情。比如,可以调用寄存器,或在操作系统中分配内存等。总之,本地方法具有和JVM相同的能力和权限。 (这里出现JVM无法控制的内存溢出问题native
heap OutOfMemory );
②:输出参数内容 
S0  — Heap上的 Survivor space 0 区已使用空间的百分比 

S0C:S0当前容量的大小 

S0U:S0已经使用的大小 

S1  — Heap上的 Survivor space 1 区已使用空间的百分比 

S1C:S1当前容量的大小 

S1U:S1已经使用的大小 

E   — Heap上的 Eden space 区已使用空间的百分比 

EC:Eden space当前容量的大小 

EU:Eden space已经使用的大小 

O   — Heap上的 Old space 区已使用空间的百分比 

OC:Old space当前容量的大小 

OU:Old space已经使用的大小 

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

PC:Perm space当前容量的大小 

PU:Perm space已经使用的大小 

YGC — 从应用程序启动到采样时发生 Young GC 的次数 

YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒) 

FGC — 从应用程序启动到采样时发生 Full GC 的次数 

FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒) 

GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC 
 
参考资料:
垃圾回收算法:http://www.cnblogs.com/AloneSword/p/4262255.html
jvm配置参数:http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: