java执行流程
从图中可以看出,java文件先会被编译成.class字节码文件,由jvm的类加载器加载到内存中,通过字节码解释器或即时编译器编译成汇编语言在操作系统上执行
Class File Format
整个class其实就是二进制的字节流,供jvm解析
整个class文件的构成,基本可以分为几个部分:
- magic—魔数(不同类型后缀的文件,文件前缀都不一样,称为魔数,.class文件的魔数前缀是CAFE BABE,占4个字节)
- minor_version:jvm小版本号
- major_version:jvm大版本,1.8版本主版本是52(0034占两个字节)
- constant_count:常量池长度
- access_flag:代表类的声明类型(public、final、abstract等)
- this_class:表示当前类
- super_class:表示当前类的父类
- interfaces_count:实现的接口数量
- interfaces:接口具体索引
- fields_count:类有哪些属性数量
- fields:类有哪些属性
- methods_count:方法数量
- methods:方法的索引
- attributes_count:属性数量
- attributes:属性表集合,附加项有一项是code,记录类的方法体中的具体代码经过javac编译后的字节码指令
Class Loading Linking Initializing
类加载的过程主要分为三部分,加载、连接(连接又包括验证、准备、解析)、初始化
加载
本阶段jvm会通过类的完全路径名找到具体的.class字节流文件(用到了双亲委派模型),将字节流文件的内容放入方法区,并转译成方法区可执行的数据结构。之后在内存中生成一个Class对象,作为方法区这个类各种数据结构的访问入口。
想获取Class对象很简单,比如String.class、String.getClass()都可以拿到对应的Class对象。Class对象可以理解为是单例的,针对一个类只会有一个Class对象。
Class对象比较特殊,它也在堆中
连接
- 验证:校验字节码内容是否正确,格式是否符合JVM规定,比如.class文件开头的魔数是cafe babe,如果有问题会报错
- 准备:这时候给类变量分配内存(static修饰的变量)和默认值,这里的默认值是0值。例如类中变量定义为
public static int value = 123;
准备阶段赋的初始值是0,value的值真正变为123是在之后的初始化阶段
- 解析:将加载到常量池的类的各种符号引用替换为直接引用
初始化
类加载的最后一步,真正执行类中定义的程序代码,静态变量赋值(上述value会真正赋值为123,执行静态块的代码)
主要是执行类构造器()方法的过程,给静态变量赋初始值。具体描述可参考“深入理解Java虚拟机”第七章225页内容
类加载器和双亲委派模型(加载阶段)
类加载器分为四种:Bootstrap类加载器(启动类加载器)、扩展类加载器、应用程序类加载器和自定义加载器。java中只能拿到除启动类加载器之外的其他类加载器,因为启动类加载器是用C++实现的。可查看Launcher类确定不同类加载器的加载范围(类加载器是Launcher的内部类)
加载过程
为什么使用双亲委派模型?
为了安全。即防止内存中出现多份同样的字节码。
从反向思考这个问题,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证,而且如果不使用这种双亲委派模型将会给虚拟机的安全带来隐患。所以,要让类对象进行比较有意义,前提是他们要被同一个类加载器加载。
打破双亲委派模型在什么时候发生
- 实现:重写loadClass()
- 何时打破
- jdk1.2之前,自定义类加载器都必须重写loadClass
- ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
- 热启动、热部署
- osgi tomcat 都有自己的模块指定classLoader(可以加载同一类库不同版本)
自定义类加载器(加载阶段)
类加载器重要方法
- loadClass(“类全路径名”)—加载类
- findLoadedClass(“类全路径名”)—类加载器查询维护的表,判断该类是否加载过
- findClass(“类全路径名”)—查找类文件并加载到内存,是自定义类加载器需要重写的方法
相关源码
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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定义类加载器只需继承ClassLoader类,实现findClass方法即可(采用模板方法模式,可能用到方法defineClass)。
扩展
有些对class编译文件要求安全性的场景,可对编译后的class文件进行加密,之后自定义类加载器,重写findClass方法中进行解密之后加载进内存
如果有多个自定义类加载器,如何指定父加载器?可通过构造方法。默认是应用程序类加载器
lazyloading(加载阶段)
示例
public static class P {
final static int i = 8;
static int j = 9;
static {
System.out.println("P");
}
}
public static class X extends P {
static {
System.out.println("X");
}
}
public static void main(String[] args) {
//1
P p;
//2
X x = new X();
//3
System.out.println(P.i);
//4
System.out.println(P.j);
}
- 不会打印"P",“X”
- 会打印"P",“X”
- 不会打印"P",“X”
- 会打印"P"
Jvm的编译(混合编译、解释编译、即时编译)(加载阶段)
检测热点代码
-XX:CompileThreshold = 10000