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
}
其中最重要的四个表
- 常量池(constant_pool):符号
- 字段表,方法表:两个表格式一样,描述字段和方法的签名。类型,参数,访问控制等。
- 属性表:字段和方法表,都携带属性表,属性表是最灵活的。包括方法的字节码都放在“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对象的方法
MyClass.class
object.getClass()
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的方法。
字节码生成(静态代理)
通过javassist
、cglib
等库,对加载进来的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实例,分别是程序执行前,和程序执行中调用。
-
public static void premain(String agentArgs, Instrumentation inst);
-
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模块化