Java并发编程--线程上下文类加载

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/LuuvyJune/article/details/86507493

阅读《Java高并发编程详解》后的笔记。

类加载过程

1、类加载的过程

  • 类加载阶段

  • 类连接阶段

(1)验证

文件格式,元数据验证、字节码验证、符号引用验证。

(2)准备

为类变量(静态变量)分配内存并设置初始值。

(3)解析

类接口解析、字段解析、类方法解析、接口方法解析。

  • 类初始化阶段

如果某各类没有静态代码块、静态变量,那就不会生成<clinit>(),接口中只有变量的初始化操作才会生成<clinit>()。

执行<clinit>()方法,该方法包含了所有类变量的赋值动作和静态语句块的执行代码,所有的类变量被赋予正确的值,即编写程序指定的值。

保证顺序性,父类<clinit>()方法最先执行,父类的静态变量总是得到优先赋值。

以下代码中,静态语句块只能对后面的静态变量进行赋值,但不能对其进行访问。

触发类的初始化会调用<clinit>()方法,JVM保证了该方法在多线程的执行环境下的同步语义。若有多线程同时访问这个方法,只能有一个线程执行到静态代码块中的内容,并且静态代码块仅仅只会被执行一次:

public class TicketRunnable {

    static{
        try {
            System.out.println("111111");
            TimeUnit.MINUTES.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
    }
    public static void main(String args[]){

        //jdk1.8新特性
       IntStream.range(0,5).forEach(i->new Thread(TicketRunnable::new));
    }

}

看一段程序,分析:

连接阶段准备时:x=0,y=0,ticketRunnable=null

连接阶段初始化执行<clinit>(): x=0,y=0,ticketRunnable=new TicketRunnable();

 执行类的构造方法:x=1,y=1

public class TicketRunnable {

    //(1)
    private static int x = 0;
    private static int y;
    private static TicketRunnable ticketRunnable= new TicketRunnable();//(2)

    public TicketRunnable(){
        x++;
        y++;
    }
    
    public static TicketRunnable getInstance(){
        return ticketRunnable;
    }
    public static void main(String args[]){
        TicketRunnable ticketRunnable = TicketRunnable.getInstance();
        System.out.println(ticketRunnable.x);
        System.out.println(ticketRunnable.y);
    }
}

 当(1)和(2)交换位置后:

连接阶段准备时:ticketRunnable=null,x=0,y=0,

连接阶段初始化:赋予正确的初始值

执行构造函数: ticketRunnable=TicketRunnable@2cdf8d8a,x=1,y=1,

然后为x初始化,x没有显式赋值,x=0,为y初始化,由于没有给定初始值,构造函数中赋的值才是正确的,y=1

最后结果:ticketRunnable=TicketRunnable@2cdf8d8a,x=0,y=1。

2、类的主动使用和被动使用

6种主动使用类的场景:

  • new
  • 访问类静态变量
  • 访问类静态方法 
  • 对某个类进行反射操作
  • 初始化子类导致父类初始化
  • 启动类,执行main函数所在的类导致该类初始化

除以上6种,其余叫被动调用,不会导致类加载和初始化:

  • 构造某各类数组:Simple[] s = new Simple[10];
  • 引用类的静态常量  public static final int MAX;

JVM类加载器

1、JVM内置三大类加载器

2、自定义加载器:是classLoader的直接子类或间接子类。 

3、双亲委托机制

当一个类加载器背地哦啊用了LoadClass之后,它并不会直接将其加载,而是交给当前类加载器的父加载器直到最顶层的父加载器,然后依次向下进行加载。

破坏双亲委托机制:

应用:热部署 运行时进行某个模块的升级,不停止服务增加某个功能

  • 绕过系统类加载器,将扩展类加载器作为父加载器;
  • 构造指定其父加载器为null

4、类加载命名空间、运行时包、类的卸载

(1)每一个类加载器实例都有各自的命名空间,该命名空间是由该加载器及其所有父加载器构成的。

  • 使用不同类加载器加载同一个class
  • 同一个类加载器的不同实例,加载同一个class

这两种情况在对内存和方法区会产生多个class对象。

(2)JVM规定不同的运行时包下的类,彼此间不可进行访问。

初始类加载器:自定义SImple class  由  自定义classLoader --BrokerDelegateLoader加载,能访问不同的运行时包下的类,如String。原因:

(3)类的卸载

一个class被GC回收

  • 该类的所有实例已被GC,如Simple class中所有实例被回收;
  • 加载该类的classLoader的实例被GC;
  • 该类的class实例没有在其他地方被调用。

线程上下文类加载器

JDK核心类库提供了很多SPI(Service Provider Interface),常见的SPI包括JDBC,JNDI等。JDK只规定了这些接口之间的逻辑关系,不提供具体实现。如JDBC,不管数据库怎么切换,应用程序只需要更换JDBC的驱动jar包以及数据库驱动名称。

这里应用程序只需要面向接口编程,但是java.lang.sql所有接口由JDK提供,加载这些接口的类加载器是根加载器,第三方厂商提供的类库驱动是由系统类加载器加载的,由于JVM类加载器的双亲委托机制,如Connections,Statement,Rowset等都由根加载器加载,第三方的JDBC驱动包中的具体实现不会被启动类加载器(根加载器)加载,于是JDK提供了线程上下文类加载器,这时启动类加载器(根加载器)委托子类加载器加载厂商提供的SPI具体实现。

猜你喜欢

转载自blog.csdn.net/LuuvyJune/article/details/86507493