四:JVM调优原理与常见异常处理方案

在jvm调优之前,我们必须先了解jvm的内存模型与GC回收机制,这些在我前面的文章里面有介绍!接下来我们通过一个案例来调整jvm性能。

一测试案例:

  1.1 编写demo

复制代码
import java.text.DecimalFormat;
/**
  -Xms 堆初始值
  -Xmx 堆最大可用值
  -Xmn 年轻代大小
  -Xss 栈的大小(栈的深度)

  -XX:+PrintGC 打印GC日志
  -XX:+PrintGCDetails 打印详细的GC日志
  -Xloggc 指定gc log的位子
  file.encoding 文件编码

       -XX:CMSInitiatingOccupancyFaction   老年代内存占用这个百分比阈值后,自动触发full gc
    -XX:+UseParallelGC 并行收集器。此配置仅对年轻代有效(无法与CMS收集器配合工作)
    -XX:+DisableExplicitGC 禁止代码中显示调用GC。即System.gc()函数
    -XX:+UseSerialGC 串行收集器
    -XX:+UseParNewGC ParNew收集器(默认和cpu核数相等,清理新生代)
    -XX:ParallelGCThreads 指定ParNew收集器线程数量,cpu数量小于8时默认相等,大于8时=3+(5*cpu_count/8)
    -XX:+UseConcMarkSweepGC 并发标记清除,即使用CMS收集器(清理老年代)
    -XX:+UseParallelOldGC 并行收集器
    -XX:+UseParallelGC 合并回收
    -XX:CMSInitiatingOccupancyFraction 设定CMS在对内存占用率达到指定的百分比的时候开始GC
    -XX:+UseCMSCompactAtFullCollection 与CMSInitiatingOccupancyFraction搭配使用默认为true
    -XX:CMSFullGCsBeforeCompaction 多少次Full GC后对内存空间进行压缩整理
    -XX:+CMSScavengeBeforeRemark 开启在CMS重新标记阶段之前先ygc一次
        -XX:+PrintTenuringDistribution    让JVM在每次MinorGC后打印出当前使用的Survivor中对象的年龄分布
        -XX:TargetSurvivorRatio   表示MinorGC结束后Survivor区域中占用空间的期望比例
        -XX:+UseCompressedOops  使用压缩对象指针
        -XX:+UseCompressedClassPointers   使用压缩类指针
        -XX:CompressedClassSpaceSize 设置Klass Metaspace的大小,默认1G
        -XX:MaxDirectMemorySize  指定DirectMemory直接内存,若未指定,则默认与Java堆最大值一样


  -XX:PretenureSizeThreshold 大对象的大小,超过这个值直接进入老年代。(一般给个1MB足以)
  -XX:MaxTenuringThreshold 对象年龄,默认15次之后较大几率放进入老年代,为0则不进入s0 s1区,直接进老年代
  -XX:SurvivorRatio 新生代中eden空间和from(s0),to(s1)空间的比例
  -XX:NewRatio 新生代与老年代的比例.默认 -XX:NewRatio=2 标识新生代1/3,老年代占2/3
  -XX:MetaspaceSize 元空间初始值
  -XX:MaxMetaspaceSize 元空间最大值
  一般生产环境都肯定会指定这两个参数
  -XX:+HeapDumpOnOutOfMemoryError:内存溢出生成快照文件
  -XX:HeapDumpPath= 存放快照路径

*/
public class JVMDemo {
    public static void main(String[] args) throws InterruptedException {
        // 最大内存
        long maxMemory = Runtime.getRuntime().maxMemory();
        System.out.println("最大堆内存为: "+format(maxMemory)+"MB");
        // 已经使用内存
        long totalMemory = Runtime.getRuntime().totalMemory();
        System.out.println("已使用堆内存: "+format(totalMemory)+"MB");
        // 当前剩余内存
        long freeMemory = Runtime.getRuntime().freeMemory();
        System.out.println("剩余堆内存为: "+format(freeMemory)+"MB");
                
        byte[] b1 = new byte[4 * 1024 * 1024];
        System.out.println("-----分配了4m堆内存-----");
        
        // 当前剩余内存
        long freeMemory2 = Runtime.getRuntime().freeMemory();
        System.out.println("剩余堆内存为: "+format(freeMemory2)+"MB");
    }
    /**
     * 将堆内存单位格式化成 MB
     */
    static private String format(long maxMemory) {
        float num = (float) maxMemory / (1024 * 1024);
        DecimalFormat df = new DecimalFormat("0.00");// 格式化小数
        String s = df.format(num);// 返回的是String类型
        return s;
    }
}
复制代码

 

   1.2 配置参数,打印jvm信息:  右键 --> Run As --> Run Configurations... --> Arguments --> VM arguments 输入配置信息  -XX:+PrintGCDetails -XX:+UseSerialGC

  

  1.3 运行java代码, 查看jvm信息

