java的动态性

类和类加载构成了java跨平台的基石,也是java出色动态性的原因。

#1. 类和类加载器

与c,c++编译成目标文件不同,java是编译成类文件。平台无关的类文件就构成了java跨平台的基石。

类文件的结构

编译好的class文件,可以用javap -verbose来解析类文件的详细信息。

Constant pool:
   #1 = Methodref          #7.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#23         // A.a:I
   #3 = Fieldref           #4.#24         // A.b:I
   #4 = Class              #25            // A
   #5 = Methodref          #4.#22         // A."<init>":()V
   #6 = Methodref          #4.#26         // A.sum:(I)I
   #7 = Class              #27            // java/lang/Object
   {
     volatile int a;
       descriptor: I
       flags: ACC_VOLATILE

       public static void main(java.lang.String[]);
         descriptor: ([Ljava/lang/String;)V
         flags: ACC_PUBLIC, ACC_STATIC
         Code:
           stack=2, locals=1, args_size=1
              0: new           #4                  // class A
              3: dup
              4: invokespecial #5                  // Method "<init>":()V
              7: iconst_3
              8: invokevirtual #6                  // Method sum:(I)I
             11: pop
             12: return
           LineNumberTable:
             line 11: 0
             line 12: 12
      }

其中最重要的四个表

  1. 常量池(constant_pool):符号
  2. 字段表,方法表:两个表格式一样,描述字段和方法的签名。类型,参数,访问控制等。
  3. 属性表:字段和方法表,都携带属性表,属性表是最灵活的。包括方法的字节码都放在“code”属性中

1 LineNumberTable属性 java源代码的行号和字节码行号的对应关系,调试断点使用。默认生成。-g:none 或 -g:lines来取消或生产这些信息。 2 LocalVariableTable属性 描述栈桢中局部变量表中的变量与java源码中定义的变量之间的关系。 没有的时候,报错或者调试,没有局部变量的名字 3 sourcefile属性 也是可选的。-g:none 或 -g:lines来取消或生产这些信息。

其中调用sum方法的字节码invokevirtual #6,#6的常量池,描述了调用的方法是属于哪个类的哪个方法。所以,java不能像python那样动态调用方法。

动态加载类文件

编译好的类文件,不用进行静态链接,直接就可以运行。没有静态链接,就不会引入系统相关的库,保证了java的跨平台。

类文件,通过类加载器加载在合适的时间加载,然后动态链接,初始化。

解析,是将虚拟机常量池的符号引用替换为直接引用的过程。

初始化:执行类构造器(clinit)。自动生成,处理类的静态变量赋值,和静态代码块。静态变量分配在堆空间上。

类加载器

重载Class<?> findClass(String name)来从不同的源来加载类,比如网络下载,解压文件,动态生成等。重载Class<?> loadClass(String name)来打破双亲委派模型。

invokestatic #20 调用静态方法。在类加载的解析阶段,就会把符号解析成直接引用。

反射

使用反射不同于常规的Java编程,其中它与 元数据–描述其它数据的数据协作。Java语言反射接入的特殊类型的原数据是JVM中类和对象的描述。反射使您能够运行时接入广泛的类信息。它甚至使您能够读写字段,调用运行时选择的类的方法。

得到class对象的方法

  1. MyClass.class
  2. object.getClass()
  3. Class.forName(name)
Class<Hello> helloClass = Hello.class;
Method helloMethod = helloClass.getDeclaredMethod("sayHello");
helloMethod.invoke(hello, null);

绽放java的动态性,字节码生成技术和类加载器的替换。

#2. 动态代理

动态代理提供了强大的动态性。解耦了代理类和原始类,能够以面向切面的方式编程。是面向对象设计模式的延展,打破面向对象的壁垒,能够以更灵活的方式更改原始类的行为。

partial class,对已有的对象添加方法和字段,在没有这个机制的情况下,装饰者,vistor模式都是为了这个目的。

spring AOP,就是基于动态代理实现的。但是只能对原始类的方法进行扩展。如果要对原始类的字段进行扩展,就需要用到更复杂的AspectJ。

jdk实现的Proxy类,调用Proxy.newProxyInstance(classLoader,interface,InvocationHandler),就可以创建动态代理

Hello hello = new Hello();
IHello helloInter = (IHello) Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(),
		new InvocationHandler() {
			@Override
			public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable {
				return method.invoke(hello, par);
			}
		});
helloInter.sayHello();

动态代理的实现原理

有jdk的实现和cglib的实现,jdk只能对接口创建动态代理,cglib可以对类创建动态代理。

jdk动态代理,应用反射和生成字节码两项技术实现的动态代理,会通过字节码拼接动态生成一个$Proxy0.class的类,实现动态代理接口的所有方法。都是调用InvocationHandler的invoke,并传入通过反射获取的原来Hello的方法。

字节码生成(静态代理)

通过javassistcglib等库,对加载进来的class文件进行修改,达到增强原来class代码的目的。比如对所有实体类包下的class都添加一个save的方法。这样修改的代码,减少了手写代码的工作量,而且只有初次增强代码时候的消耗。

ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("other.Stuff");
CtMethod m = cc.getDeclaredMethod("run");
m.addLocalVariable("elapsedTime", CtClass.longType);
m.insertBefore("elapsedTime = System.currentTimeMillis();");
Class modify = clascc.toClass()

#3. 代码热更新

classLoader

打破双亲委派,将需要热更新的类,用新的classloader重新加载。(但是这种方式把之前的类变量也还原了)

osgi 就用了复杂的classLoader来实现动态模块化。可以实现模块的热插拔。

Instrument & javaAgent

更强大的热更新,提供了虚拟机级别的AOP。加载javaAgent的时候通过Instrument来动态改变运行中的方法字节码。但是Instrument改变的class不能添加和删除字段或方法

javaAgent,就是一个jar包,并且在manifest中,指定了javaAgent被加载后,需要执行的类,被指定的类,需要有符合规范的agent方法。

javaAgent的执行方法,会让我们获得Instrumenttation对象。Instrument就提供了重新加载定义class的方法。可以动态更新class。 inst.retransformClasses(classes);

有两种方法获取Instrumentation实例,分别是程序执行前,和程序执行中调用。

  1. public static void premain(String agentArgs, Instrumentation inst);

  2. public static void agentmain (String agentArgs, Instrumentation inst);

premain的调用

premain的调用是在程序启动前,添加-javaagent:jarpath[=options]参数,程序就会在main函数之前加载javaAgent,并执行premain

系统本身也实现一些javaAgent,来方便我们使用。通过java命令,可以看到jvm自带的一些javaagent可以通过-agentlib:<libname>[=<options>]来执行,包括-agentlib:hprof性能监控,-agentlib:jdwp=help程序调试。

实战:通过javaAgent破解加密的class,代码在github

agentmain的调用

程序运行中,attach程序,调用loadAgent,tools包里的VirtualMachine.attach可以动态attach一个运行中的jvm虚拟机,并可以vm.loadAgent(jar);动态加载agent。agent jar并加载后就会执行agentmain

实战:attach进程,retranformClass来热更新代码,代码在github


延展阅读

  • jvm对动态语言的支持,invokeDynamic指令
  • java模块化

Updated: