javac
编译 Java 程序成为 .class
文件后,还需要 Java 虚拟机识别 .class
后缀的文件,并且解析它的指令,然后才会被操作系统识别从而能调用操作系统上的函数。Clojure
、JRuby
、Groovy
等,编译到最后都是 .class
文件,Java 语言的维护者,只需要控制好 JVM 这个解析器,就可以将这些扩展语言无缝的运行在 JVM 之上了。The Java Virtual Machine Specification
-- JVM 规范:定义了 .class 文件的结构、加载机制、数据存储、运行时栈等诸多内容,最常用的 JVM 规范实现就是 Hotspot VM
。The Java Language Specification
-- Java 语言规范:定义 Java 语法规范,比如 switch、for、泛型、lambda 等。HelloWorld.java
,它遵循的就是 Java 语言规范。其中,它调用了 System.out
等模块,也就是 JRE 里提供的类库。public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
javac HelloWorld.java
进行编译后,会产生 HelloWorld
的字节码。javap -v -p HelloWorld
来查看字节码文件内容。下面是System.out.println("Hello World");
的字节码内容:...
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <Hello World>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return
...
getstatic
、ldc
、invokevirtual
、return
等,就是 opcode。hexdump
看一下字节码的二进制内容,与以上字节码对应的二进制,就是下面这几个数字(十六进制展示):
b2 00 02 12 03 b6 00 04 b1
对应关系:0xb2 getstatic // 获取静态字段的值
0x12 ldc // 常量池中的常量值入栈
0xb6 invokevirtual // 运行时方法绑定调用方法
0xb1 return // void 函数返回
b2 00 02
,就代表了 getstatic #2 <java/lang/System.out>
。.class
文件的时候,实际上就相当于启动了一个 JVM 进程。这些 .class 文件会被加载、存放到 元数据(metaspace)
中,执行引擎将会通过 混合模式
执行这些字节码。然后 JVM 会翻译这些字节码为操作系统相关的函数,它有两种执行方式:opcode + 操作数
翻译成机器代码;.class
文件的黑盒存在,输入字节码,调用操作系统函数。-XX:PermSize
和 -XX:MaxPermSize
等参数调优,已经没有了意义。但大体上,比较重要的内存区域是固定的。constant_pool
,是每个类每个接口所拥有的,(如字节码中的 getstatic #2 <java/lang/System.out>
)。这部分数据在方法区,也就是元数据区。returnAddress
类型的值就是指向特定指令内存地址的指针。String.intern
相关的运行时常量池也放在这里。这个区域有大小限制,很容易造成 JVM 内存溢出,从而造成 JVM 崩溃。Perm 区在 Java 8 中已经被彻底废除,取而代之的是 Metaspace。原来的 Perm 区是在堆上的,现在的元空间是在非堆上的。-XX:MaxMetaspaceSize
来控制大小。-XX:MaxDirectMemorySize
来控制。一个 .class
文件,需要经历 ”加载、验证、准备、解析、初始化“ 的过程,然后才会被 JVM 的执行引擎执行。
.class
文件,加载到 Java 的方法区内。java.lang.VerifyError
错误。比如,一些低版本的 JVM,是无法加载一些高版本的类库的。// 如果没有为类变量赋值,它会有一个默认的初始值。
public class A {
static int a ;
public static void main(String[] args) {
// 输出 0
System.out.println(a);
}
}
// 如果没有给局部变量赋初始值,是不能使用的。
public class B {
public static void main(String[] args) {
int b ;
// 编译报错
System.out.println(b);
}
}
java.lang.NoSuchFieldError
根据继承关系从下往上,找不到相关字段时的报错。java.lang.IllegalAccessError
字段或者方法,访问权限不具备时的错误。java.lang.NoSuchMethodError
找不到相关方法时的错误。public class A {
static int a = 0 ;
static {
a = 1;
b = 1;
}
static int b = 0;
public static void main(String[] args) {
// 输出 1
System.out.println(a);
// 输出 0
System.out.println(b);
}
}
static {
b = b + 1;
}
static int b = 0;
<cinit>
与 <init>
public class A {
static {
System.out.println("1");
}
public A(){
System.out.println("2");
}
}
public class B extends A {
static{
System.out.println("a");
}
public B(){
System.out.println("b");
}
public static void main(String[] args){
// static 代码块只会执行一次,
// 对象的构造方法执行两次。
A ab = new B();
ab = new B();
}
}
/**
输出:
1
a
2
b
2
b
*/
rt.jar
、resources.jar
、charsets.jar
等。-Xbootclasspath
参数可以完成指定操作。lib/ext
目录下的 jar 包和 .class
文件。同样的,通过系统变量 java.ext.dirs
可以指定这个目录。URLClassLoader
。.class
文件,自定义的 Java 类会首先尝试使用这个类加载器进行加载。ClassLoader#loadClass
方法,可以知道,首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。同时,应当注意到,这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效:如果出现一些业务需求比如 “加载一个远程的 .class
文件” 或 “加密 .class
文件”,那么这时候就需要自定义一个新的类加载器。
所以,为了支持一些自定义加载类多功能的需求,Java 设计者作出了一些妥协,即可以打破双亲委派机制。
.class
文件,而两个应用可能会依赖同一个第三方的不同版本,它们是相互没有影响的。Class.forName("com.mysql.jdbc.Driver")
,用于加载所需要的驱动类。MySQL 通过在 META-INF/services
目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。所以,即使删除了 Class.forName
这一行代码,也能加载到正确的驱动类。rt.jar
的。它们的类加载器是 Bootstrap ClassLoader,也就是最上层的那个。而具体的数据库驱动,却属于业务代码,这个启动类加载器是无法加载的。在数据库驱动加载的源码中,应用启动的时候,把当前的类加载器设置成了线程的上下文类加载器;而对于一个刚刚启动的应用程序来说,当前的加载器就是启动 main 方法的 Application ClassLoader。使用它来加载第三方驱动,是没有什么问题的。java.util.ServiceLoader
类进行动态装载。这种方式,同样打破了双亲委派的机制。HashMap
为例。当 Java 的原生 API 不能满足需求时,比如要修改 HashMap
类,就必须要使用到 Java 的 endorsed
技术。这时候就需要将自己写的 HashMap 类,打包成一个 jar 包,然后放到 -Djava.endorsed.dirs
指定的目录中。注意类名和包名,应该和 JDK 自带的是一样的。但是,java.lang
包下面的类除外,因为这些都是特殊保护的。-Djava.endorsed.dirs
指定的目录下的 jar 包,会比核心类库 rt.jar
中的文件,优先级更高,可以被最先加载到。分析字节码的小工具
javap
javap
是 JDK 自带的反解析工具。它的作用是将 .class 字节码文件解析成可读的文件格式。在使用 javap 时一般会添加 -v 参数,尽量多打印一些信息。同时也会使用 -p 参数,打印一些私有的字段和方法。
javap -p -v HelloWorldjavac -g:lines
强制生成 LineNumberTable
。javac -g:vars
强制生成 LocalVariableTable
。javac -g
生成所有的 debug
信息。jclasslib
jclasslib
是一个图形化的工具,能够更加直观的查看字节码中的内容。它还分门别类的对类中的各个部分进行了整理,非常的人性化。// A.java
class B {
private int a = 1234;
static long C = 1111;
public long test(long num) {
long ret = this.a + num + C;
return ret;
}
}
public class A {
private B b = new B();
public static void main(String[] args) {
A a = new A();
long num = 4321 ;
long ret = a.b.test(num);
System.out.println(ret);
}
}
private B b = new B()
时,就会触发 B 类的加载。A 和 B 会被加载到元空间的方法区,进入 main 方法后,将会交给执行引擎执行。这个执行过程是在栈上完成的,其中有几个重要的区域,包括虚拟机栈、程序计数器等。javap -p -v A
javap -p -v B
1: invokespecial #1 // Method java/lang/Object."<init>":()V
#2 = Fieldref #6.#27 // B.a:I
...
#6 = Class #29 // B
#27 = NameAndType #8:#9 // a:I
...
#8 = Utf8 a
#9 = Utf8 I
:I
这样特殊的字符。它们也是有意义的,如果经常使用 jmap 这种命令,应该不会陌生。大体包括:B
基本类型 byte
C
基本类型 char
D
基本类型 double
F
基本类型 float
I
基本类型 int
J
基本类型 long
S
基本类型 short
Z
基本类型 boolean
V
特殊类型 void
L
对象类型,以分号结尾,如 Ljava/lang/Object;
[Ljava/lang/String;
数组类型,每一位使用一个前置的 [
字符来描述public long test(long);
descriptor: (J)J
flags: ACC_PUBLIC
Code:
stack=4, locals=5, args_size=2
0: aload_0
1: getfield #2 // Field a:I
4: i2l
5: lload_1
6: ladd
7: getstatic #3 // Field C:J
10: ladd
11: lstore_3
12: lload_3
13: lreturn
LineNumberTable:
line 13: 0
line 14: 12
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 this LB;
0 14 1 num J
12 2 3 ret J
main
线程会拥有两个主要的运行时区域:Java 虚拟机栈和程序计数器。其中,虚拟机栈中的每一项内容叫作栈帧,栈帧中包含四项内容:局部变量报表、操作数栈、动态链接和完成出口。字节码指令,就是靠操作这些数据结构运行的。下面我们看一下具体的字节码指令。
0: aload_0
1: getfield #2
4: i2l
5: lload_1
6: ladd
7: getstatic #3
10: ladd
11: lstore_3
12: lload_3
13: lreturn
11: lstore_3
,它首先把变量存放到了变量报表,然后又拿出这个值,把它入栈。会有这种多此一举的操作的原因就是:函数定义了 ret 变量。JVM 不知道后面还会不会用到这个变量,所以只好傻瓜式的顺序执行。
如果把程序稍微改动一下,直接返回这个值: public long test(long num) {
return this.a + num + C;
}
0: aload_0
1: getfield #2 // Field a:I
4: i2l
5: lload_1
6: ladd
7: getstatic #3 // Field C:J
10: ladd
11: lreturn