说到JVM的参数调休,一定得知道JVM的内存结构,理解JVM内存结构的参数调优都是耍流氓,参数调优至少要知道调的是什么,优化的是什么。
JVM内存和物理机内存其实是有很多对应关系的,JVM的内存也受到物理机内存的约束,毕竟JVM内存是基于物理内存虚拟出来的东西,大体来说,
JVM内存分为heap区、stack区、method区、程序计数器,再细分一点的话heap区又分为新代和老代,栈区又分为java栈和本地方法调用栈,但是这里要注意的是JVM规范中并没有对本地方法栈实现方法和数据结构做强制规定,虚拟机的实现者可以自由实现它,在HotSpot虚拟机中直接就给二者合二为一了。内存结构示意图大致如下:
(图片来源于网络:云时代架构公众号)
程序计数器/栈
对于程序计数器区,主要是为了多线程情况下在CPU进行时间片轮转执行之后能回到之前线程的执行地方继续执行,而执行依赖的执行现场由java 栈来保存,java里的栈是每个线程所私有的,没有并发同步问题。也因为程序计数器区并不会因为程序的执行而变大所以这个区域是唯一一个不会发生OOM的区域;前面我们说到,java线程在执行的时候会记录当前的执行现场以便于后面恢复执行现场,典型的就是递归调用,我们都知道递归调用其实就是方法的调用逻辑从上到下而方法的返回逻辑从下到上,每计算一层调用的时候需要依赖下层结果,这个下层结果存储在哪里?就是栈了,这也就是为什么有时候在代码里用到递归的时候,如果运用不合理比如出现了死循环或者递归终止条件设置有问题导致程序一直递归执行下去的时候会报stackoverflow的原因。
方法区
方法区存储的包括静态变量、常量池(编译期生成的字面量和常量)、方法信息、名称信息、字段信息、以及编译后的代码信息,方法区是全局的,换句话说方法区存储的东西是所有线程共享的,也存在线程安全问题,必要的时候需要采取一定得同步手段;那么前面说到了方法区存储的信息包括哪些,由此可以看出方法区存储的东西并不是固定的而是和我们的程序设计有关,所以在项目里类太多、静态变量太多的时候可能出现OOM,错误就是java.lang.OutOfMemoryError: PermGen,当然这个是在java8之前的情况,在java8里去掉了Perm区改成了Metadata区,最大的区别是Perm区是java虚拟内存的一部分,大小受到分配给java内存大小限制,而Metadata区大部分class元数据存储在本地内存,从某种意义上来说更不容易发生java.lang.OutOfMemoryError: PermGen这个类似错误,但是并不是说就不会出现oom了,比如若我们设置了-XX:MaxMetaspaceSize参数或者没设置这个参数但是加载的类多到耗尽了物理机的内存,这个时候也会出现OOM,所以作为开发在并不能说因为去掉了Perm就万事大吉。
堆区
总的来说堆区存储的是类对象和数组对象以及作为类成员变量申明的基本数据类型,堆区也是共享的也会有线程安全问题,同时也是GC的工作区域,通常说得java内存管理指的就是java的堆内存管理,参数调优大部分也是针对堆内存的参数调优。
对java的各个区域做了介绍之后,下面重点介绍下各个jvm参数的意义以及使用场景:
-Xmx JAVA堆的最大值,默认为物理内存的1/4
-Xms JAVA堆内存的初始,一般最好和-Xmx一样大,避免在程序运行过程中动态的申请调整堆内存
-Xss 每个线程的stack大小,相信有了对前面stack的介绍这个会很好理解
-Xmn JAVA HEAP的young区大小,前面说了堆分为yong区和old区
-XX:MetaspaceSize class metadata的初始空间配额,以bytes为单位,达到该值就会触发GC进行类型卸载,同时GC会对该值进行调整
-XX:MaxMetaspaceSize 指定分配给class metadata的最大内存大小,默认上限是物理内存,超过此值就会触发FullGC
MaxHeapFreeRatio GC后收缩堆内存预估最大值的空闲内存百分比,默认100
MinHeapFreeRatio GC后放大堆内存预估最大值得空闲内存百分比,默认0
MaxHeapSize 堆内存的大小上限,即-Xmx
NewSize 新生代预估内存占用的默认值
MaxNewSize 新生代内存占用的最大值
OldSize 老年代默认大小
NewRatio 老年代对比新生代的大小,比如2代表老年代空间是新生代空间的两倍大小
SurvivorRatio Eden/Survivor,8表示Survivor:Eden为1:8,因为Survivor有两个,所以Eden的占比为8/10
CompressedClassSpaceSize 类指针压缩空间大小,默认1G,在64位平台上指针压缩默认是打开的
常见JVM内存管理/查看指令:
(1)jstat -gcutil pid
最后的参数1000是让1秒刷新一次,我们看内存回收只看一条数据其实是看不出来什么的,往往需要观察一段时间的垃圾回收才能有所了然,下面对各个字段做一个简单的说明:
S0:Survivor 0区的空间使用率
S1:Survivor 1区的空间使用率
E:Eden区的空间使用率
O:老年代的空间使用率
M:元数据区的空间使用率
CCS:类指针压缩空间使用率
YGC:新生代GC次数
YGCT:新生代GC总时长
FGC:FullGC总次数
FGCT:FullGC总时长
GCT:总共的GC时长
(2)查看jvm参数设置以及内存使用情况:jmap -heap pid
可以看出这个命令给出的信息还是比较全的,从结构上分为几个部分:HeapConfiguration、Heap Usage,其中Heap Useage又分为PS Young Generation和PS Old Generation,PS Young Generation又分为Eden Space\From Space\To space,所以可以看出这个命令也主要是围绕GC操纵空间堆来的,包括堆的基本配置以及堆的使用情况,这个命令其实很实用,上面就是配置下面就是实用情况,当堆内存不足的时候可以很直观的看出哪个参数设置得不合理,如果看堆设置没看出有什么问题,那么再结合jstat -gcutil pid看看垃圾回收情况,差不多就能看出大概是什么原因导致堆内存溢出了。
(3)jinfo 可以输出并修改运行时的java进程信息(内容太多只截图部分,更多信息请自行执行查看)
从上图可以看出,这个命令给出的信息很多很全面,包括虚拟机基本信息、java运行时环境变量、甚至包含操作系统内核版本信息等,命令结果翻到最下面还有默认的jvm参数信息,就是我们没有设置但有默认值的jvm参数,总之如果要系统全面的看java运行相关的参数,这个命令很合适。
(4)jps 与unix上的ps类似,用以显示本地的java进程信息,可以看出本地运行着几个java程序,并显示他们的进程号
从执行结果看,我因为本地跑着一个tomcat所以有一个Bootstrap的进程,然后我执行了三遍jps每次执行完之后都会有一个名字叫jps的临时进程,可以看出jps本身也是一个java进程,只是这个进程执行完自己的功能之后就结束了生命,下次再次执行的时候就启动了一个新的java进程
(5)jstat -class pid 显示加载的class数量以及所占空间信息
可以看出我本地的虚拟机cassloader一共加载了2692个类,一共占用了2791.6Bytes,未加载类个数为0,加载这么多类一共耗时4.29秒
(6)jstat -compiler pid 显示VM实时编译的数量等信息
(7)jstat -gc pid 可以显示GC的信息,查看GC的次数以及时间
可以看出这个命令和我们前面说得jstat -gcutil pid命令还是有所区别的,主要区别在于关于s0 s1 e o m 区的使用描述上,
S0C 年轻代中第一个survivor(幸存区)的容量(字节)
S1C 。。。第二个。。。
S0U 年轻代第一个Survivor(幸存区)的目前已使用空间
S1U 。。。第二个。。。
EC 年轻代中Eden(伊甸园)的容量(字节)
EU 年轻代Eden(伊甸园)目前已使用量
。。。后面的O区M区一样的,YGC,YCGT,FGC,FGCT,GCT和jstat -gcutil一样的
(8)jstat -gcnew pid new对象的信息
S0C S1C S0U S1U EC EU YGC YGCT和jstat -gc pid一样的,
TT ???
MIT ???
DSS ???
(9)jstat -gcnewcapacity pid new对象的信息以及占用容量
(10)jstat -gcold pid old对象的信息
可jstat -gcnew对应, 只不过一个是着重在new区一个着重在old区
(11)jstat -gcoldcapacity pid 和new的一样
(12)jstat -gcpermcapacity pid perm对象信息以及其占用量,不过从java8中以及移除了perm区
由于我的电脑是安装的java8 所以这个命令提示找不到了
(13)jstat -printcompilation pid 当前VM执行的信息
(14)jstat -gcutil pid 1000 10 1000ms统计一次gc情况,统计10次
(15)jstack 查看jvm的线程运行情况,是否有死锁现象等信息
(16)jmap pid打印出某个java进程内存内的所有对象的情况,如:产生了哪些对象以及数量
(17)jmap -dump:format=b,file=heap.bin pid
format 格式,这里b是二进制的意思
file 保存路径及文件名
pid 进程号
这个命令会在当前路径生成一个名为heap.bin的二进制内存dump文件,这种文件有专门的分析工具如jvirsual Vm ,mat等,如用jvirsual vm打开刚刚的dump文件之后截图大概是这样的:
从这个tab可以看到dump时候得堆基本信息,当然还有其他很多用于排查oom的信息。
(18)jmap -histo:live pid|less 堆中活动对象以及其大小
从上面的信息可以看出排在前三位的分别是java.lang.String、java.lang.reflect.Method以及java.util.HashMap$Node,我测试的机器只启动了tomcat,并没有部署任何应用,由此我们可以得出的结论是String对象加载很多,反射用得也很多,Map也很多
(19)jconsole 一个java GUI监视工具,可以图表化的形式显示各种数据,并可通过远程连接监视远程的服务器VM
当然,上面列出的这些只是一些可以用的工具,至于在具体的工程环境中,当出现OOM之后怎么选择工具快速的定位出OOM发生的原因以及解决问题才是最重要的,这需要一定得经验积累,后面会在这篇博文里面添加具体的问题定位实例,毕竟工作几年的“老油条”怎么也是遇见过OOM的。
暂完。