类加载机制

一个Java文件从编码完成到最终执行,一般主要包括两个过程

  • 编译
  • 运行

编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
运行,则是把编译生成的.class文件交给Java虚拟机(JVM)执行。
而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
例子,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
在这里插入图片描述
加载:加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。字节码验证,符号引用验证等。
准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,编译时期即放入常量池。
解析:将常量池中的符号引用转为直接引用。
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
初始化:这个阶段主要是对类变量初始化,是执行类构造器的过程。只对static修饰的变量和static静态代码块进行初始化。初始化阶段是执行类构造器clinit()方法的过程。clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的。

  • 类的实例化是指创建一个类的实例(对象)的过程,通常是new的一个过程。
  • 类的初始化是指为类中各个类成员(被static修饰的成员变量,以及static静态代码块)赋初始值的过程,是类生命周期中的一个阶段。

虚拟机什么时候才会加载Class文件并初始化类?
在这里插入图片描述
关于第五条:在JDK7中,新增了java.lang.invoke.MethodHandle方法句柄,其实反射和java.lang.invoke.MethodHandle都是间接调用方法的途径,但java.lang.invoke.MethodHandle比反射更简洁,用反射功能会写一大堆冗余代码。

init方法和clinit方法的不同:

1、init()和clinit()方法执行时机不同
init()是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init()方法,而clinit()是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit()方法。

2、init()和clinit()方法执行目的不同
init() is the (or one of the) constructor(s) for the instance, and non-static field initialization.
clinit() are the static initialization blocks for the class, and static field initialization.
init()是instance实例构造器,对非静态变量解析,代码块初始化,实例构造器init()则会被虚拟机调用多次
而clinit()是class类构造器对静态变量,静态代码块进行初始化。类构造器clinit()最多会被虚拟机调用一次

类构造器clinit()与实例构造器init()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器clinit()执行之前,父类的类构造clinit()执行完毕。由于父类的构造器clinit()先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器clinit()对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器clinit()。
虚拟机会保证一个类的类构造器clinit()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器clinit(),其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行clinit()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行clinit()方法,因为在同一个类加载器下,一个类型只会被初始化一次。

为什么懒汉式单例是线程安全的,不是延迟加载?

singleton 作为类成员变量的new实例化发生在类Singleton 类加载的初始化阶段,初始化阶段是执行类构造器clinit() 方法的过程。

clinit() 方法是由编译器自动收集类中的所有类变量(static)的赋值动作和静态语句块(static{})块中的语句合并产生的。因此,private static Singleton singleton = new Singleton();也会被放入到这个方法中。

虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。需要注意的是,其他线程虽然会被阻塞,但如果执行clinit()方法的那条线程退出clinit()方法后,其他线程唤醒后不会再次进入clinit()方法。同一个类加载器下,一个类型只会初始化一次。

为什么枚举是线程安全的,还能保证序列化单例?
由反编译后的代码可知,INSTANCE被声明为 static 的,可以知道虚拟机会保证一个类的clinit() 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。

Java规范中规定,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,以下面枚举为例,序列化的时候只将 INSTANCE这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值