一.背景
最近公司官网隔段时间出现个别人访问处于加载白页的情况,判断可能nginx负载(用的ip_hash方法)upstream下的负载点出现了问题,在逐个访问时查出有个负载点有问题,该负载点tomcat是部署在一台windows下,tomcat日志报的是“java.lang.OutOfMemoryError: Java heap space”,java堆内存溢出属于jvm最大堆内存-Xmx不够出现的当时设置的是-Xmx2048m,正常情况下2g的够用了,用jmap -heap pid看了下堆内存的使用情况发现-Xmx的值竟不是2048m。在catalina文件有设置参数却没生效,百度才知window服务器tomcat加系统服务中时启动参数要在注册表中配置,最后在注册表中加了参数重启后就再没出现过问题。对此想把JVM的GC工作机制及性能优化记录下。
二.JVM的GC工作机制
1.JVM的结构大致分这几块,上图是我从网上拿的感觉画的不错很清晰。
a.类加载器:在JVM启动时或者在类运行时将需要的class加载到JVM中;
b.执行引擎:负责执行class文件中包含的字节码指令;
c.内存区域:是在JVM运行的时候操作所分配的内存区;
d.本地方法接口:主要是调用C或C++实现的本地方法及返回结果。
2.内存区域可以按如下将其分为两队
a.虚拟机栈:虚拟机栈描述的是Java 方法执行的内存模型,用于执行java方法,每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
b.本地方法栈:用于执行本地方法,和虚拟机栈一样。本地方法和java方法的关系就是java方法通过调用本地方法(第三方dll库)来访问操作系统的底层资源。
c.程序计数器:是一块较小的内存空间,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
d.堆:JAVA虚拟机管理的内存中最大的一块,所有线程共享,几乎所有的对象实例和数组都在这类分配内存,GC主要就是在JAVA堆中进行的,所以GC的性能优化经常在这块内存上处理。
e.方法区:又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
在java中最普通的程序行为也会涉及到java栈,java堆,方法区这三个重要的内存区域
如一个对象的创建:Object obj = new object();
Object obj 会反映到java栈的本地变量表中,作为reference类型数据存在,new object()这部分会反映到java堆中存储所有object类型实例数据值,在java堆中还包含能找到对象类型数据(对象类型,实现的接口,父类,方法等)的地址信息,这些类型数据存储在方法区。
3.GC回收算法
GC回收算法看了很多博客写的大致分为以下四类算法而且有很清晰的图解
a.标记清除算法
先标记要回收的对象,然后再清理,这种算法会产生大量的不连续的内存碎片,new一个大的内存时就没有足够的连续内存空间可分配,而且这种方法遍历GC Roots效率比较低,在进行GC的时候需要停止应用程序。
回收前:
存活 |
回收 |
|
|
可用 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回收后:
存活 |
可用 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
b.复制算法
将内存分为两块,只使用一块保留另一块,当使用的这块内存用完会进行一次内存清理,将仍存活的对象复制到另一块内存上,这种算法能避免大量内存碎片但是需要正常情况的双倍内存。
回收前:
存活 |
回收 |
|
|
可用 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回收后:
|
|
|
|
存活 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
c.标记整理算法
先标记要回收的对象,将存活对象移至一端,最后清理端边界以外的内存,这种方法避免了标记清除算法的大量连续碎片内存的产生,也避免了复制算法的内存减半的问题,但是还是有标记遍历效率不高的缺点
回收前:
存活 |
回收 |
|
|
可用 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回收后:
存活 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可用 |
|
|
|
|
|
|
|
d.分代收集算法
分代收集算法是上面三种算法的集合,根据对象的生命周期的不同将内存划分为几块然后根据各块的特点采用最适当的收集算法。
4.java堆内存管理
JVM堆内存分为2块:Permanent Space 和 Heap Space。
Permanent = 持久代(Permanent Generation),主要存放的是Java类定义信息,与垃圾收集器要收集的Java对象关系不大。
Heap = new( 新生代)+ old (老年代),new = eden 区+Survivor区,其中Survivor区有两个块(from+to),下面这张图能充分的说明堆内存。
创建一个对象时java的内存分配过程如下:
三.性能问题的分析与定位
首先可以通过jmap -heap pid 查看当前Java程序堆内存的使用情况,视情况调整各代内存参数。
1.cpu过高时通过top命令查出占用cpu过高的pid
2.top -Hp pid 查出占用cpu过高的线程ip
3.printf "%x\n" ip,将线程ip转化为十六进制
4.通过jstack pid |grep 16进制线程ip 分析问题
5.通过jstat -gc pid [间隔时间/毫秒] [查询次数]查看GC回收情况