为什么要调优 ?

最近几天在和朋友一起玩我的世界服务器,服务器是我之前购买的用于部署项目的2c2g的阿里云,闲置无用,便拿来当作服务器,在部署期间遇到过很大问题,但因为我是Java选手,部署方面的报错,调用Jar包的参数等还是能看明白,倒也算不上问题。

真正的问题在于正式游玩的时候,虽然服务器标称是2c2g,但通过SSH连接后显示实际的物理内存只有1.7g,阿里云服务器自带的服务器监控进程和系统本身还需要占用一定内存,实际可用的内存仅有1.5g左右,在游玩期间多次遭遇服务器卡死但没崩溃,服务器直接崩溃等等场景。

作为一个Java选手,平时其实就在烦劳,如何将所学知识用于实际,这一次的问题很明显,Minecraft Server是一个Java进程,总结下来就是,如何最大化的利用内存空间,让Java进程运行的更加稳定,使用同样的内存,得到更高的性能等等。

调优调优,调的是什么,优化的是什么?

说到JVM,这里可以顺便思考思考,JVM的作用?总结下来,无非以下几点

  • 将编译好的字节码解释为机器码
  • 透明化内存的管理,自动的分配、回收内存
  • 为Java程序提供运行环境,加载各种类
  • 屏蔽平台的差异,实现一次编译,到处运行
  • 。。。

很明显,解释的过程是一种强规范,没法调整。加载的过程也是一种规范,或许可以尝试减少一些不必要的类的加载,或者调整类加载的顺序,或者只在使用类的时候去加载,而不使用的时候就直接卸载这个类,当然,我目前还没有这个能力去做这方面的调优,这方面会涉及到对Minecraft依赖的深入了解,我认为应该由Minecraft的厂商去调优。编译方面的优化,我只知道编译代码的重排序,在保证运行结果的情况下,充分的调用CPU,这部分应该由Java的开发者去设计。

那么到最后,有关JVM调优可以调的东西,已经呼之欲出,那就是JVM对于内存的管理,因为每个程序对于Java空间的使用都是高度自由的,每个由JVM运行的程序实际需要使用多少内存,如何淘汰内存,JVM只能给出最基本的最通用的方案。为了让程序可以更加高效的使用内存,所以需要由我们来进行调优操作。

JVM调优的目标

  • 减少内存泄漏和溢出:通过合理的内存分配和垃圾回收策略避免OutOfMemoryError
  • 减少GC停顿时间:优化GC算法和配置,使得应用停顿时间最小化。
  • 提高吞吐量:最大化CPU和内存利用率,提高应用的处理效率。
  • 提升响应时间:减少应用程序在高并发时的响应延迟,提升用户体验。

开调

首先查看一下 java21 使用的默认的垃圾回收器是什么

[root@iZbp1cygmrq21v08er1kv3Z bin]# ./java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=28776064 JVM堆内存的初始大小 28776064字节(约27MB)
-XX:MaxHeapSize=460417024 JVM堆内存的最大大小 460417024字节(约440MB)
-XX:MinHeapSize=6815736 JVM堆内存的最小大小 6815736字节(约6.8MB)
-XX:+PrintCommandLineFlags 打印JVM启动时所使用的所有命令行参数
-XX:ReservedCodeCacheSize=251658240 打印JVM启动时所使用的所有命令行参数(代码缓存用于存储编译后的本地代码(JIT编译后))
-XX:+SegmentedCodeCache 启用分段代码缓存 (JVM代码缓存分为多个段,每个段分别用于存储不同用途的代码(如非方法代码、方法代码等))
-XX:+UseCompressedOops 启用压缩普通对象指针(OOPs)一个JVM优化技术,用于减少64位JVM中对象指针的大小。
-XX:+UseSerialGC 使用Serial GC(串行垃圾回收器)
openjdk version "21.0.1" 2023-10-17
OpenJDK Runtime Environment (build 21.0.1+12-29)
OpenJDK 64-Bit Server VM (build 21.0.1+12-29, mixed mode, sharing)

可以看到我使用的默认的垃圾回收器是 Serial GC

Serial 收集器

Serial 收集器是最基础、历史最悠久的收集器,曾经(在 JDK 1.3.1 之前)是HotSpot 虚拟机新生代收集器的唯一选择。

这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。「这里就说明了,为什么有时候会卡住,但是没有崩溃」

image-20240907204623073

优势

  • 对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)「内存占用,此语境中指为保证垃圾收集能够顺利高效地进行而存储的额外信息。」最小的
  • 对于单核处理器或处理器核心数较少的环境,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

常用的GC调优参数

