1、Java虚拟机可能带来的问题
先列举一下大家在日常工作可能已经遇到的问题:
- 正在运行的 Java 进程,可能突然就 OOM 内存溢出了
- 线上系统产生卡顿,CPU 疯狂运转,GC 时间飙升,严重影响了服务响应时间
- 面对一堆 JVM 的参数无从下手,错失了性能提升的可能,或者因为某个参数的错误配置,产生了尴尬的负面效果
- 想要了解线上应用的垃圾回收状况,却不知从何开始,服务监控状况无法掌控
- JVM出现fullGC很频繁,怎么去线上排期问题?
- 类加载为什么要使用双亲委派模式,有没有什么场景打破了这个模式?
2、实战记录
示例:
package com.xes.jvm.gc;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
*
*/
public class GCCase {
private static class CardInfo {
BigDecimal price = new BigDecimal(0.0);
String name = "张三";
int age = 5;
Date birthdate = new Date();
public void m() {}
}
private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
new ThreadPoolExecutor.DiscardOldestPolicy());
public static void main(String[] args) throws Exception {
executor.setMaximumPoolSize(50);
for (;;){
modelFit();
Thread.sleep(100);
}
}
private static void modelFit(){
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info -> {
// do something
executor.scheduleWithFixedDelay(() -> {
//do sth with info
info.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
private static List<CardInfo> getAllCardInfo(){
List<CardInfo> taskList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
CardInfo ci = new CardInfo();
taskList.add(ci);
}
return taskList;
}
}
在 Linux 服务跑起来
java -cp jvm-optimization-1.0-SNAPSHOT.jar -Xms200M -Xmx200M -
XX:+PrintGC com.xes.jvm.gc.GCCase -> catalina.out &
常见配置汇总
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
CPU 占用过高排查思路
先通过 top 命令找到消耗 cpu 很高的进程 id 假设是 26246
top 命令是我们在 Linux 下最常用的命令之一,它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息。
通过top -p PID的方式,查看指定进程的消耗情况,再使用 -H的命令,找到消耗资源过高的线程信息
定位到消耗较高的线程是26248,转换十六进制后是0x6688,通过jstack命名继续查看java进程的堆栈信息,发现消耗大量资源的线程是VM Thread
继续通过jstat -gc 26246命令,发现FGC的频次非常的高,最终定位到是因为FULL GC导致服务器CPU资源被耗尽
3、常见问题分析
超大对象
代码中创建了很多大对象 , 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OOM
超过预期访问量
通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
比如如果一个系统高峰期的内存需求需要 2 个 G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至OOM过多使用 Finalizer
过度使用终结器(Finalizer),对象没有立即被 GC,Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的 CPU 时间较少,因此它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出 OutOfMemoryError 异常。内存泄漏
1.大量对象引用没有释放,JVM 无法对其自动回收。
2.长生命周期的对象持有短生命周期对象的引用
例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
3.连接未关闭
如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
package com.xes.jvm.gc;
public class GCCase2 implements Runnable{
public int flag = 1;
static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
GCCase2 td1 = new GCCase2();
GCCase2 td2 = new GCCase2();
td1.flag = 1;
td2.flag = 0;
new Thread(td1).start();
new Thread(td2).start();
}
}
通过jps 或是 top命令,找到对应的java进程,查看线程信息时,发现状态都是sleep
再根据jstack -l 27370,发现提示发现死锁,分析死锁提示发现在GCCase2的18行和30行都在等待锁释放。
至此已定位到死锁的位置,后面就可以进一步优化了!
`