类加载和类加载器

类加载和类加载器

1. 类加载过程

1.加载

1.1 何为加载

加载类的二进制数据
类的加载指的是将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个Class对象,此Class对象用来封装类在方法区内的数据结构。
(jvm规范并未说明Class对象位于哪里,HosSpot将其放在方法区中)

1.2 加载的方式

  1. 从本地磁盘中加载class二进制文件
  2. 通过网络加载class二进制文件
  3. 从zip,jar等归档文件中加载
  4. 从专有数据库中提取class文件
  5. 源代码动态编译为class文件

1.3 加载的注意事项

  1. jvm允许类加载器在预料某个类将要被使用时就预先加载他,如果预先加载过程中遇到了class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误。
  2. 如果这个类一直没有被程序使用,那么类加载器就不会报告错误。

2.连接

1. 验证:确保类加载的正确性

  • 类文件的结构检查
  • 语义检查
  • 字节码验证
  • 二进制兼容性验证

2. 准备:为类的静态变量分配内存(也是分配默认值的过程)

3. 解析:将类中的符号引用转为直接引用

3.初始化

为类的静态变量赋正确的初始值

3.1 初始化时机

所有的Java虚拟机实现 必须在每个类或接口 被Java程序首次主动使用时 才初始化他们

1 主动使用

主动使用包含七种情况

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或对该静态变量赋值。
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个类的子类(子类初始化父类也会初始化)
  6. Java虚拟机启动时被标明为启动类的类(如包含main方法)
  7. jdk1.7开始提供的动态语言支持
2.被动使用

不是主动使用的都为被动使用

3.2初始化步骤

  1. 类没有加载和连接,先进行加载和连接。
  2. 类存在直接父类,并且父类没有初始化,那么先初始化直接父类,
  3. 类中存在初始化语句,那就依次执行初始化语句。

初始化代码示例

  1. 子父类
package jvmstu.classloader;

public class ClassLoader01 {
    public static void main(String[] args) {
        //当打印str1时,根据主动使用时才会初始化得,Child.str1 使用的是父类的静态变量,不会初始化子类,子类静态代码块不会执行。
        //System.out.println(Child.str1);
        //当打印str1时,根据主动使用时才会初始化得,Child.str2 使用的是子类的静态变量,子类初始化父类也会初始化,子类和父类的静态代码块都会执行。
        System.out.println(Child.str2);
    }
}

class Papa{
    public static String str1="im Papa";
    static {
        System.out.println("Papa 初始化");
    }
}

class Child extends Papa {
    public static String str2="im Child";
    static {
        System.out.println("Child 初始化");
    }
}
  1. 常量
package jvmstu.classloader;

public class ClassLoader02 {
    public static void main(String[] args) {
        System.out.println(Papa1.str1);
    }
}

class Papa1{
    //常量在 编译阶段 会存入 调用这个常量的方法所在的类的常量池中。
    //运行时,Papa1.str1 并没有引用到Papa1类,因此Papa1没有初始化。
    //甚至可以将编译好的Child1的class文件删除,也不会影响程序运行。
    public static final String str1="im Papa";
    static {
        System.out.println("Papa 初始化");
    }
}
  1. 编译期常量和运行期常量
package jvmstu.classloader;

import java.util.UUID;

public class ClassLoader03 {
    public static void main(String[] args) {
        System.out.println(Papa2.str1);
    }
}

class Papa2{
    //UUID.randomUUID().toString() 运行时才会知道是什么,不是编译器常量。
    //运行时常量在运行时会使用类的静态常量。
    //运行时,Papa2.str1 引用到Papa2类,因此Papa2初始化。
    public static final String str1= UUID.randomUUID().toString();
    static {
        System.out.println("Papa 初始化");
    }
}
  1. 创建类的实例 new对象
package jvmstu.classloader;

import java.util.UUID;

public class ClassLoader04 {
    public static void main(String[] args) {
        //创建类的实例,会首次主动使用类。
        Papa3 papa=new Papa3();
        System.out.println("====");
        Papa3 papa2=new Papa3();
    }
}

class Papa3{
    
    public static final String str1= UUID.randomUUID().toString();
    static {
        System.out.println("Papa 初始化");
    }
}
  1. 接口的初始化
package jvmstu.classloader;


import java.util.UUID;

public class ClassLoader05 {
    public static void main(String[] args) {
        //此处删掉编译好的class(不管是Papa5还是Child5)并不会出错
        //接口在初始化时,不一定要求父接口全部完成初始化
        //当真正使用父接口时,父接口才会初始化。
        System.out.println(Child5.str2);
    }
}

interface Papa5{
    public static  final String str1= UUID.randomUUID().toString();
}
interface Child5 extends Papa5{
    public static  String str2= "papa5 初始化";
}
  1. 初始化前准备阶段
package jvmstu.classloader;

public class ClassLoader06 {
    public static void main(String[] args) {
        //当调用getInstance(静态方法)会进行类的初始化
        //初始化前准备阶段会为静态变量分配内存,同时赋予默认值。
        //初始化阶段,会从上到下执行初始化赋值。
        //new 对象会调用构造方法。
        Singleton singleton = Singleton.getInstance();
        System.out.println("con1: "+Singleton.con1);
        System.out.println("con2: "+Singleton.con2);
    }
}


class Singleton{
    public static int con1;

    //public static int con2=0;
    private static Singleton singleton=new Singleton();

    public static int con2=0;