参数 描述
-XX:NewRatio=<n> 设置老年代与新生代的大小比例
-Xmn<size> 直接设置新生代的大小(包括Eden和两个Survivor区)
-Xms<size> 设置JVM启动时的初始堆内存大小。
-Xmx<size> 设置JVM堆内存的最大大小。
-XX:NewSize=<size> 设置年轻代内存的初始大小。
-XX:MaxNewSize=<size> 设置年轻代内存的最大大小。
-XX:MetaspaceSize=<size> 设置元空间的初始大小(Java 8及以后)。
-XX:MaxMetaspaceSize=<size> 设置元空间的最大大小(Java 8及以后)。
-XX:+UseG1GC 启用G1垃圾回收器(自Java 9起默认)。
-XX:+UseConcMarkSweepGC 启用CMS垃圾回收器。
-XX:+UseParallelGC 启用Parallel垃圾回收器。
-XX:+UseZGC 启用Z Garbage Collector(用于低延迟场景)。
-XX:+UseShenandoahGC 启用Shenandoah垃圾回收器(低延迟大堆内存应用)。
-XX:MaxGCPauseMillis=<n> 设置垃圾回收的最大暂停时间。
-XX:+PrintGCDetails 打印垃圾回收的详细信息。
-Xlog:gc* 开启并记录垃圾回收日志。
-XX:SurvivorRatio=<ratio> 设置Eden区与Survivor区的比例。
-XX:MaxTenuringThreshold=<n> 设置对象晋升到老年代之前在年轻代存活的次数。
-XX:+PrintFlagsFinal 输出所有JVM参数及其最终值。
-XX:+HeapDumpOnOutOfMemoryError 内存不足时生成堆转储。
-XX:HeapDumpPath=<path> 设置生成的堆转储文件的路径。
-XX:ParallelGCThreads=<n> 设置并行GC使用的线程数。
-Xss<size> 设置每个线程的栈大小。
-XX:+UseCompressedOops 启用64位JVM中的对象指针压缩。
-XX:+AggressiveOpts 启用编译时的激进优化。
-XX:+TieredCompilation 启用分层编译(默认启用)。
-XX:+AlwaysPreTouch 在JVM启动时预先触碰内存以避免延迟。
-XX:+UnlockExperimentalVMOptions 启用实验性的JVM参数。
-XX:+UseLargePages 启用大页内存支持以提升内存访问性能。
-XX:+HeapDumpOnOutOfMemoryError 在内存溢出时生成堆转储文件。
-XX:+UseEpsilonGC 启用Epsilon垃圾回收器(无操作GC)。
-Djava.security.manager 启用Java安全管理器。
-Djava.security.policy=<path> 指定安全策略文件。

