如何用ASM的MethodVisitor统计常量访问次数及问题排查
统计类中Static Final常量使用次数的问题解析与解决方案
问题原因
- 编译期常量的折叠优化
对于static final的编译期常量(比如基本类型、字符串,且值在编译阶段就能确定),javac会执行常量折叠优化:直接将常量的值嵌入到代码使用的位置,而不是生成GETSTATIC指令去访问字段。这就导致visitFieldInsn方法根本不会被触发,因为字节码里已经没有访问该字段的指令了。 - 常量加载指令的多样性
常量折叠后,不同类型的常量会对应不同的字节码加载指令:- 小范围整数用
ICONST_*/BIPUSH/SIPUSH - 字符串、包装类实例用
LDC - 浮点数用
FCONST_*/DCONST_*
如果只监听visitLdcInsn,会漏掉其他类型常量的加载指令,自然无法统计到所有常量的使用。
- 小范围整数用
解决方案
要完整统计static final常量的使用,需要分两步处理:先收集类中所有static final常量的元数据,再在方法中匹配所有可能加载常量值的字节码指令,最终映射到对应的常量字段名统计次数。
步骤1:收集类中Static Final常量的元数据
在ClassVisitor的visitField方法中,筛选出static final字段,记录字段名和对应的值(编译期常量会直接携带value参数),同时标记运行期初始化的static final字段(这类字段会通过GETSTATIC访问)。
// 存储编译期static final常量的字段名->值映射 private Map<String, Object> fieldNameToValue = new HashMap<>(); // 存储运行期初始化的static final字段名 private Set<String> runtimeStaticFinalFields = new HashSet<>(); @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { boolean isStaticFinal = (access & Opcodes.ACC_STATIC) != 0 && (access & Opcodes.ACC_FINAL) != 0; if (isStaticFinal) { if (value != null) { // 编译期常量,直接记录值 fieldNameToValue.put(name, value); } else { // 运行期初始化的static final字段,后续监听GETSTATIC指令 runtimeStaticFinalFields.add(name); } } return super.visitField(access, name, desc, signature, value); }
步骤2:在MethodVisitor中匹配常量加载指令
需要覆盖MethodVisitor中所有可能加载常量的方法,将指令中的值与之前收集的常量元数据匹配,更新constantFrequencyMap的计数:
private Map<String, Integer> constantFrequencyMap = new HashMap<>(); @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { // 处理运行期static final常量的GETSTATIC访问 if (opcode == Opcodes.GETSTATIC && runtimeStaticFinalFields.contains(name)) { constantFrequencyMap.put(name, constantFrequencyMap.getOrDefault(name, 0) + 1); } super.visitFieldInsn(opcode, owner, name, desc); } @Override public void visitLdcInsn(Object cst) { matchConstantValue(cst); super.visitLdcInsn(cst); } // 处理整数常量加载指令 @Override public void visitIconst_0() { matchConstantValue(0); super.visitIconst_0(); } @Override public void visitIconst_1() { matchConstantValue(1); super.visitIconst_1(); } @Override public void visitIconst_2() { matchConstantValue(2); super.visitIconst_2(); } @Override public void visitIconst_3() { matchConstantValue(3); super.visitIconst_3(); } @Override public void visitIconst_4() { matchConstantValue(4); super.visitIconst_4(); } @Override public void visitIconst_5() { matchConstantValue(5); super.visitIconst_5(); } @Override public void visitIconst_m1() { matchConstantValue(-1); super.visitIconst_m1(); } @Override public void visitBIPUSH(int value) { matchConstantValue(value); super.visitBIPUSH(value); } @Override public void visitSIPUSH(int value) { matchConstantValue(value); super.visitSIPUSH(value); } // 处理浮点数常量加载指令 @Override public void visitFconst_0() { matchConstantValue(0.0f); super.visitFconst_0(); } @Override public void visitFconst_1() { matchConstantValue(1.0f); super.visitFconst_1(); } @Override public void visitFconst_2() { matchConstantValue(2.0f); super.visitFconst_2(); } @Override public void visitDconst_0() { matchConstantValue(0.0d); super.visitDconst_0(); } @Override public void visitDconst_1() { matchConstantValue(1.0d); super.visitDconst_1(); } // 匹配常量值到对应的字段名,更新计数 private void matchConstantValue(Object targetValue) { for (Map.Entry<String, Object> entry : fieldNameToValue.entrySet()) { if (targetValue.equals(entry.getValue())) { String fieldName = entry.getKey(); constantFrequencyMap.put(fieldName, constantFrequencyMap.getOrDefault(fieldName, 0) + 1); // 如果存在值重复的常量,这里可以结合字段类型进一步区分 break; } } }
注意事项
- 值冲突问题:如果两个不同的
static final常量值相同(比如两个不同的字符串常量值一样),仅通过值匹配会统计错误。这种情况需要结合字段的类型描述符(desc)来做更精确的匹配,比如在收集元数据时同时存储字段的类型,匹配时先校验类型再比较值。 - 运行期常量的特殊性:对于在静态代码块中初始化的
static final字段,字节码会生成GETSTATIC指令,因此可以通过visitFieldInsn直接捕获统计。
内容的提问来源于stack exchange,提问作者Zihe Ji
相关产品推荐
相关产品推荐

