JVM字节码之其他

  • 泛型在字节码中的表现
  • 反射
  • javac是如何编译的

泛型

泛型在字节码中

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {

}
public void test(List<String> list) { }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void test(java.util.List<java.lang.String>);
descriptor: (Ljava/util/List;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/yuda/test/Main;
0 1 1 list Ljava/util/List;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 1 1 list Ljava/util/List<Ljava/lang/String;>;
Signature: #22 // (Ljava/util/List<Ljava/lang/String;>;)V

可以看到Code里面没有任何String相关的表示, 说明JVM处理泛型时, 会自己删除掉这种类型, 只能识别为List, 这样就导致了无法用下面方式重载方法:

1
2
public void test(List<String> list)  { }
public void test(List<Integer> list) { }

但是他多了一个Signature里面记录了参数的类型, 通过Signature, 可以在反射中取到类型.

反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
Class<?> clz = Class.forName("com.yuda.test.Main");
Method[] methods = clz.getMethods();
for (Method method : methods) {
if (method.getName().equals("test")){
Parameter[] parameters = method.getParameters();
Type[] genericParameterTypes = method.getGenericParameterTypes();
for (Type genericParameterType : genericParameterTypes) {
System.out.println(genericParameterType.getTypeName());
}
}
}
}
public void test(List<String> list) { }
}

上面是通过反射取泛型的例子.

反射原理

故意让反射执行的方法报错, 看看它的异常栈.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> clz = Class.forName("com.yuda.test.Main");
Method method = clz.getDeclaredMethod("test");
for (int i = 0; i < 20; i++) {
method.invoke(null);
}
}
private static int count = 0;
public static void test() {
new Exception("test#" + (count++)).printStackTrace();
}
}

循环了20次, 报错日志中搜索sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 却发现只有16次调用, 剩余4次使用了sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43).

应该是超过了某个阈值(16), 反射使用另外一种方式.

NativeMethodAccessorImpl

1
2
3
4
5
6
7
8
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}

return invoke0(this.method, var1, var2);
}

ReflectionFactory.inflationThreshold()进去发现默认是15(1+15=16), 16次都会调到private static native Object invoke0(Method var0, Object var1, Object[] var2);一个native的方法,之后就会进入新的逻辑, 通过sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)来执行, GeneratedMethodAccessor1是通过 ASM 生成新的类.

为啥这么做? 小于15的认为使用率比较少, 用native方法比较划算, 大于15次认为经常使用, 使用生成字节码的方式比较划算. 可见native的方式执行起来比较慢, 生成字节码方式执行比较快, 但是多了一步生成字节码的操作, 这一步增加了耗时.

-Dsun.reflect.noInflation=true设置是否不使用native, -Dsun.reflect.inflationThreshold=20设置阈值

javac编译

javac

  1. parse : 读取java文件, 做词法分析和语法分析, 生成抽象语法树(AST).
  2. enter : 填充符号表(symbol table), 方便后续从里面取东西. 由标识符与标识符相关信息构成
  3. process : 注解的处理, 在编译过程中立即代码中的注解, 影响生成的字节码.
  4. attr : 语义合法性检查和进行逻辑判断.
    1. 冲突的类定义
    2. 访问权限, 检查用的对不对
    3. 重载方法的使用选择
    4. 折叠常量.
  5. flow : 数据流分析.
    1. 非void方法是否有return
    2. 受检异常是否有处理
    3. 局部变量是否初始化
    4. final是否有赋值
    5. 是否有语句不可达
  6. desugar : 去除语法糖, 转换成标准的写法, 以便统一的处理.
    • 泛型
    • 内部类
    • foreach
    • 包装类与基本类型隐式转换
    • 字符串与枚举的switch
    • i++++i
    • 变长参数test(String... strs)
  7. generate : 生成最终的 Class文件.