聊聊类加载的过程

引子

废话不多说,先看看代码
各种代码块的执行的顺序问题
父子类静态代码块、示例代码块、构造方法、以及局部代码块的顺序

public class Parent {
    public static String s1 = "abc";
    public static final String s2 = "def";
    public final  String s3 = "ghi";
    private String s4 = "123";
    static {
        System.out.println("Parent static block");
    }

    public Parent() {
        System.out.println("Parent Constructor");
    }

    {
        System.out.println("Parent 实例代码块");
    }
}
public class Child  extends  Parent{
    public static String childS = "child";

    static {
        System.out.println("Child static bolock");
    }

    public Child(){
        System.out.println("Child Constructor");
    }

    {
        System.out.println("Child 实例代码块");
    }

public class Main {
    public static void main(String[] args) {
        new Child();
        {
            System.out.println("局部代码块");
        }
    }
}

在这里插入图片描述

表明的现象就是
总体的是静态代码块 》 实例代码块》构造方法代码块 》局部代码块

只要是构造方法执行,必然在构造方法执行之前自动执行”实例语句块“中的代码。构造方法执行一次,实例代码块执行对应一次。

  • 父类的静态代码块、静态变量(按照书写顺序)

    具体的这里面的静态代码块和静态变量加载的顺序是按照书写先后顺序

  • 子类的静态代码块、静态变量

  • 父类的成员变量值初始化(static修饰的变量)

  • 父类的成员变量赋值 + 实例代码块 + 父类的成员构造代码块

  • 子类的成员变量值初始化(static修饰的变量)

  • 子类的成员变量赋值 +实例代码块+ 子类的构造代码块

  • 局部代码块

这些的执行顺序就是和类加载机制有关的,具体的解释可以详细参照类加载的初始化阶段。

类加载

根据下图了解类加载和类的生命周期
在这里插入图片描述

而解析阶段则不一定: 它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定) 这里目前我是在是理解不了啊…

类加载器在JVM中所处的层次结构
在这里插入图片描述

加载的类信息存放于一块成为方法区的内存空间

加载

Java虚拟机把描述类的数据从Class文件加载到内存, 并对数据进行 校验、 转换解析和初始化, 最终形成可以被虚拟机直接使用的Java类 型, 这个过程被称作虚拟机的类加载机制

但是上面所说的class文件不单单是.class文件结尾的文件,还有一些其他的类型

这里说的class文件可以是扩展从任何地方读取都可以,也可以再运行时动态生成,是以下的任意一种方式。

  • 从ZIP压缩包中读取, 这很常见, 最终成为日后JAR、 EAR、 WAR
    格式的基础。

  • 从网络中获取, 这种场景最典型的应用就是Web Applet。

  • 运行时计算生成, 这种场景使用得最多的就是动态代理技术, 在
    java.lang.reflect.Proxy中, 就是用了ProxyGenerator.generateProxyClass()
    来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。

    字节码技术,就是一种动态代理技术,相比较于静态代理而言,同样的都是在逻辑上进行增强,但是具体的代码不用像是静态代理一样自己去写代码增强,而是可以为我们动态生成(这里的理解比较浅薄)

  • 由其他文件生成, 典型场景是JSP应用, 由JSP文件生成对应的
    Class文件。 (本质上是一个Servlet,运行起来以后,就会把jsp文件编译成一个class文件)

加载的三个阶段

1) 通过一个类的全限定名来获取定义此类的二进制字节流。
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数
据结构。
(方法区是一个虚概念,具体落地看jdk的版本信息。jdk1.7 永久代 1.8是元空间。方法区1.7是在java的JVM(进程中)的内存。1.8 方法区到本地内存里面,而不是java内存,并且改名字叫元数据区。)
3) 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法
区这个类的各种数据的访问入口

链接阶段

验证

  • 验证:安全验证,规范验证

验证是连接阶段的第一步, 这一阶段的目的是确保Class文件的字节
流中包含的信息符合《Java虚拟机规范》 的全部约束要求, 保证这些信
息被当作代码运行后不会危害虚拟机自身的安全。

准备

准备阶段是正式为类中定义的变量(即静态变量, 被static修饰的变
量) 分配内存并设置类变量初始值的阶段
在这里插入图片描述

在准备阶段,类变量(static)只会被设置初始值(默认初始值 0 null 0.0),初始化阶段才被赋值为具体的值。
但是并不是所有的static修饰的都是在准备阶段进行初始值的设定。
被static final 修饰的常量是在编译之后就可以确定具体的值(这里的具体的值就是形如“sdf”);

解析

将常量池内的符号引用转换为直接引用的过程。
解析阶段和初始化的阶段的前后顺序是不确定的。

符号引用:符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定已经在内存中存在。

在这里插入图片描述

初始化阶段

在准备阶段,变量已经赋过一次系统要求的初始值
而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源
或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。

初始化阶段就是执行类构造器< clinit>()方法的过程 ,这个方法是Class对象的构造方法,是由JVM来完成初始化的。

< clinit >()方法与类的构造函数(或者说实例构造器< init >()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的< init>()方法执行之前,父类的< clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。

·< clinit >()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的。

  • 这里的赋值为所有的static修饰的变量赋值(static final除外 它是在编译是就确定的)和执行静态代码块代码块

这里一个需要注意的地方

  • 静态代码块可以对之后声明的变量赋值,但是不可以使用,是非法的前向引用。
    同时最后的值 还是按照出现的顺序决定的
    在这里插入图片描述

  • 如果没有静态变量、或者静态代码块,那么字节码文件中就不会有static clinit方法 。
    < clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法。

  • 虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

在这里需要区别 clinit 和 init 方法

Java对象在被创建时,会进行实例化操作。该部分操作封装在方法中,并且子类的方法中会首先对父类方法的调用。

public< linit > 里面包含了

  • 1.为成员变量赋值(注意final 修饰的也是在这里赋值,也就是说所有非static修饰的[所有类里面非 static 和static fianl 修饰的])
  • 2.实例代码块
  • 3.构造代码块

对象的构造方法编译为字节码< linit>,与 类似,搜集成员变量、实例代码块、构造方法 这也就是导致了为什么我明明new的是子类的对象,却有了父类的集成员变量、实例代码块、构造方法 同时也解释了最开始我们的各个代码块执行顺序的问题。(别问为什么, 字节码 show bytecode 里面就是这么写的)

有图为证在这里插入图片描述

最后一个小小的总结
1.static修饰的变量
准备阶段是为类变量(static修饰的变量)默认初始值,初始化阶段才被赋值为具体的值
2.static final 修饰的静态常量
static final 修饰的常量,class常量池即存在字面量及符号引用。编译的时候初始化就完成了。
3.final修饰的常量
如果是常量 final修饰的常量,因为final在编译的时候就会分配默认值,在构造方法完毕前就完成初始化 (也就是 init 方法结束前)就确定好直接的比如是5;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值