近期学习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中是错的,具体例子可以参考我这篇文章:
更新常量后,请重新编译你的class
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()所持都是同一个锁。
分享到:
相关推荐
Java虚拟机(JVM)是Java程序运行的基础,它是一个抽象的计算机系统,负责执行Java字节码。本文将深入探讨JVM的工作...这本《JVM工作原理学习笔记》应包含了这些内容的详细讲解,对于学习和提升JVM相关知识极具价值。
TL学院诸葛老师JVM架构师课程笔记TL学院诸葛老师JVM架构师课程笔记TL学院诸葛老师JVM架构师课程笔记TL学院诸葛老师JVM架构师课程笔记TL学院诸葛老师JVM架构师课程笔记TL学院诸葛老师JVM架构师课程笔记TL学院诸葛老师...
《JVM内存管理学习笔记》 在Java世界中,JVM(Java Virtual Machine)是运行所有Java应用程序的核心。深入理解JVM内存管理对于优化程序性能、预防和解决内存泄漏问题至关重要。本文将从JVM内存模型、内存区域划分、...
### JVM学习笔记(一) #### 一、JVM概述与工具使用 JVM(Java Virtual Machine)是Java语言的核心组成部分之一,它为Java程序提供了一个跨平台的运行环境。本篇学习笔记主要介绍如何利用一系列工具来查看和监控JVM...
这个资料包不仅涵盖了理论知识,还包含个人的学习笔记,对于学习和掌握JVM的各个方面都将大有裨益。无论是初学者还是经验丰富的开发者,都可以从中找到提升自己技能的宝贵资源。通过深入学习和实践,可以更好地理解...
"jvm视频及笔记"这个资源显然是一份全面学习JVM的材料,结合了视频教程和书面笔记,帮助学习者深入理解JVM的工作原理及其在实际开发中的应用。 JVM的学习可以从以下几个重要的知识点开始: 1. **JVM架构**:JVM...
本文将深入探讨JVM中的访问控制器,主要基于“java之jvm学习笔记十一(访问控制器)-源码”这一主题,以及相关的源码分析。 首先,我们得了解Java的安全模型。Java安全模型基于一种称为安全管理器(SecurityManager)...
这本"Java并发编程学习笔记"可能是作者在深入研究Java并发特性、工具和最佳实践过程中积累的心得体会。下面,我们将根据这个主题,探讨一些关键的Java并发编程知识点。 1. **线程与进程**:在多任务环境中,线程是...
Java虚拟机(JVM)是Java程序的核心组成部分,它负责执行字节码并...理解JVM的类加载机制对于优化程序性能、解决类加载问题以及深入学习Java运行机制至关重要。开发者应当掌握这些概念,以便更好地编写和调试Java代码。
在讨论Java编程思想学习笔记时,首先需要了解的是Java语言的平台无关性,而这一特性正是通过Java虚拟机(JVM)得以实现的。JVM作为Java程序设计的关键组成部分,对于Java开发人员来说是必须掌握的基础知识。在该学习...
这个“java之jvm学习笔记五(实践写自己的类装载器)”很可能是对这一主题的详细探讨。 类装载器在Java中的主要职责是动态加载类到JVM中。Java的类装载器分为三个基本层次:启动类装载器(Bootstrap ClassLoader)、...
本篇JVM学习笔记主要涵盖了以下几个核心知识点: 1. **运行时数据区**: - **程序计数器**:记录当前线程执行的字节码的行号,用于线程恢复执行时跳转到正确位置。 - **Java虚拟机栈**:每个方法执行时创建的栈帧...
这份“JVM的学习笔记PDF版”应该包含了关于JVM的详细信息,帮助学习者深入理解这个复杂的系统。JVM允许Java代码跨平台运行,通过解释器、类加载器、垃圾收集器等组件实现“一次编写,到处运行”的理念。 1. **JVM...
JVM学习笔记(缓慢更新).md
JVM的学习可以从其基本结构、代码编译和执行过程,以及内存管理和垃圾回收机制三个方面进行深入探讨。 首先,JVM的基本结构分为逻辑结构和物理结构。逻辑结构主要包括Java源码编译器、JVM执行引擎、类加载器等组件...
JVM性学习笔记-基本原理,内存模型,JVM参数设置,类加载器原理,JDK自带工具
本文将根据"JVM性能学习笔记思维导图"的主题,详细阐述JVM的主要组成部分,性能调优的关键点以及相关的工具与实践策略。** 1. **JVM结构与内存模型** - **类装载器(ClassLoader)**:负责加载类文件,确保类在运行...
### 马士兵JVM调优笔记知识点梳理 #### 一、Java内存结构 Java程序运行时,其内存被划分为几个不同的区域,包括堆内存(Heap)、方法区(Method Area)、栈(Stack)、程序计数器(Program Counter Register)以及...
Java2编程详解学习笔记主要涵盖了Java语言的基础知识、设计原理以及安装和启动JDK的步骤。下面是对这些知识点的详细阐述: 1. **Java能做什么** - **Applets**:Java小程序,可以在网页中嵌入,提供交互体验。 - ...