在一些高并发的程序,或者一些大量使用内存来进行计算的程序,有时候常常会遇到一些这样的问题:程序刚开始运行挺快的,后来就运行缓慢下来了,甚至于到了一定时间还会出现OOM或者StackOverFlow等错误。要理解这些错误产生的根源,就要了解JVM是何如划分、管理、回收内存的,本篇博客将从博主对JVM的认识以及实际经验角度出发,聊聊这些话题。


JVM内存结构

一旦涉足JVM内存结构,恐怕会冒出大量的术语:新生代?老生代?永久代?等等,我们暂且抛开这些名称,基于我们的JAVA基础,它应该是什么样子的呢?


第一:程序是多线程的(单线程不过是多线程的一个极端而已),要知道CPU在多个线程之间来回切换,是保留有上下文信息的,那么这些信息是应该被存储的。说的简单点,至少每个线程都应该知道,如果CPU要执行自己,那么应该从哪里开始呢?因此JVM内存结构中,应该要有这样的区域A,而且这个A区域应该是每个线程的专属区域。


第二:无论是我们编写的代码,还是利用的第三方的工具,都是需要类装载器进行加载的,这说明应该有区域B专门用于存储这些信息,比如编译后的类,方法,常量池等。我们是否曾经遇到Server启动时,会抛出OOM的错误呢?可能就是因为Server启动内存较小,而需要Load Class太多导致的。这块区域B应该对每个线程进行开放共享。


第三:从学习JAVA开始,我们就知道了内存有堆和栈的概念,而且我们都说new 出来的对象是存放在堆中的,要知道JAVA是面向对象的,所以说这一块应该是占用空间较大的一块,也是内存管理、回收的一个核心点。堆,也是每个线程都可以来访问的,如果堆的空间不足了,却仍需为对象分配空间的话,就会OOM了。


第四:既然上面说到了堆,那么下面就得说下栈的概念了。在多线程中,我们希望形成栈封闭,来达到线程安全的目的,比如ThreadLocal,使用局部变量代替成员变量等。以上其实说明,栈空间是每个线程所私有的,栈中存放的是方法中的局部变量,方法入口信息等。每一次调用方法,都涉及到入栈和出栈,如果有一个递归方法调用了几千次,甚至几万次,那么可能会因为在栈中积压了那么多信息导致栈溢出的,从而抛出StackOverFlow。


综合以上,其实,我们就容易得到下面的:


wKioL1cKEliwEfW2AAArv8UmPoc364.png


程序计数器,就是上面所说的A区域;方法区就是B区域。

根据上面的讨论,程序计数器和栈跟随线程的生命周期,而堆和方法区是由JVM的GC机制所管理的,那么下面我们来讨论JVM中的分代垃圾回收机制。



分代垃圾回收机制

JVM把堆细分了2个代:新生代、老生代(老年代);而方法区叫做永久代。先来看一个图:

wKiom1cKGVDDi0mpAAApSBi2-GU657.png


新生代,即young genration,分为3个部分,一个Eden,2个survior。所谓Eden,即伊甸园,顾名思义,其实就是新的生命,快乐;而survior表示幸存者。如果创建对象,会优先在Eden中分配,如果Eden不足了,会进行一次Minor GC,将Eden中可以清空的清理掉,如果不能回收的,让它进入一个survior区域,这样幸存者的概念就出来了,其实本质上是复制回收算法。那么为什么要搞2个survior呢?是因为如果Eden+1个survior进行Minor GC的时候,另一个survior就发挥作用了,显然2个survior中必然有一个是空闲的。那么我们应该让Eden的空间大些,不然进行频繁的Minor GC也会消耗资源的。


老生代,即old genration/tenured genration。如果young genration区域的空间不足了,发生了多次Minor GC的话,那么会把young genration中的一部分对象COPY TO 老生代区域。其实这里涉及到一个对象年龄的问题。当old genration区域也不足时,就会进行Full GC。要知道Full GC,是非常耗时的,如果程序在执行的过程中,JVM进行Full GC的话,就会严重影响性能,导致程序执行时间大大增加了。


永久代,即permant generation。一般不参与垃圾回收的。


通过上面的分析,我们已经发现,新生代、老生代、永久代的空间大小其实在一定程度上可能会影响我们的程序执行效果,因为他们的大小会影响JVM GC,因此我们应该关注这些参数的设置。



JVM参数设置


wKiom1cKH2rjxQjeAAB3_fr17HM539.png


-Xms : 堆的初始化大小

-Xmx : 堆的最大大小

-Xmn : young geration的大小

-Xss : 栈大小

-XX:PermSize : 永久代初始大小

-XX:MaxPermSize : 永久代最大大小

......



LINUX下如何监控程序的JVM状况?


在Linux下,要想监控程序的JVM内存使用,比如Eden,S0,S1,YGC,FULL GC等的情况的话,该怎么做呢?


首先来说,我们要找到程序在LINUX中运行的PID,很简单,我们可以通过top 或者 ps来查找。在这里,我们想想,如果是多线程的程序,比如有10个线程启动的话,是否在top或者ps中会有10个这样的JAVA命令进程呢?那么到底是什么个情况呢?我们先来看看:


wKiom1cKJGTguweYAABzjEJsN_E574.png


top的-H选项其实已经很明白指出:会打印出此进程下面的所有线程信息。也就是说,一个程序,启动了多个线程,在LINUX下一个线程将会对应一个进程!


此时,我们可以利用jstack进一步分析线程的DUMP文件:


wKioL1cKJhiDetsKAACWujw9FVA711.png


我们可以轻而易举的获取,这些线程的名称、优先级、TID(JAVA中线程的唯一标示)、NID(将这个16进制的数字转成10进制那么就和top/ps中的PID对应上了),运行状态信息等。


更进一步,我们还可以利用jstat来分析内存使用状况:


wKioL1cKTgqCZZqkAAAgVUm-7mg971.png

【关于jstat完全可以利用man jstat来获取帮助信息】

通过jstat命令,我们将一目了然的清楚程序新生代,老生代,永久代的占比,各种GC的次数以及耗时等信息了。