复制代码
// 这些信息与电脑配置参数有关,我们的具体数据可能不一样,但是内存模型数据比例是一样的
最大堆内存为: 1890.81MB  // 大约1900MB
已使用堆内存: 119.88MB   // 已用120MB
剩余堆内存为: 117.89MB   // 剩余117MB
-----分配了4m堆内存-----
剩余堆内存为: 113.89MB   // 用了4mb后还剩113MB
Heap
 def new generation   total 38080K, used 6805K [0x0000000085c00000, 0x0000000088550000, 0x00000000ae800000)
 // 可以看出新生代中默认 eden区 from区 to区比例为 33856:4224:4224 即 8:1:1
  eden space 33856K,  20% used [0x0000000085c00000, 0x00000000862a57a8, 0x0000000087d10000)
  from space 4224K,   0% used [0x0000000087d10000, 0x0000000087d10000, 0x0000000088130000)
  to   space 4224K,   0% used [0x0000000088130000, 0x0000000088130000, 0x0000000088550000)
 // 老年代和新生代默认比例为 84672:(33856+4224+4224)  即2:1
 tenured generation   total 84672K, used 0K [0x00000000ae800000, 0x00000000b3ab0000, 0x0000000100000000)
   the space 84672K,   0% used [0x00000000ae800000, 0x00000000ae800000, 0x00000000ae800200, 0x00000000b3ab0000)
 // 元空间
 Metaspace       used 3652K, capacity 4600K, committed 4864K, reserved 1056768K
  class space    used 410K, capacity 428K, committed 512K, reserved 1048576K
复制代码

二性能调优:

这个时候我们对jvm堆内存有了一个大致的认识,我们开始正式的性能调优。
案例:通过jmeter压力测试工具来访问一个网站,测试系统的吞吐量。
 2.1.1:双击启动 jmeter/bin/jmeter.bat 后台不要关闭
 2.1.2: 添加线程组 (用50个线程去发起5w个请求)

   

 2.1.3: 添加http请求 指定服务器  ip   port   url。 

   

 2.1.4: 添加聚合报告

  

 2.1.5:查看测试结果 (只需要关注总请求量  和  吞吐量)

  

串行收集器吞吐量调优

复制代码
【在tomcat里设置如下参数配置】
打印详细日志   最大堆和初始堆为32mb
内存溢出时生成快照文件
串行回收
非堆区初始大小32mb

-XX:+PrintGCDetails -Xmx32M -Xms32M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\app\jvmlog\app.dump -XX:+UseSerialGC -XX:PermSize=32M
(1.8以前的永久区写法)

复制代码

 

并行收集器吞吐量调优

复制代码
【在tomcat设置如下参数】
打印详细日志   最大堆和初始堆为1000mb
内存溢出时生成快照文件
并行合并回收
并行收集器线程数量为8个
非堆区初始大小500mb

-XX:+PrintGCDetails -Xmx1000M -Xms1000M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\app\jvmlog\app.dump
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=8 -XX:PermSize=500M (1.8以前的永久区写法)
复制代码

对比可以看出:第一次调优由于堆内存太小而经常触发GC,且由于是串行清理,吞吐量仅仅只有131;而第二次加大了堆内存几乎不触发GC清理,且由于是并行清理所以吞吐量提升到了2482。

调优总结:  

  1.将初始的堆大小与最大堆大小相等,来垃圾回收次数从而提高效率。 
  2.根据自身服务器性能,将堆内存最大化,以此提高吞吐量。
  3.将新生代或老年代的比例设置为2/1 或者 3/1,让GC尽量去新生代去回收。
  4.合理的使用并行收集器。数量和CPU核数相同,例如8核的CPU就设置为8,既提高了工作效率又避免了上下的频繁切换。

在windows中设置JVM参数

修改 tomcat/bin/catalina.bat 文件
set JAVA_OPTS=-Dfile.encoding=UTF-8 -server -XX:+PrintGCDetails -Xms900m -Xmx900m -XX:SurvivorRatio=8 -XX:NewRatio=2 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\app\jvmlog\app.dump -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=8 -XX:MaxMetaspaceSize=128m 

在linux中设置JVM参数

复制代码
修改 tomcat/bin/catalina.sh 文件
JAVA_OPTS="-Dfile.encoding=UTF-8 -server 
           -XX:+PrintGCDetails 
           -Xms1000m -Xmx1000m 
           -XX:SurvivorRatio=8 -XX:NewRatio=2 
           -XX:MaxMetaspaceSize=200m
           -XX:+HeapDumpOnOutOfMemoryError
       -XX:HeapDumpPath=/app/jvmlog/app.dump -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=8"
复制代码

 

生产环境定位问题

  1. 我们启动项目的时候,可以设置jvm参数使内存溢出时生成dump快照文件(事先创建好输出的目录文件夹 /log)

复制代码

#!/bin/sh
#指定项目路径
export project_path=/usr/local/wulei/demo
#指定jar包名称
export JAR_NAME=app.jar
#指定java环境变量
export JAVA_HOME=/usr/local/wulei/jdk8

#清空之前的启动日志
echo "" > $project_path/nohup.out

echo -e "\033[47;34;5m ======= 开始启动项目..... ======= \033[0m"
#启动脚本

#nohup java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/log/app.dump -Xms20m -Xmx20m -jar demo1.jar &

nohup $JAVA_HOME/bin/java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/log/app.dump -Xms20m -Xmx20m -jar $project_path/$JAR_NAME 2>&1 &

#打印启动日志
tail -f $project_path/nohup.out

复制代码

  2. 可以看到OOM异常后生成了dump文件,我们可以通过jdk的jvisualvm.exe工具来分析问题。 

  

  3. 根据stack堆栈调用轨迹,可以查看到哪一块代码在死锁,等待,修改代码。点进去查看详细信息能看到具体是  Web类的第16行的数组对象造成的。

   

====================================================================

以4核8G为例的调优模板

复制代码

堆初始值和最大值为4g
新生代3g
栈1m
元空间初始值和最大值为256m
新生代用parnew回收
老年代用cms回收,且占用率达到92自动触发full gc,并进行空间压缩
禁止代码中 System.gc()
打印内存溢出的日志

-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=/usr/local/app/oom.hprof     

复制代码

 

 

posted @   吴磊的  阅读(988)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
//生成目录索引列表
点击右上角即可分享
微信分享提示