类加载的执行过程

代码经过编译后成为了.class文件,类加载子系统负责从文件系统中或是网络中加载.class文件,加载后的class类信息放在方法区,类加载器只负责加载,是否运行由执行引擎决定
在这里插入图片描述
在这里插入图片描述

加载
预加载

虚拟机启动的时候会加载JAVA_HOME/lib/rt.jar中的.class文件例如java.lang.*,java.util*,java.io.*等

public class LoadTest {
    public static void main(String[] args) {
        System.out.println("输出预加载类");
    }
}

添加启动参数
在这里插入图片描述
启动main方法
在这里插入图片描述

运行时加载

虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
加载的时候先获取.class的二进制流,然后将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中,最后在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的
.class的二进制流可以从zip包中获取(jar、ear、war),从网络中获取(典型应用就是Applet),运行时计算生成(动态代理),由其他文件生成(jsp),从数据库中读取,这种场景比较少见

连接

连接包括验证、准备、解析三个过程

验证

.class文件未必要从Java源码编译而来,可以使用任何途径产生,验证阶段是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

验证阶段主要做了:
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证

准备

准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配
这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如 public static int value = 6,value在准备阶段过后是0而不是6,给value赋值为6的动作将在初始化阶段才进行;但是如果被final修饰 public static final int value = 6 ,会在准备阶段,虚拟机就会给value赋值为6。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

符号引用
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("HellowWorld");
    }
}

将字节码文件HelloWorld .class反编译后可以看到它的常量池信息和方法信息:javap -v HelloWorld > HelloWorld.txt

在这里插入图片描述

在这里插入图片描述

符号引用包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符,它和虚拟机的内存布局是没有关系的,引用的目标不一定已经加载到了内存中。

直接引用

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。

初始化

类的初始化阶段是类加载过程的最后一个步骤,之前只有加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。

初始化阶段就是执行类构造器clinit()方法的过程。clinit()并不是程序员在Java代码中直接编写的方法,
它是Javac编译器生成的,clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

public class TestClinit { 
    //i在静态代码块后定义
    static { i = 0; // 给变量赋值可以正常编译通过
        System.out.print(i); // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

clinit()方法与类的构造方法不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的clinit()方法执行前,父类的clinit()方法已经执行完毕。因此在Java虚拟机中第一个被执行的clinit()方法的类型java.lang.Object。

clinit()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法。接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法。但是接口的clinit()方法不需要先执行父接口的clinit()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。

Java虚拟机必须保证一个类的clinit()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程clinit()方法执行完毕。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值