1. jvm 监控命令参数
常用
jps 查看java进程
jstat 查看jvm gc 统计信息
jinfo 查看 修改jvm 参数
jmap 生成dump文件,查看堆信息
jstack 查看线程快照
2.oom案例 1 堆溢出
java.lang.OutOfMemoryError: Java heap space
内存溢出情况
1. 误用线程池 fixed
new FixedThreadPool(2); 核心线程数,最大线程数都为 nthread 因为是new LinkedBlockQueue() (最大值是Integer.MAX_VALUE)无尽队列,导致oom
2. 误用代缓存的线程池
使用的SynchronousQueue(), 同步队列,导致 coreThread 失效,最大线程 是Integer.MAX_VALUE 也会导致 oom
3. 动态生成的类过多
自定义的类加载器 加载动态生成的类,导致元空间不足。
导致元空间 内存不足
-XX:MaxMetaspaceSize=24m
2.1 GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常, overhead limit exceeded
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 添加参数
-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。 - dump内存,检查是否存在内存泄漏,如果没有,加大内存。
2.2 元空间溢出
2.3 线程溢出
3.调优
3.1 调整堆大小,提高服务吞吐量
增大堆空间大小。
3.2 jit优化 会 1.6 后默认开启 逃逸分析 标量替换
变量使用是否超过了作用范围
问题:堆是分配对象的唯一选择吗
大部分情况对象分配在堆中。
特殊情况,经过jit 逃逸分析
之后,一个对象并没有逃逸出方法的化,那么就可以被优化成栈上分配
但是在 栈上分配 没有真正意义上把对象放在栈里面,而是把这个聚合量(对象)进行分解,分解成具体的标量。
void alloc(){
//逃逸分析
Ponit p=new Point(1,2);
sout(p.x,p.y);
}
class Point{
int x;
int y;
}
// 经过分析后,发生标量替换。,此时方法的point 已经不算对象了。
void alloc(){
int x=1;
int y=2;
sout(x,y);
}
半解释半编译
java 是半解释,半编译的语言。
在一开始执行时,解释器不用热机, 直接逐行解释,提高响应。后期对热点代码
进行编译,缓存,提升整体性能。
3.2.1 栈上分配
通过逃逸分析,变量没有发生逃逸,对这个对象进行栈上分配。
3.2.2 锁消除
通过分析,一个被发现只能从一个线程被访问,那么对于这个对象可以不考虑同步,
3.2.3 标量替换
一个对象时聚合量,如果没有发生逃逸分析,进行拆解成为 标量(对象里面的字段属性)
3.3 合理配置堆内存
内存太大,full gc 时间长
内存太小 频繁触发minor gc
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。
方法区(永久代 PermSize和MaxPermSize 或 元空间 MetaspaceSize 和 MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。
年轻代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍。
3.3 如何计算老年代存活对象大小
查看 gc日志
JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)。
3.3.4 gc频率
比如从数据库获取一条数据占用128个字节,需要获取1000条数据,那么一次读取到内存的大小就是(128 B/1024 Kb/1024M)* 1000 = 0.122M ,那么我们程序可能需要并发读取,比如每秒读取100次,那么内存占用就是0.122100 = 12.2M ,如果堆内存设置1个G,那么年轻代大小大约就是333M,那么333M80% / 12.2M =21.84s ,也就是说我们的程序几乎每分钟进行两到三次youngGC。这样可以让我们对系统有一个大致的估算。
0.122M * 100 = 12.2M /秒 —Eden区
1024M * 1/3 * 80% = 273M
273 / 12.2M = 22.38s —> YGC 每分钟2-3次YGC
3.4 新生代 老年代比例 自适应策略 useAdaptiveSizePolicy
1.8 默认 useParrelGC 该gc回收器默认开启useAdaptivepolicy,那么会根据gc 的情况自动计算eden from to 区的大小。 默认 6:1:1
注意事项:
1、在 JDK 1.8 中,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false;不过不同版本的JDK存在差异;
2、UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数失效;
3、由于UseAdaptiveSizePolicy会动态调整 Eden、Survivor 的大小,有些情况存在Survivor 被自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉 Eden区后,还存活的对象进入Survivor 装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发FULL GC,如果一次FULL GC的耗时很长(比如到达几百毫秒),那么在要求高响应的系统就是不可取的。
附:对于面向外部的大流量、低延迟系统,不建议启用此参数,建议关闭该参数。
3.5 cpu占用高 如何排查
- top 查看cpu过高的 进程id 比如 pid 1456
top ->得到cpu高的进程id 1456 - top -Hp pid可以查看某个进程的线程信息 进程id
-H 显示线程信息,-p指定pid
top -Hp 1456
得到 线程id
- printf %x 将线程id十进制转十六进制
printf %x 1465 0x5b9 - jstack -pid | grep -A 30 过滤出线程id锁关联的栈信息 需要16进制
jtstack -1456 | grep -A 30 0x5b9
3.6 G1并发 执行的线程数对性能的影响
-XX: ConcGcThreads 设置并发标记的线程数。n=ParallelGCThreads 的1/4左右。
配置完线程数之后,我们的·请求的平均响应时间和GC时间都有一个明显的减少了
,仅从效果上来看,我们这次的优化是有一定效果的。大家在工作中对于线上项目进行优化的时候,可以考虑到这方面的优化。
4 享学 gc 调优
4.0 性能gc 监控 jstat
jstat - gc 5000 20
5000ms 统计一次 20次
jstat-gc 8404 5000 20 | awk ‘{print $13,$14,$15,$16,$17}’
awk 打印 13列…
4.1 查询初始化参数
java -XX:PrintFlagsFinal -version | grep HeapSize
jmap -heap jmap -histo 1980 | grep 20
jmap –histo 1196 | head -20
(这样只会显示排名前 20 的数据)
4.2 调优参数
在高并发场景下,对象是朝生夕死,增加新生代内存,同时老年代够用就行。
-Xms1500m -Xmx1500m -Xmn1000m -XX:SurvivorRatio=8
增加新生代内存。
java -jar -Xms1500m -Xmx1500m -Xmn1000m -XX:SurvivorRatio=8 jvm-1.0-SNAPSHOT.jar
分析结果
500m->1500m
eden 180 -》500 没有明显增加。 每次检索gc对象的空间变大,所以gc 时间增加
YGC 消耗时间 1。扫描垃圾对象(耗时) 2 复制存活对象
方案二
很大的新生代,较小的老年代。
当前场景生命周期的短
推荐策略
- 新生代大小选择
- 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,新生代收集发生的频率也是最小的.同时,减少到达老年代的对象.
- 吞吐量优先的应用 尽可能的设置大,可能到达 Gbit 的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用.
- 避免设置过小.当新生代设置过小时会导致:
- 1.MinorGC 次数更加频繁
- 2.可能导致 MinorGC 对象直接进入老年代,如果此时老年代满了,会触发 FullGC.
- 老年代大小选择
响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;
如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:并发垃圾收集信息、持久代并发收集次数、传统 GC 信息、花在新生代和老年代回收上的时间比例。
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的新生代和一个较小的老年代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。
gc 调优策略
1. 降低 Minor GC 频率 增大新生代空间
由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。 单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。
情况 1:假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,因为这个对象存活时间>间隔时间,那么正常情况下,MinorGC 的时间为 :T1+T2。
情况 2:当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:即 T12(空间大了)+T20
可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。
在 JVM 中,复制对象的成本要远高于扫描成本
。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden区的大小。
这个就解释了之前的内存调整方案中,方案一为什么性能还差些,但是到了方案二话,性能就有明显的上升。
4.3 JVM 性能调优之预估调优与问题排查
动态年龄判断
s区 相同年龄所有对象的大小超过 s区的一半,直接晋级到old 区。
秒杀 大促场景频繁 full gc
full gc 时间长, 新生代 老年代 方法区
useAdaptiveSizePolicy
启用自适应策略
会自适应新生代 eden 和 s0的比例
4.4 mat 分析使用
使用Eclipse Memory Analyzer Tool(MAT)分析线上故障(一)
incoming outgoing reference
- with incoming references 对象被谁引用了
C 的引入 c1对象 c2对象 c的class 对象
也就是c 对象被谁引用了
- with outgoing references 对象 引用了谁
-
shallow retained heap
浅堆
对象自生占用的内存深堆
统计的结果 自身对象被gc,释放的内存大小。
改变
支配树试图 上到下找
堆中最大的对象
只能显示 支配的对象。 所有执行b的对象都经过a 那么称为 a 支配b
线程视图
path to gc roots
c被b 引用 ,b被a 引用
根据大对象 定位代码
JAVA内存泄露使用MAT(Memory Analyzer Tool)快速定位代码
thread local 引用图
thread 里面有个threadlocal map ,map 的key 是 弱引用 threadlocal
4.5 内存泄漏
内存泄漏指:不使用的对象,但是存在引用,使得jvm误以为还在使用。无法回收掉。
内存溢出 : 申请内存,没有足够的内存可以使用了。
1. 静态集合类
2. 单例对象
内存中 单例对象一直存在
3. 内部类持有外部类
外部类A 内部类B
内部类B 被C对象 引用,但是外部类A对象 不再使用,由于C引用B 导致A也回收不了。
4. 各种连接、IO 没有释放资源
没有关闭流
5. 变量不合理的作用域
一个变量的作用范围大于其使用范围,很有可能会造成内存泄漏。
6. hash 值的改变
存入hashmap 中,放入之后,又修改字段,那么hash值改变,存在内存泄漏。
7. 缓存泄漏
8. 监听器 和回调
在 api 中注册回调,却没有显示的取消,那么就会聚集导致内存泄漏。