分析Java进程内存占用

  1. 使用jps和jmap/jhsdb命令

    1. 查找Java进程ID(PID): 使用jps命令找到正在运行的Java进程ID。

      [root@iZbp1cygmrq21v08er1kv3Z bin]# ./jps
      8464 Jps
      11667 BootstrapLauncher
    2. 查看Java进程的堆内存使用情况: 使用jmap -heap <pid>查看指定Java进程的堆内存详细信息。

      [root@iZbp1cygmrq21v08er1kv3Z bin]# ./jmap -heap 11667
      Error: -heap option used
      Cannot connect to core dump or remote debug server. Use jhsdb jmap instead

      - 说明Java21已经不支持使用jmap了,建议使用jhsdb代替

      [root@iZbp1cygmrq21v08er1kv3Z bin]# ./jhsdb jmap --heap --pid 11667
      Attaching to process ID 11667, please wait...
      Debugger attached successfully.
      Server compiler detected.
      JVM version is 21.0.1+12-29

      using thread-local object allocation.
      Mark Sweep Compact GC

      Heap Configuration:
      MinHeapFreeRatio = 40
      MaxHeapFreeRatio = 70
      MaxHeapSize = 2147483648 (2048.0MB)
      NewSize = 357892096 (341.3125MB)
      MaxNewSize = 715784192 (682.625MB)
      OldSize = 715849728 (682.6875MB)
      NewRatio = 2
      SurvivorRatio = 8
      MetaspaceSize = 22020096 (21.0MB)
      CompressedClassSpaceSize = 1073741824 (1024.0MB)
      MaxMetaspaceSize = 17592186044415 MB

      Heap Usage:
      New Generation (Eden + 1 Survivor Space): 新生代内存使用情况
      capacity = 392626176 (374.4375MB)
      used = 312816152 (298.3247299194336MB)
      free = 79810024 (76.1127700805664MB)
      79.67277046755028% used
      Eden Space: Eden
      capacity = 349044736 (332.875MB)
      used = 312809144 (298.3180465698242MB)
      free = 36235592 (34.55695343017578MB)
      89.61863960039781% used
      From Space: Survial1
      capacity = 43581440 (41.5625MB)
      used = 7008 (0.006683349609375MB)
      free = 43574432 (41.555816650390625MB)
      0.016080239661654134% used
      To Space: Survial2 这里可以看出来 Serial堆采用的标记复制算法的区域划分是 8:1
      capacity = 43581440 (41.5625MB)
      used = 0 (0.0MB)
      free = 43581440 (41.5625MB)
      0.0% used
      tenured generation: 老年代
      capacity = 872280064 (831.87109375MB)
      used = 476408736 (454.3387756347656MB)
      free = 395871328 (377.5323181152344MB)
      54.61648794486263% used

      这个是在服务器一个人都没有的情况下测的
    3. 生成堆转储

       ./jhsdb jmap --pid 11667 --binaryheap --dumpfile  serverDump.hprof
      崩溃了 。。。

      image-20240907213817889

      只能重启了

  2. 使用jstat查看GC内存信息

    jstat用于实时监控JVM的内存和GC(垃圾回收)情况。

    [root@iZbp1cygmrq21v08er1kv3Z bin]# ./jstat -gc 1578 1000 10
    S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
    43904.0 43904.0 0.0 607.4 351296.0 245370.1 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 246168.6 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 252758.1 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 253556.5 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 254355.0 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 255153.4 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 255951.9 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 256750.3 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 257548.7 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396
    43904.0 43904.0 0.0 607.4 351296.0 258447.0 878076.0 575893.6 103360.0 101672.8 14848.0 14110.2 19 3.461 4 1.935 - - 5.396

    查看GC统计信息:

    S0C / S1C:Survivor 0 / Survivor 1 区容量。

    S0U / S1U:Survivor 0 / Survivor 1 区使用量。

    EC:Eden区容量。

    EU:Eden区使用量。

    OC:老年代容量。

    OU:老年代使用量。

    YGC:年轻代GC次数。

    FGC:Full GC次数。

  3. 使用topps命令查看系统层面的内存使用情况

    [root@iZbp1cygmrq21v08er1kv3Z server]# top
    top - 21:58:31 up 12:17, 2 users, load average: 0.50, 0.71, 3.94
    Tasks: 98 total, 1 running, 97 sleeping, 0 stopped, 0 zombie
    %Cpu(s): 2.4 us, 1.7 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
    KiB Mem : 1798504 total, 77300 free, 1544436 used, 176768 buff/cache
    KiB Swap: 3145724 total, 3113344 free, 32380 used. 113016 avail Mem

    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
    1578 root 20 0 4870020 1.4g 16800 S 22.0 79.1 4:01.39 java
    1058 root 10 -10 154608 18888 2516 S 4.3 1.1 24:03.85 AliYunDunMonito
    1022 root 20 0 115272 4588 2824 S 0.3 0.3 4:19.04 AliYunDun

    [root@iZbp1cygmrq21v08er1kv3Z server]# ps -p 1578 -o %mem,rss,vsz,cmd
    %MEM RSS VSZ CMD
    79.0 1422416 4870020 java/bin/java @user_jvm_args.txt @libraries/net/minecraftforge/forge/1.20.1-47.2.18/unix_args.txt

    %mem:内存使用百分比。

    rss:常驻内存大小(以KB为单位)。

    vsz:虚拟内存大小(以KB为单位)。

    cmd:命令行。

  4. 使用VisualVM分析内存

  5. 使用MAT(Memory Analyzer Tool)分析内存

总结

总的来说,这台服务器无法负担除了Serial之外的其他垃圾回收器,因为只有2c 2g,对于这种核心较少,内存较小的服务器来说,Serial是比较适合的。

通过jmap/jstat 等工具来分析内存的使用情况,可以看出目前老年代的内存还有所富裕,可以考虑调高新生代的内存占比,因为MC是一个实时加载的游戏,每多一个玩家,服务器需要处理的地图区块就会越多,又因为玩家是不断移动的,所以对于新生代的压力应该会比较大,这点从 gc次数 也可以看出来,刚刚启动不久,YGC就已经19次了。

同时考虑的是降低MC对于每个玩家生成的区块数量,反应到JVM上就是减少新生代对象的产生(大概)。

还有一点就是在游玩的时候,尽可能的在一起,让服务器不需要去重复的加载很多其他的区块。

QWQ 调优之后直接启动不了了

  • 尝试将新生代和老年代的比例设置为1:1
  • 尝试固定新生代为1G

改为默认的设置,单人游玩一段时间,统计数据如下

image-20240907230501040

我的推测是没问题的。可以看到,经过GC后,新生代几乎全灭,而老年代基本上没什么变化

至于为什么启动不了,只能之后进一步了解了