JVM 编程(Oolong)学习笔记

近期学习Engel的《Programming for the Java Virtual Machine》。这本书实际上讲的是JVM原理和机制,只不过用一种汇编语言的形式来展开讲解。Oolong是大牛Engel先生自己发明的一种基于JVM的汇编语言,按他自己的介绍,Oolong实际只是JVM bytecode的一个易于理解的教学版本,其本身似乎并不具备什么开发应用的价值。

由于对jvm本身已经有一定的熟悉,所以这里没有什么详细的笔记可做,仅就书中一些比较迷惑的地方做一些说明。


数组操作:
对数组元素的写操作,书中有个例子在1.5以上的jvm中是错的:

iconst_5 ;设定数组size
anewarray java/lang/String ;相当于 new String[5]
dup ;复制数组引用供此段代码执行后的代码使用
ldc "Hello" ;将要替换的数组元素的对象压入栈顶
iconst_0 ;指定目标元素的下标是0
aastore ;替换元素

经过试验发现,数组元素替换的压栈顺序错了,正确的应该如下:

...
dup
iconst_0
ldc "Hello"
aastore

如果运行书上的代码,jvm会在企图从栈顶pop出一个String对象的时候,发现拿到的东西是个int,于是报错。


Exception:
.throws语句 是可选的,jvm会抛出所有没有本地截取的异常,不论.throws是否明示了这个异常类型。但.throws有积极的设计意义:对于一个代码正确系统,一个方法只应该抛出.throws里注明的异常,其上层caller(如另一个方法)应该建立在这个目标方法只会抛出.throws所注异常的假设之上。在一个方法里尝试捕捉一些它自己不知道的异常是诡异的,是违反“最小惊讶原则”的。


invokeinterface:
有三点需要注意:
1. 其最后一个参数是invoke该方法需要使用的栈槽(stack slot)的数量。假设该方法像这样: myObj.myMethod(long a, int b), 那么所需栈槽为 1(ref)+2(long)+1(int)=4
2. 该参数其实并无太大意义,因为这个数字其实是可以推测的。只是因为jvm bytecode就是这样的,Oolong为了保持对bytecode的直译性,才保留下来。
3. invokeinterface的性能相对invokevirtual要慢上一个数量级,使用应慎重。


Constructor:
在子类的<init>里,对超类的<init>的调用必须采用invokespecial的方式。逻辑很简单,假如你用invokevirtual的方式来调用超类的constructor,那么jvm会采用既有的分发方式进行调用,于是又回回转到子类的<init>里来,性能无限递归循环。


Constant Propagation:
事实上,一个较好的实践是,写java的时候就对所有具不变性质的变量加上final修饰。CPU的寄存器远比内存要快,所以:

;变量x在1的位置,值为32
iload_1 ;push x

总是不及

bipush 32 ;push x

来得快。对x的声明加上final,编译器就可以得到明确的指示,可以按照第二种方式进行优化(即使没有final,编译器也可能完成优化但这只能取决与编译器的人品)。


Optimization:
编译器如javac会将java代码转换为bytecode会做一些优化,在jvm实例加载类的时候会由JIT进一步对bytecode进行优化。Inline究竟在那个阶段发生,Engel没有明确提出,需要进一步学习研究。
Inline在C++里也是一个重要的编译器优化选项,目的是把透明的方法或字段的调用,替换为确定的具体代码,比如:

class Demo {
void method_1() {
method_2();
method_3();
}

void method_2() {
System.out.println("Method_2");
}

void method_3() {
System.out.println("Method_3");
}
}

经过inline优化,会变成:

class Demo {
void method_1() {
System.out.println("Method_2"); //method_2的代码原封不动搬过来
System.out.println("Method_3"); //method_3代码
}

void method_2() {
System.out.println("Method_2");
}

void method_3() {
System.out.println("Method_3");
}
}

当然,优化的结果只是bytecode形式的,这里用java只是方便说明。
inline之后节省的开销有:参数压栈,创建被叫方法栈帧,销毁被叫方法栈帧,等。

由于java有polymorph的特性,inline有时无法实行,因为jvm和编译器无法确定究竟应该copy哪一段代码,是超类的方法,还是子类的覆盖方法。我们能从一定程度上帮助jvm,比如对超类的方法加上final修饰符,则jvm将可以确定inline只会使用这个方法的代码。
对于字段的inline,值得注意的是,Engel在14.3.1章中给出的TeaParty的例子,其对结果的描述至少在JDK5/6中是错的,具体例子可以参考我这篇文章:
[url=http://jeff312.iteye.com/blog/558988]更新常量后,请重新编译你的class[/url]


Thread Lock:
使用线程锁时,如果不明白里面的机制,则不免心惊胆跳。也不奇怪,因为在bytecode级别,锁的获取与释放分别由两个指令来实现:

monitorenter ;获得锁
monitorexit ;释放锁

这两个指令必须成对出现,一旦有遗漏,比如少了一个monitorexit,锁就不能释放给其它线程,如果线程不死,锁就永远被该线程hold住了,变成了所谓‘死锁’-deadlock。所以,java编译器为了避免死锁出现,对这段java代码:

synchronized (obj) {
//做点什么
}

总是编译成:

.catch all from begin to end using handler
begin:

aload_1 ;将锁对象压入栈顶
monitorenter ;将aload_1的对象锁住
;做点什么
monitorexit ;正常释放锁

end:
goto next_code ;继续执行synchronized后的代码

handler:
aload_1
monitorexit ;在非正常跳出代码块时,无论如何都要释放锁

next_code:
;其它代码

因此,在java里面使用锁还是很安全的,Oolong或bytecode里就得时刻小心泄露问题。

多重锁又是如何实现的呢?锁上加锁,不免有些诡异,其实原理很简单,比如下面的java代码:

class LockDemo {
synchronized void mainLock() {
subLock1();
subLock2();
}

synchronized void subLock1() {
//做点什么1
}

synchronized void subLock2() {
//做点什么2
}
}

编译bytecode时,每遇到一个synchronized关键字,就套上一对monitorenter/monitorexit,上面的java代码翻译成Oolong就成了(为清晰起见,我把所有方法的代码都inline到一起了):

;最初,lock_count = 0
aload_0
monitorenter ;mainLock()上锁, lock_count = 1

aload_0
monitorenter ;subLock1()上锁, lock_count = 2
;做点什么1
aload_0
monitorexit ;subLock1()解锁, lock_count = 1

aload_0
monitorenter ;subLock2()上锁, lock_count = 2
;做点什么2
aload_0
monitorexit ;subLock2()解锁, lock_count = 1

aload_0
monitorexit ;mainLock()解锁, lock_count = 0

可见,每执行一次monitorenter,lock_count就增加1;每执行一次monitorexit,lock_count就减少1。
JVM规定,只有当lock_count = 0 时,别的线程才有机会获得锁。因此,在subLock1()和subLock2()之间不可能有其它的线程介入,mainLock()与subLock1()和subLock2()所持都是同一个锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值