java特种兵读书笔记(3-1)——java程序员的OS之跨平台与字节码

来源:互联网 发布:手游刷金币的软件 编辑:程序博客网 时间:2024/05/19 13:56

用javap看一下自增操作的实现


int a=1,b=1,c=1;

c++;

int d=a++;

b=b++;

这时d=1这个好理解,那么b=1,这个不好理解了。看看javap的编译过程:

单独执行c++其实和单独执行++c的操作是一样的。

从头解释,第一行赋值,以a=1解释:

1)首先将int型的1推送到栈顶(iconst_1)

2)将栈顶抛出的数据赋值给第一个slot所在的int类型的本地变量中(istore_0)

再解释b=b++:

1)首先第三个slot(b所在的slot)所在的int型本地变量拷贝到栈顶(iload_2)

2)将第三个slot(b所在的slot)所在的int型本地变量加1(iinc)

3)将栈顶抛出的数据写入到第三个slot所在的int型本地变量(istore_2)

关键点在于,把本地变量b拷贝了一份到栈顶保存,然后本地变量自增,然后用栈顶保存的数据覆盖本地变量b的值(如果是b=++b的话,就是先本地变量自增,然后压栈,然后pop覆盖本地变量的值了,虽然后面的压栈和赋值操作是多余的)。可以用如下代码来解释:

int temp =b;b++;b=temp;

只是真正的操作没有temp这个真实存在的本地变量,而是在栈顶的一份数据拷贝。

iload:数据加载(拷贝)到栈顶

istore:将数据从栈顶pop出,赋值到本地变量中。

java为什么跨平台


java的Class文件是统一的,是基于字节码的。字节码是以byte为单位存储的文件。它是描述程序需要运行的虚指令的集合。该虚指令的集合和平台无关,java虚拟机会将其翻译为对应的OS指令

java->Class文件(虚指令集合)->OS指令

java虚拟机会为不同的OS平台编写对应的JRE(java运行时环境),与OS动态链接,将虚指令翻译为对应操作系统的汇编指令。

javac


解析java源文件,处理注解,属性标注,检查,泛型处理,语法糖转换。最后生成.Class文件。

class文件


class文件头包括:常量池区域,类描述信息,属性列表,方法列表,attributes列表。

常量池


为什么不将内容直接放在字节码中?

程序中会出现很多重复的内容拷贝,将它们用常量池保存起来之后,描述程序时只需要用很短的一个人口地址就可以表示了,不需要用很长的字符串表示。这样不但节约了空间,Class字节码文件也显得更加规整。

ClassLoader与java字节码的加载


BootStrapClassLoader:加载java.lang.*。它是由JVM内核实现,在HotSpot VM中使用C++实现的。

ExtClassLoader:加载jre/lib/ext目录下的jar包。用户可以将自己的jar放到该目录下,用该ClassLoader加载。

AppClassLoader:加载classpath下的内容。

如果没有找到对应的类,会抛出ClassNotFoundException异常。

如果不符合字节码规范结构,会抛出ClassNotFoundError异常。

类启动与static


如果类的static变量是本身自己,就会出现类加载错误,因为static变量首先加载,然后发现该类还没有加载完成。

static块在加载的时候会阻塞线程。如果static块中出现死循环,程序会一直处于启动状态。所以在static块中不要写太多的业务逻辑,也不要做大量的IO操作。

NoClassDefFoundError,如果有这个异常出现,说明是自己代码写的有问题,需要检查一下上述问题了。

代码是否可以被JIT优化


return b()==null?"":b();

这样b()方法会被调用两次,JIT做了inline之后其实无所谓。

但是如果b()方法中有访问数据库或者IO操作的话,那么就要注意了。

if(map.contains("abc")) {map.get("abc")}

这样会在map中查找两次,可以用get得到的结果是否为null来判断就好了,这样只对map操作了一次。

static块,static变量,final static变量,普通块,构造方法


static变量的赋值操作会被放在static块中进行(javap可以看出来)。如果是final static普通变量,比如int或者常量池中的String(不是new String()出来的)会直接赋值。

普通块会编译在构造方法之前,但是在super()之后。

动态代理


动态代理是java默认提供的AOP技术,也是早期Spring AOP框架所选用的技术(早期Spring代理类必须用接口获取返回值)。代理,需要一个代理原始类的实体存在,我们需要实现invocationHandler接口,调用其invoke方法。

该技术的缺点:必须基于接口来实现,就是说代理的类一定要有接口,而且只能调用接口对应的方法,实例的其他方法无法访问。其次,方法之间的调用无法AOP,只有外部调用这些方法的时候才会调用代理的invoke。

字节码增强技术


有两种实现机制:

一种是通过创建原始类的子类,现在的SpringAOP就是这样实现的。它创建的子类通常以原始类的类名为前缀,再加上一个奇怪的后缀,以避免类名重复。

另一种是更加暴力的直接修改Class字节码,在许多类的跟踪过程中会使用到此技术。

字节码增强技术的实现:

1)从内存中获取原始的字节码,然后通过一些开源的API修改其byte[]数组,得到一个新的byte数组。

2)将这个byte[]数组写入到PermGen区,即加载它,替换原来的Class字节码。

如果Spring的AOP用字节码增强技术实现的话:

首先会定义一个ClassLoader,所有的类都用这个ClassLoader去加载,再通过ASM字节码增强去实现它,然后当使用getBean得到对象的时候,就是字节码修改后的Class生成的对象了。

javaassist增强字节码


/**
* 用javaassist做AOP
*/
public static void testJavaAssist() throws Exception {
// 用javaassist增加字节码,在print方法前面打印了before
CtClass ctClass = ClassPool.getDefault().get("oscar.test.classload.Loading");
CtMethod ctMethod = ctClass.getDeclaredMethod("print");
// 可以通过insertBefore来调用Loading的test方法
ctMethod.insertBefore("test(new Integer(1));");
// 可以通过insertBefore来给Loading的name变量赋值
ctMethod.insertBefore("name=\"yuxiang\";");
ctMethod.insertBefore("System.out.println(\"before\");");
// 这里得到的就是增强之后的字节码
byte[] bytesForClass = ctClass.toBytecode();
// 用增强后的字节码生成类
Class<?> clazz = loader.defineClassByBytes("oscar.test.classload.Loading", bytesForClass);
// Class<?> clazz = loader.loadClass("oscar.test.classload.Loading");
// 用增强后的Class生成对象,并调用方法
clazz.getMethod("print").invoke(clazz.newInstance());
}

public class DynamicClassLoader extends URLClassLoader {// 定义自己的类加载器

public DynamicClassLoader(URLClassLoader parent) {
super(parent.getURLs(), parent);
}

public Class<?> defineClassByBytes(String className, byte[] bytes) {// 可以用增强的字节码
return this.defineClass(className, bytes, 0, bytes.length);
}
}

public class Loading {//测试类

public void print() {
System.out.println("print loading");
}
}

通过javaassist实现了在调用Loading类的print方法之前,打印一句话,类似Spring的AOP。相比于ASM要方便很多。需要注意的是,javaassist对于同一个类,只能重新定义一次。再次增强字节码时,会报错,class is frozen and pruned。




0 0
原创粉丝点击