    private Singleton(){
        con1++;
        con2++;
        System.out.println(con1);
        System.out.println(con2);
    }
    public static Singleton getInstance(){
        return singleton;
    }
}
  1. 加载方式
package jvmstu.classloader;

/**
 * classLoader.loadClass 不会初始化类
 * Class.forName 会初始化化类
 */
public class ClassLoader08 {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader=ClassLoader.getSystemClassLoader();
        Class<?> loadClass = classLoader.loadClass("jvmstu.classloader.Papa8");
        System.out.println(loadClass);
        System.out.println("==================");
        Class<?> aClass = Class.forName("jvmstu.classloader.Papa8");
        System.out.println(aClass);

    }
}


class Papa8{
    static int a=2;
    static {
        System.out.println("papa load init");
    }
}

2.类加载器

类加载器有三种
在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)

1.加载器类型

1.1 根加载器

启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

1.2 扩展(Extension)类加载器

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

1.3 系统(System)类加载器

也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

2.加载器工作模式

双亲委派模式
双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派模式的好处

  1. 可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  2. 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

3 自定义加载器

用户可以编写自定义加载器,用于个性化的加载。

3.1 如何编写一个自定义加载器

  1. 继承ClassLoader类
  2. 重写findClass方法和loadClassData方法
    1. findClass 调用 loadClassData 返回class
    2. loadClassData,加载二进制文件,是自定义的io操作,可以在网络或者各种介质中加载。实际上如何将二进制文件转换为类,是由native方法,也即jvm完成的。

自定义加载器代码示例
注意
因为类的双亲委托机制,在classpath下的类都会被父加载器加载,因此还是不会调用自定义的加载器加载。
将classpath下的ClassLoader01.class移动到自定义的path下,此时父加载器不能加载ClassLoader01,自定义加载器会开始加载。

package jvmstu.classloader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 自定义加载器
 */
public class ClassLoader09 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";

    //构造方法
    //系统类加载器作为父构造器
    public ClassLoader09(String classLoaderName){
        super();
        this.classLoaderName=classLoaderName;
    }
    //指定父构造器
    public ClassLoader09(ClassLoader parent,String classLoaderName){
        super(parent);
        this.classLoaderName=classLoaderName;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @Override
    public String toString() {
        return "ClassLoader09{" +
                "classLoaderName='" + classLoaderName + '\'' +
                ", fileExtension='" + fileExtension + '\'' +
                '}';
    }
    //重写findClass 方法
    protected Class<?> findClass(String className){
        byte [] data=loadClassData(className);

        return this.defineClass(className,data,0,data.length);
    }
    //实现loadClassData 读取二进制文件。
    private byte [] loadClassData(String name){
        byte [] data=null;
        InputStream inputStream=null;
        ByteArrayOutputStream baos=null;

        try{
            String replace = name.replace(".", "/");
            System.out.println(replace);
            inputStream=new FileInputStream(path+replace+this.fileExtension);

            baos=new ByteArrayOutputStream();

            int ch=0;
            while(-1!=(ch=inputStream.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();

        }catch (Exception ex){
            ex.printStackTrace();
        }finally {
            try{
                inputStream.close();
                baos.close();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        }
        return data;
    }

    public static void main(String[] args) throws Exception {
        ClassLoader09 loader = new ClassLoader09("loader");
        loader.setPath("C:/Users/13166/Desktop/test/");
        Class<?> aClass = loader.loadClass("jvmstu.classloader.ClassLoader01");
        Object o = aClass.newInstance();
        System.out.println(o.getClass().getClassLoader());
    }
}

4.自定义系统类加载器

jvm允许将自定义的类加载器定义为系统(System)类加载器,只需添加jvm参数。(查看源码可见逻辑)

5.线程上下文类加载器

双亲委托模型下,类数据是由下到上加载的,对于spi(服务提供接口),java的核心库是由启动类加载器加载的,而这些接口的实现是由厂商实现的,由系统加载器加载。这样双亲加载模型就无法满足spi要求。
因此线程上下文类加载器就是解决这个问题的。父加载器加载的类,可以访问当前线程上下文类加载器加载的类,解决了父加载器加载的类不能访问子加载器加载的类的问题。

6.类加载器的命名空间

6.1 何为命名空间

  1. 每个类加载器都有自己的命名空间,命名空间由该加载器和该加载器的父加载器所加载的类组成
  2. 同一命名空间中,不会出现类的完整名字相同的两个类。
  3. 不同命名空间中,可以出现类的完整名字相同的两个类。

注意事项

  1. 子加载器所加载的类可以通过“引用”加载访问父加载器加载的类。(双亲委托)
  2. 父加载器所加载的类不可以通过“引用”加载本该由子加载器加载的类
  3. 没有关系加载器所加载的类是互相不可见的

6.2 类加载器和命名空间的关系

在双亲委托模型下

  1. 同一个命名空间的类是相互可见的
  2. 子加载器的命名空间包含所有父加载器的命名空间,因此子加载器加载的类是可以看见父加载器加载的类,父加载器加载的类则不可看见子加载器加载的类。
  3. 如果两个加载器之间没有直接或间接的关系,则他们各自加载的类相互不可见。

3.类的使用和卸载

使用 加载连接和初始化的过程
卸载 内存中销毁类
一个类在何时结束生命周期,取决于类的class文件何时被销毁
java自带的类加载器会始终引用他们加载的类,因此不会卸载。自定义的加载器加载的类可以被卸载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349