摘要
在玩大型整合包的时候,常常会遇到并发修改异常(ConcurrentModificationException, CME)和下标越界异常(IndexOutOfBoundsException, IOOBE)等问题导致游戏崩溃。由于并发修改往往发生在多个不同的线程中,因而仅通过崩溃日志中一个线程的调用栈输出,通常无法确认是哪个模组导致的:
SimpleReloadInstance 的并发修改异常于是本模组应运而生,旨在通过输出一段时间内特定容器在每个线程被修改的历史,辅助玩家进行问题排查:
本模组可以输出一个容器的修改历史
使用方法
将模组(jar 文件)放进 mods 文件夹,即和其它模组一样正常安装,无需在意加载器是什么。
在启动器(或保存有虚拟机参数的文件)中修改 Java 虚拟机参数(即 JVM 参数),向其中加入 “-javaagent:mods/CMESuckMyDuck-<version>.jar=<class full name>;<field name>;<type>;<phase>”(参数含义与示例见下文)。
启动游戏,运行并直到崩溃再次发生。
阅读游戏目录中的 CMESuckMyDuck.log 文件查看被监视容器的修改历史。
参数含义
class full name
被监视容器所在类(class)的全名(即内部名,用“/”代替“.”,如“net/minecraft/server/packs/resources/SimpleReloadInstance”)。
field name
被监视容器的变量名,目前只支持类成员变量,不支持局部变量。
对于 Forge,请使用 SRG 名(1.16.5 及以前如 field_123456_a,1.17 及以后如 f_123456_)。
对于 Fabric,请使用 intermediary 名(包括类全名也要使用中间名,如 field_123456)。
对于 NeoForge,请使用官方名(如 instances、structureRepository 等)。
type
被监视容器的类型,目前只支持 List、Set、Map。
phase
static 或 nonstatic,表示容器是类静态成员还是非静态成员。
如何确认填什么参数
这里我们举一个例子。首先我们根据 CME 或 IOOBE 的调用栈,确认容器所在的类:
SoundEngine 的并发修改异常的调用栈接着,阅读该类的代码,确认该类的哪个成员遭受了并发修改:
SoundEngine 中被并发修改的类于是我们确认需要被监视的容器是 field_217942_m(SoundEngine 中的 instanceToChannel),于是我们安装 CMESuckMyDuck-1.0.0.jar 模组,向 Java 虚拟机参数中加入“-javaagent:mods/CMESuckMyDuck-1.0.0.jar=net/minecraft/client/audio/SoundEngine;field_217942_m;Map;nonstatic”,并等待下一次报错。
最后,打开 CMESuckMyDuck.log 日志,阅读容器修改历史,便可以确认哪几个线程发生了冲突,并发修改了容器导致了崩溃。
JVM 参数示例
ConcurrentModificationException from SoundEngine in Forge 1.16.5 Environment
-javaagent:mods/CMESuckMyDuck-1.0.0.jar=net/minecraft/client/audio/SoundEngine;field_217942_m;Map;nonstatic
ConcurrentModificationException from PotionBrewing in Forge 1.20.1 Environment
-javaagent:mods/CMESuckMyDuck-1.0.0.jar=net/minecraft/world/item/alchemy/PotionBrewing;f_43494_;List;static
ArrayIndexOutOfBoundsException from Zeta mod
-javaagent:mods/CMESuckMyDuck-1.0.0.jar=org/violetmoon/zetaimplforge/event/ForgeZetaEventBus;convertedHandlers;Map;nonstatic
其它设置
日志级别(不推荐修改)
使用系统属性“-Dcme_suck_my_duck.log_level=<level>”修改日志级别,有 0~3 四个级别。
0 将会输出容器一切访问历史,不仅包括修改,还包括查询如 Map#get、Set#containsAll 等,往往会导致日志过于冗长。
1 将不再输出查询历史,而是仅输出容器的修改历史,是模组默认的日志级别。
2 将不再输出容器修改历史,仅输出向类注入修改时的警告与异常,方便开发者调试。
3 将仅输出模组 premain 阶段的异常。