在一些高并发的程序,或者一些大量使用内存来进行计算的程序,有时候经常会遇到一些这样的问题:程序刚开始运行挺快的,后来就运行缓慢下来了,甚至于到了必定时间还会出现OOM或者StackOverFlow等错误。要理解这些错误产生的根源,就要了解JVM是何如划分、管理、回收内存的,本篇博客将从博主对JVM的认识以及实际经验角度出发,聊聊这些话题。算法
JVM内存结构安全
一旦涉足JVM内存结构,恐怕会冒出大量的术语:新生代?老生代?永久代?等等,咱们暂且抛开这些名称,基于咱们的JAVA基础,它应该是什么样子的呢?多线程
第一:程序是多线程的(单线程不过是多线程的一个极端而已),要知道CPU在多个线程之间来回切换,是保留有上下文信息的,那么这些信息是应该被存储的。说的简单点,至少每一个线程都应该知道,若是CPU要执行本身,那么应该从哪里开始呢?所以JVM内存结构中,应该要有这样的区域A,并且这个A区域应该是每一个线程的专属区域。并发
第二:不管是咱们编写的代码,仍是利用的第三方的工具,都是须要类装载器进行加载的,这说明应该有区域B专门用于存储这些信息,好比编译后的类,方法,常量池等。咱们是否曾经遇到Server启动时,会抛出OOM的错误呢?可能就是由于Server启动内存较小,而须要Load Class太多致使的。这块区域B应该对每一个线程进行开放共享。ide
第三:从学习JAVA开始,咱们就知道了内存有堆和栈的概念,并且咱们都说new 出来的对象是存放在堆中的,要知道JAVA是面向对象的,因此说这一块应该是占用空间较大的一块,也是内存管理、回收的一个核心点。堆,也是每一个线程均可以来访问的,若是堆的空间不足了,却仍需为对象分配空间的话,就会OOM了。高并发
第四:既然上面说到了堆,那么下面就得说下栈的概念了。在多线程中,咱们但愿造成栈封闭,来达到线程安全的目的,好比ThreadLocal,使用局部变量代替成员变量等。以上其实说明,栈空间是每一个线程所私有的,栈中存放的是方法中的局部变量,方法入口信息等。每一次调用方法,都涉及到入栈和出栈,若是有一个递归方法调用了几千次,甚至几万次,那么可能会由于在栈中积压了那么多信息致使栈溢出的,从而抛出StackOverFlow。工具
综合以上,其实,咱们就容易获得下面的:性能
程序计数器,就是上面所说的A区域;方法区就是B区域。学习
根据上面的讨论,程序计数器和栈跟随线程的生命周期,而堆和方法区是由JVM的GC机制所管理的,那么下面咱们来讨论JVM中的分代垃圾回收机制。spa
分代垃圾回收机制
JVM把堆细分了2个代:新生代、老生代(老年代);而方法区叫作永久代。先来看一个图:
新生代,即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参数设置
-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命令进程呢?那么究竟是什么个状况呢?咱们先来看看:
top的-H选项其实已经很明白指出:会打印出此进程下面的全部线程信息。也就是说,一个程序,启动了多个线程,在LINUX下一个线程将会对应一个进程!
此时,咱们能够利用jstack进一步分析线程的DUMP文件:
咱们能够垂手可得的获取,这些线程的名称、优先级、TID(JAVA中线程的惟一标示)、NID(将这个16进制的数字转成10进制那么就和top/ps中的PID对应上了),运行状态信息等。
更进一步,咱们还能够利用jstat来分析内存使用情况:
【关于jstat彻底能够利用man jstat来获取帮助信息】
经过jstat命令,咱们将一目了然的清楚程序新生代,老生代,永久代的占比,各类GC的次数以及耗时等信息了。