在看完《深入了解Java虚拟机》对象创建和类加载之后,想要连贯的对一段代码的执行过程进行一个追踪,以下是目前的个人理解。
先是一段的简单的HelloWorld代码
public class HelloWorld {
public static void main(String[] args) {
String string = "HelloWorld";
System.out.println(string);
}
}
整个的代码执行过程可以分为三个阶段:
- 代码编译
- 类加载
- 类执行
代码编译
代码的编译不是很了解,这里盗用网上一个博主的图片:
//TODO
编译工具为javac,编译的结果是HelloWorld.class, 接下来说下HelloWorld.class文件的结构和内容。
字节码截图如下:
以若干字节为单位,U4即表示4个字节的长度,依次类推。
- 前U4是魔数,用以确定该class文件是否能被虚拟机接受,表示这是一个class文件。
- 接下来U4是版本号,U3,U4表示次版本号,U5,U6表示主版本号,高版本可以兼容低版本,反之不行。0x00000033即表示51.
接下来是常量池信息:
-
接下来U2,常量池计数器,0x22,即常量池内有34个常量,即标注部分。
- 常量池中每一个常量都是以一个U1的类型标志位开始的。我们可以借助工具javap来分析class文件中的信息。
Classfile /D:/IDEA_workspace/juc/target/classes/com/zhangcf/test/impl/HelloWorld.class
Last modified 2018-8-31; size 576 bytes
MD5 checksum 516c2b5a8758322677a3f5dec4ef0d15
Compiled from "HelloWorld.java"
public class com.zhangcf.test.impl.HelloWorld
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // HelloWorld
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/zhangcf/test/impl/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/zhangcf/test/impl/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 HelloWorld
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/zhangcf/test/impl/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.zhangcf.test.impl.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhangcf/test/impl/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String HelloWorld
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
以上可以分为两个部分,第一个不是表示class文件中各位置的信息,后面大括号内的内容表示类中的方法,这里显示了无参构造和main方法。
整个class文件的结构可以总结为以下的图示:
其中
字段表(field_info)表示类中各字段的信息,包括访问标识,字段索引,描述符索引和属性表。
方法表(method_info)表示类中的方法,包含访问标识,名称索引,描述符索引,参数数量,参数属性信息等属性表,与字段表不同的是,方法表中的属性表包含Code属性,表示方法的方法实体。
class文件的最后固定是属性表(attrbute_info),关于属性表:
- 属性表不仅存在于class文件的最后,字段表,方法表和方法表中的Code属性中也存在属性表,也即是属性表中也是可以存在属性表。
- 属性表的长度是不固定的,不同的属性,属性表的长度是不同的。
类加载
类加载简单来说就是把class文件加载到虚拟机中,先说下类加载器:
类加载器
Java中的类加载器可以说有四种,分别是启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension Classloader), 应用程序类加载器(Application ClassLoader)和自定义类加载器。
其中,第一种是JVM内置的,代码中是不能显式使用的,另外的三种类加载器都需要继承ClassLoader类,ClassLoader类中的方法如下:
启动类加载器用于加载{JAVA_HOME}/lib/rt.jar;
扩展类加载器主要用于加载{JAVA_HOME}/lib/ext下的jar包;
应用程序类加载器用于加载用户类路径下的class文件或jar包,为系统默认类加载器。
自定义类加载器中一般只需要重写loadClass(...)方法,若要使用上一级类加载器,调用super.loadClass(...)。
双亲委派原则
在一个类的加载中,它会首先被上层的类加载器所加载,若未找到该类文件,则交还给下级类加载器加载。
双亲委派原则的好处:
- 避免重复加载,一次基础类加载之后,下次再需要加载的时候,直接使用即可,无需重复加载
- 安全因素,这样的话,java的核心api不会被随意替换,可以防止恶意替换。
怎么样加载与系统类同名的类(打破双亲委派原则)?
该类位于D:\com\zhangcf\ClassPart\内
package com.zhangcf.ClassPart;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) {
File file = new File("D:\\com\\zhangcf\\ClassPart\\" + name.substring(name.lastIndexOf(".") + 1) + ".class");
URI uri = file.toURI();
String myPath = "D:\\com\\zhangcf\\ClassPart\\" + name.substring(name.lastIndexOf(".") + 1) + ".class";
byte[] bytes = null;
Path path = null;
try{
path = Paths.get(uri);
bytes = Files.readAllBytes(path);
} catch (IOException e) {
e.printStackTrace();
}
Class cl = defineClass(name,bytes,0,bytes.length);
return cl;
}
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> cla = myClassLoader.findClass("com.zhangcf.ClassPart.String");
try {
Object object = cla.newInstance();
Method method = cla.getMethod("print");
method.invoke(object);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
类加载的过程
类加载有7步组成。分别是 加载,验证,准备,解析,初始化,使用,卸载。
加载:把class文件加载到虚拟机中,可以是本地文件,也可以是网络文件,也可以是在运行时生成,如动态代理。并在内存中生成一个Class对象,HotSpot虚拟机是将该对象存在方法区内。
验证:确保class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机。分为四个验证步骤:文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:为类变量分配内存,并设置初始零值。这些变量都将在方法区中进行内存分配。
解析:将常量池内的符号引用替换为直接引用的过程。
关于符号引用和直接引用:
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
初始化:类加载过程的最后一步,根据程序员主观计划去初始化类变量和其他资源。