Java从编码到执行:
已知有个java文件叫Demo.java,执行
javac命令后生成Demo.class。再执行java命令,就会使用ClassLoader加载到内存,同时Java相关类库也会加载到内存。然后调用字节码解释器或即时编译器进行编译。最后调用执行引擎进行程序执行。
关于javac的过程,会专门写一篇JVM学习笔记 - 00 类编译来描述javac过程。
类加载过程主要有三个步骤Loading -> Linking -> Initializing,其中Linking又包含了Verification -> Preparation -> Resolution三个步骤,如下图:
Loading
双亲委派
BootstrapClassLoader加载lib/rt.jar,lib/charset.jar等核心类;
ExtClassLoader加载扩展包jre/lib/ext/*.jar,或由-Djava.ext.dirs指定;
AppClassLoader加载classpath指定内容;
Costom Classloader为自定义类加载器。
加载一个类的过程如下:
- 从自定义
ClassLoader中查找该类是否加载,已加载则停止;未加载则去AppClassLoader,查找; - 从
AppClassLoader中查找该类是否加载,已加载则停止;未加载则去ExtClassLoader,查找; - 从
ExtClassLoader中查找该类是否加载,已加载则停止;未加载则去BootstrapClassLoader,查找; - 从
BootstrapClassLoader中查找该类是否加载,已加载则停止;未加载则判断该类是否由该加载器加载,若是则直接加载,若不是则去ExtClassLoader进一步判断; - 判断该类是否由
ExtClassLoader加载,若是则加载,若不是则去AppClassLoader进一步判断; - 判断该类是否由
AppClassLoader加载,若是则加载,若不是则去自定义ClassLoader进一步判断; - 判断该类是否由
ClassLoader加载,若是则加载,若不是则抛出ClassNotFoundException异常;
注意: 这几个类加载其互相不为继承关系,每个ClassLoader都有一个private final ClassLoader parent;属性,表示父ClassLoader。
ClassLoader源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
···
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
在上述步骤中,查找类是否被加载由方法findLoadedClass()来处理;去父加载器加载由parent.loadClass()来处理;当加载器为BootstrapClassLoader时,parent为null,直接执行findBootstrapClassOrNull()方法;具体的加载过程由findClass()来处理。
自定义类加载器 // TODO
自定义类加载器只需要继承ClassLoader,并重写findClass()方法即可,一个简单的例子如下:
public class TestClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("test/", name.replace(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = fis.read()) != 0) {
baos.write(b);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
}
Hello World如下:
public class HelloWorld {
public void print() {
System.out.println("Hello World");
}
}
测试代码如下:
public static void main(String[] args) throws Exception {
ClassLoader l = new TestClassLoader();
Class clazz = l.loadClass("com.wzy.test.HelloWorld");
HelloWorld helloWorld = (HelloWorld) clazz.newInstance();
helloWorld.print();
}
混合执行 编译执行 解释执行
Java默认使用混合模式,也就是混合使用解释器(bytecode intepreter)和热点代码编译(JIT, Just In-Time Compiler)。起始节点采用解释执行,然后进行热点代码检测。多测被调用的方法以及多次被调用的循环会被进行编译。
参数设定:
-Xmixed - 混合模式
-Xcomp - 编译模式
-Xint - 解释模式
Linking
Verification
验证文件是否符合JVM规定,详细可以查看文章JVM学习笔记 - 00 类编译 // TODO
Preparation
静态成员变量赋默认值,例如:int为0,double为0.0,引用类型为null等。但是,被final修饰的static变量,例如public static int value = 123;会直接赋值为123。
Resolution
将类、方法、属性等符号引用解析为直接引用,常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用。
符号引用: 与虚拟机布局无关,由于java类编译成class文件时,虚拟机还不知道引用类的地址,所以就以无歧义的符号来描述引用目标。这里有个简单的例子:
public class Test {
private static int i_static_1;
private static int i_static_2 = 5;
private int i1;
private int i2 = 10;
}
反编译后
public class com.wzy.springboot.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#19 // com/wzy/springboot/Test.i2:I
#3 = Fieldref #4.#20 // com/wzy/springboot/Test.i_static_2:I
#4 = Class #21 // com/wzy/springboot/Test
#5 = Class #22 // java/lang/Object
#6 = Utf8 i_static_1
#7 = Utf8 I
#8 = Utf8 i_static_2
#9 = Utf8 i1
#10 = Utf8 i2
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 <clinit>
#16 = Utf8 SourceFile
#17 = Utf8 Test.java
#18 = NameAndType #11:#12 // "<init>":()V
#19 = NameAndType #10:#7 // i2:I
#20 = NameAndType #8:#7 // i_static_2:I
#21 = Utf8 com/wzy/springboot/Test
#22 = Utf8 java/lang/Object
常量池中带有Utf8的其实就是符号引用。
直接引用: 和虚拟机布局相关,不同虚拟机对于相同符号引用翻译出来的直接引用不同。如果有了直接引用,那目标一定被加载到了内存中。 直接引用可以是:
- 直接指向类、方法和属性的指针;
- 相对偏移量(指向实例变量、实例方法的直接引用都是偏移量);
- 一个间接定位到对象的句柄。
Initializing
调用初始化代码,即类构造器<clinit>,给静态成员变量赋初始值。举个无聊的例子:
public class Test {
public static void main(String[] args) {
System.out.println(T.count);
}
}
class T {
public static T t = new T(); // null
public static int count = 2; // 0
private T() {
count ++;
//System.out.println("--" + count);
}
}
这段代码打印结果为2。Preparation阶段,t为null,count为0。Initializing阶段,new T()调用头早方法,count变为1,紧接着count被赋初始值,又变回了2。