【深入理解】JVM学习笔记—类加载机制

最近看张龙老师的jvm课程做的一些笔记。

类加载

class loading

在java代码中,类的加载、连接和初始化过程都是在程序运行期间完成的。(类从磁盘加载到内存中经历的三个阶段)
提供了更大的灵活性,增加了更多的可能性。
类加载器深入剖析:
Java虚拟机与程序的生命周期
在如下几种情况下,java虚拟机将结束生命周期
(1)执行了System.exit()方法
(2)程序正常执行结束
(3)程序在执行过程中遇到了异常或错误而异常终止
(4)由于操作系统出现错误而导致虚拟机进程终止

类的加载、连接与初始化:

加载:查找并加载类的二进制数据到java虚拟机中
连接
验证 : 确保被加载的类的正确性
准备:为类的静态变量分配内存,并将其初始化为默认值,但是到达初始化之前类变量都没有初始化为真正的初始值
解析:把类中的符号引用转换为直接引用,就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程
初始化:为类的静态变量赋予正确的初始值
类从磁盘上加载到内存中要经历五个阶段:加载、连接、初始化、使用、卸载

Java程序对类的使用方式可分为两种

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

主动使用(七种)
  • (1)创建类的实例
  • (2)访问某个类或接口的静态变量 getstatic(助记符),或者对该静态变量赋值 putstatic
  • (3)调用类的静态方法 invokestatic
  • (4)反射(Class.forName(“com.test.Test”))
  • (5)初始化一个类的子类
  • (6)Java虚拟机启动时被标明启动类的类
  • (7)JDK1.7开始提供的动态语言支持(了解)
被动使用

除了上面七种情况外,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化

类的加载详解:

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

加载.calss文件的方式
  • (1)从本地系统中直接加载
  • (2)通过网络下载.class文件
  • (3)从zip,jar等归档文件中加载.class文件
  • (4)从专用数据库中提取.class文件
  • (5)将java源文件动态编译为.class文件
    测试1:
/**
        对于静态字段来说,只有直接定义了该字段的类才会被初始化
        当一个类在初始化时,要求父类全部都已经初始化完毕
        -XX:+TraceClassLoading,用于追踪类的加载信息并打印出来

        -XX:+<option>,表示开启option选项
        -XX:-<option>,表示关闭option选项
        -XX:<option>=value,表示将option的值设置为value
*/
public class MyTest{
    public static void main(String[] args){
        System.out.println(MyChild.str);    //输出:MyParent static block 、 hello world   (因为对MyChild不是主动使用)
        System.out.println(MyChild.str2);  //输出:MyParent static block  、MyChild static block、welcome
    }
}
class MyParent{
    public static String str="hello world";
    static {
        System.out.println("MyParent static block");
    }
}
class MyChild extends MyParent{
    public static String str2="welcome";
    static {
        System.out.println("MyChild static block");
    }
}

测试2:

/**
        常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中
        本质上,调用类并没有直接调用到定义常量的类,因此并不会触发定义常量的类的初始化
        注意:这里指的是将常量存到MyTest2的常量池中,之后MyTest2和MyParent就没有任何关系了。
        甚至我们可以将MyParent2的class文件删除

        助记符 ldc:表示将int、float或者String类型的常量值从常量池中推送至栈顶
        助记符 bipush:表示将单字节(-128-127)的常量值推送到栈顶
        助记符 sipush:表示将一个短整型值(-32768-32369)推送至栈顶
        助记符 iconst_1:表示将int型的1推送至栈顶(iconst_m1到iconst_5)
*/
public class MyTest2{
    public static void main(String[] args){
        System.out.println(MyParent2.str);    //输出 hello world
        System.out.println(MyParent2.s);  
        System.out.println(MyParent2.i);  
        System.out.println(MyParent2.j);  
    }
}
class MyParent2{
    public static final String str="hello world";
    public static final short s=7;
    public static final int i=129;
    public static final int j=1;
    static {
        System.out.println("MyParent static block");
    }
}

测试3

/**
        当一个常量的值并非编译期间可以确定的,那么其值就不会放到调用类的常量池中
        这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化
*/
public class MyTest3{
    public static void main(String[] args){
        System.out.println(MyParent3.str);  //输出MyParent static block、kjqhdun-baoje21w-jxqioj1-2jwejc9029
    }
}
class MyParent3{
    public static final String str=UUID.randomUUID().toString();
    static {
        System.out.println("MyParent static block");
    }
}

测试4

/**
        对于数组实例来说,其类型是由JVM在运行期动态生成的,表示为 [L com.hisense.classloader.MyParent4 这种形式。
        对于数组来说,JavaDoc经构成数据的元素成为Component,实际上是将数组降低一个维度后的类型。
        
        助记符:anewarray:表示创建一个引用类型(如类、接口)的数组,并将其引用值压入栈顶
        助记符:newarray:表示创建一个指定原始类型(int boolean float double)d的数组,并将其引用值压入栈顶
*/
public class MyTest4{
    public static void main(String[] args){
        MyParent4 myParent4=new MyParent4();        //创建类的实例,属于主动使用,会导致类的初始化
        MyParent4[] myParent4s=new MyParent4[1];    //不是主动使用
        System.out.println(myParent4s.getClass());          //输出 [L com.hisense.classloader.MyParent4
        System.out.println(myParent4s.getClass().getSuperClass());    //输出Object
        
        int[] i=new int[1];
        System.out.println(i.getClass());          //输出 [ I
        System.out.println(i.getClass().getSuperClass());    //输出Object
    }
}
class MyParent4{
    static {
        System.out.println("MyParent static block");
    }
}

测试5

/**
        当一个接口在初始化时,并不要求其父接口都完成了初始化
        只有在真正使用到父接口的时候(如引用接口中定义的常量),才会初始化
*/
public class MyTest5{
    public static void main(String[] args){
         public static void main(String[] args){
            System.out.println(MyChild5.b)
         }
    }
}
interfacce MParent5{
    public static Thread thread=new thread(){
        System.out.println(" MParent5 invoke")
    };
}
interface MyChild5 extends MParent5{     //接口属性默认是 public static final
    public static int b=6;
}

测试6

/**
        准备阶段和初始化的顺序问题
*/
public class MyTest6{
    public static void main(String[] args){
         public static void main(String[] args){
            Singleton Singleton=Singleton.getInstance();
            System.out.println(Singleton.counter1);     //输出1,1
            System.out.println(Singleton.counter2);
         }
    }
}
class Singleton{
    public static int counter1;
    public static int counter2=0;               /
    private static Singleton singleton=new Singleton();
    
    private Singleton(){
        counter1++;
        counter2++;
    }
    
    // public static int counter2=0;       //   若改变此赋值语句的位置,输出  1,0
    public static Singleton getInstance(){
        return singleton;
    }
}

加载
连接(验证、准备、解析)
初始化
类初始化:为新的对象分配内存,为实例变量赋默认值,为实例变量赋正确的初始值

java编译器在它编译的每一个类都至少生成一个实例化的方法,在java的class文件中,这个实例化方法被称为。针对源代码中每一个类的构造方法,java编译器都会产生一个“”方法。

有两种类型的类加载器

1.Java虚拟机自带的加载器

  • 根类加载器(Bootstrap):该加载器没有父加载器,它负责加载虚拟机中的核心类库。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有集成java.lang.ClassLoader类。
  • 扩展类加载器(Extension):它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的jar文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是纯java类,是java.lang.ClassLoader`的子类。
  • 系统应用类加载器(System):也称为应用类加载器,它的父加载器为扩展类加载器,它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,他是用户自定义的类加载器的默认父加载器。系统类加载器时纯java类,是java.lang.ClassLoader`的子类。

2.用户自定义的类加载器

  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方式

根类加载器–>扩展类加载器–>系统应用类加载器–>自定义类加载器
类加载器并不需要等到某个类被“首次主动使用”时再加载它

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

类加载器用来把类加载到java虚拟机中。从JDK1.2版本开始,类的加载过程采用父亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则有父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

类被加载后,就进入连接阶段。连接阶段就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。

  • 类的连接-验证
    1)类文件的结构检查
    2)语义检查
    3)字节码验证
    4)二进制兼容性的验证
  • 类的连接-准备
    在准备阶段,java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0;
    public class Sample{
        private static int a=1;
        public  static long b;
        public  static long c;
        static {
            b=2;
        }
    }
  • 初始化
    在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:(1)在静态变量的声明处进行初始化;(2)在静态代码块中进行初始化。
    类的初始化步骤:
    (1)假如这个类还没有被加载和连接,那就先进行加载和连接
    (2)假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
    (3)假如类中存在初始化语句,那就依次执行这些初始化语句
    ·当java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则不适用于接口。因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定的接口的静态变量时,才会导致该接口的初始化。
    ·调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
类加载器的父亲委托机制

在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根加载器之外,其余的类加载器都有一个父加载器

  • 若有一个类能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)称为初始类加载器
    在这里插入图片描述

测试7

/**
        java.lang.String是由根加载器加载,在rt.jar包下
*/
public class MyTest7{
    public static void main(String[] args){
         public static void main(String[] args){
            Class<?> clazz=Class.forName("java.lang.String");
            System.out.println(clazz.getClassLoader());  //返回null
            
            Class<?> clazz2=Class.forName("C");
           System.out.println(clazz2.getClassLoader());  //输出sun.misc.Launcher$AppClassLoader@18b4aac2  其中AppClassLoader:系统应用类加载器
         }
    }
}
class C{
}

测试8

/**
        调用ClassLoader的loaderClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
*/
public class MyTest8{
    public static void main(String[] args){
         public static void main(String[] args){
            ClassLoader loader=ClassLoader.getSystemClassLoader();
            Class<?> clazz1=loader.loadClass("CL"); //不会初始化
            System.out.println(clazz1);
            System.out.println("-------------------");
            
            Class<?> clazz=Class.forName("CL");
            System.out.println(clazz);  //反射初始化
         }
    }
}
class CL{
    static {
        System.out.println("FinalTest static block);
    }
}

测试9-12忽略
测试13

/**
    输出AppClassLoader、ExtClassLoader、null
*/
public class MyTest13{
    public static void main(String[] args){
         public static void main(String[] args){
            ClassLoader loader=ClassLoader.getSystemClassLoader();
            System.out.println(loader);
            
            while(loader!=null){
                loader=loader.getParent();
                 System.out.println(loader);
            }
         }
    }
}

测试14

public class MyTest14{
    public static void main(String[] args){
         public static void main(String[] args){
            ClassLoader loader=Thread.currentThread().getContextClassLoader();
            System.out.println(loader);         //输出AppClassLoader
            //下面这段没整明白什么用,先记录下来
            String resourceName="com/hisense/MyTest13.class";
            Enumeration<URL> urls=loader.getResources(resourceName);
            whilr(urls.hasMoreElements()){
                URL url=urls.nextElement();
                System.out.println(url);
            }
         }
    }
}
获取类加载器的途径:
  • (1)clazz.getClassLoader(); --获取当前类的加载器
  • (2)Thread.currentThread().getContextClassLoader(); --获取当前线程上下文的加载器
  • (3)ClassLoader.getSystemClassLoader(); --获取系统的加载器
  • (4)DriverManager.getCallerClassLoader(); --获取调用者的加载器
ClassLoader源码分析与实例剖析–

ClassLoader是一个负责加载class的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,ClassLoader尝试定位或者产生一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。

测试15
/**
    对于数组,它对应的class对象不是由类加载器加载,而是由JVM在运行期动态的创建。然而对于数组类的类加载器来说,它返回的类加载器和数组内元素的类加载器是一样的。如果数组类元素是原生类,那么数组是没有类加载器的。
*/
public class MyTest15{
    public static void main(String[] args){
            String[] strings=new String[2];
            System.out.println(strings.getClass());
            System.out.println(strings.getClass().getClassLoader());    //输出null
            
            MyTest15[] mytest15=new MyTest15[2];
            System.out.println(mytest15.getClass().getClassLoader());   //输出应用类加载器
            
            int[] arr=new int[2];
            System.out.println(arr.getClass().getClassLoader());        //输出null,此null非彼null
    }
}

并行类加载器可支持并发加载,需要在类初始化期间调用ClassLoader.registerAaParallelCapable()方法进行注册。ClassLoader类默认支持并发加载,但是其子类必须在初始化期间进行注册。

测试16

/**
    创建自定义加载器,继承ClassLoader
*/
public class MyTest16 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";
    
    public MyTest16(String classLoaderName){
        super();        //将系统类当做该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    public MyTest16(ClassLoader parent,String classLoaderName){
        super(parent);      //显式指定该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    
   public MyTest16(ClassLoader parent){
        super(parent);      //显式指定该类的父加载器
    }
    
    public void setPath(String path){
        this.path=path;
    }
    @Override
    protect Class<?> findClass(String className){
        System.out.println("calssName="+className);
        className=className.replace(".",File.separator);
        byte[] data=loadClassData(className);
        return defineClass(className,data,0,data.length); //define方法为父类方法
    }
    
    private byte[] loadClassData(String name){
        InputStream is=null;
        byte[] data=null;
        ByteArrayOutputStream baos=null;
        try{
            is=new FileInputStream(new File(this.path+name+this.fileExtension));
            baos=new ByteArrayOutputStream();
            int ch;
            while(-1!=(ch=is.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();
            
        }catch(Exception e){
        }finally{
            is.close();
            baos.close();
             return data;
        }
    }
    public static void test(ClassLoader classLoader){
        Class<?> clazz=classLoader.loadClass("com.hisense.MyTest1");  
        //loadClass是父类方法,在方法内部调用findClass
        System.out.println(clazz.hashCode());
        Object  object=clazz.newInstance();
        System.out.println(object);
    }
    public static void main(String[] args){
        //父亲是系统类加载器,根据父类委托机制,MyTest1由系统类加载器加载了
        MyTest16 loader1=new MyTest16("loader1");       
        test(loader1);
        
        //仍然是系统类加载器进行加载的,因为路径正好是classpath
        MyTest16 loader2=new MyTest16("loader2");  
        loader2.path="D:\Eclipse\workspace\HiATMP-DDMS\target\classes\";
        test(loader2);
        
         //自定义的类加载器被执行,findClass方法下的输出被打印。前提是当前calsspath下不存在MyTest1.class,MyTest16的父加载器-系统类加载器会尝试从classpath中寻找MyTest1。
        MyTest16 loader3=new MyTest16("loader3");  
        loader3.path="C:\Users\weichengjie\Desktop\";
        test(loader3);
        
        //与3同时存在,输出两个class的hash不一致,findClass方法下的输出均被打印,原因是类加载器的命名空间问题。
        MyTest16 loader4=new MyTest16("loader4");  
        loader4.path="C:\Users\weichengjie\Desktop\";
        test(loader4);
        
        //将loader3作为父加载器
        MyTest16 loader5=new MyTest16(loader3,"loader3");  
        loader3.path="C:\Users\weichengjie\Desktop\";
        test(loader5);
    }
}
命名空间

每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类构成;
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;
同一命名空间内的类是互相可见的,非同一命名空间内的类是不可见的;
子加载器可以见到父加载器加载的类,父加载器也不能见到子加载器加载的类。
类的卸载
当一个类被加载、连接和初始化之后,它的生命周期就开始了。当此类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在方法区内的数据也会被卸载。
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些加载器,而这些类加载器则会始终引用他们所加载的类的Class对象,因此这些Class对象是可触及的。
由用户自定义的类加载器所加载的类是可以被卸载的。

/**
    自定义类加载器加载类的卸载
    -XX:+TraceClassUnloading
*/
   public static void main(String[] args){
        MyTest16 loader2=new MyTest16("loader2");  
        loader2.path="D:\Eclipse\workspace\HiATMP-DDMS\target\classes\";
        test(loader2);
        loader2=null;
        System.gc();   //让系统去显式执行垃圾回收
        
        输出的两个对象hashcode值不同,因为前面加载的已经被卸载了
        loader2=new MyTest16("loader6"); //  
        test(loader2);
   }

gvisualvm命令 查看当前java进程,在jdk/bin下面

测试17

/**
    创建自定义加载器,继承ClassLoader
*/
class MyCat{
    public MyCat(){
        System.out.println("MyCat is loaded..."+this.getClass().getClassLoader());
    }
}

class MySample{
    public MySample(){
        System.out.println("MySample is loaded..."+this.getClass().getClassLoader());
        new MyCat();
    }
}

public class MyTest17 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";
    
    public MyTest17(String classLoaderName){
        super();        //将系统类当做该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    public MyTest17(ClassLoader parent,String classLoaderName){
        super(parent);      //显式指定该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    
    public void setPath(String path){
        this.path=path;
    }
    @Override
    protect Class<?> findClass(String className){
        System.out.println("calssName="+className);
        className=className.replace(".",File.separator);
        byte[] data=loadClassData(className);
        return defineClass(className,data,0,data.length); //define方法为父类方法
    }
    
    private byte[] loadClassData(String name){
        InputStream is=null;
        byte[] data=null;
        ByteArrayOutputStream baos=null;
        try{
            is=new FileInputStream(new File(this.path+name+this.fileExtension));
            baos=new ByteArrayOutputStream();
            int ch;
            while(-1!=(ch=is.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();
        }catch(Exception e){
        }finally{
            is.close();
            baos.close();
             return data;
        }
    }
    public static void main(String[] args){
        MyTest17 loader1=new MyTest17("loader1");
        Class<?> clazz=loader1.loadClass("com.hisense.MySample");  
        System.out.println(clazz.hashCode());
        //如果注释掉该行,就并不会实例化MySample对象,不会加载MyCat(可能预先加载)
        Object  object=clazz.newInstance(); //加载和实例化了MySample和MyCat
    }
}

测试17_1

public class MyTest17_1 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";
    
    public MyTest17_1(String classLoaderName){
        super();        //将系统类当做该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    public MyTest17_1(ClassLoader parent,String classLoaderName){
        super(parent);      //显式指定该类的父加载器
        this.classLoaderName=classLoaderName;
    }
    
    public void setPath(String path){
        this.path=path;
    }
    @Override
    protect Class<?> findClass(String className){
        System.out.println("calssName="+className);
        className=className.replace(".",File.separator);
        byte[] data=loadClassData(className);
        return defineClass(className,data,0,data.length); //define方法为父类方法
    }
    
    private byte[] loadClassData(String name){
        InputStream is=null;
        byte[] data=null;
        ByteArrayOutputStream baos=null;
        try{
            is=new FileInputStream(new File(this.path+name+this.fileExtension));
            baos=new ByteArrayOutputStream();
            int ch;
            while(-1!=(ch=is.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();
        }catch(Exception e){
        }finally{
            is.close();
            baos.close();
             return data;
        }
    }
    public static void main(String[] args){
        MyTest17_1 loader1=new MyTest17_1("loader1");
        loader1.path="C:\Users\weichengjie\Desktop";
        Class<?> clazz=loader1.loadClass("com.hisense.MySample");  
        System.out.println(clazz.hashCode());
        //MyCat是由加载MySample的加载器去加载的:
        如果只删除classpath下的MyCat,则会报错,NoClassDefFoundError;
        如果只删除calsspath下的MySample,则由自定义加载器加载桌面上的MySample,由系统应用加载器加载MyCat。
        Object  object=clazz.newInstance(); 
    }
    
}

测试17_1_1

//修改MyCat和MySample
class MyCat{
    public MyCat(){
        System.out.println("MyCat is loaded..."+this.getClass().getClassLoader());
        System.out.println("from MyCat: "+MySample.class);
    }
}

class MySample{
    public MySample(){
        System.out.println("MySample is loaded..."+this.getClass().getClassLoader());
        new MyCat();
        System.out.println("from MySample :"+ MyCat.class);
    }
}

public class MyTest17_1 {
        public static void main(String[] args){
        //修改MyCat后,仍然删除classpath下的MySample,留下MyCat,程序报错
        //因为命名空间,父加载器找不到子加载器所加载的类,因此MyCat找不到        
        //MySample。
        MyTest17_1 loader1=new MyTest17_1("loader1");
        loader1.path="C:\Users\weichengjie\Desktop";
        Class<?> clazz=loader1.loadClass("com.hisense.MySample");  
        System.out.println(clazz.hashCode());
        Object  object=clazz.newInstance(); 
    }
}
关于命名空间重要说明:

1
子加载器所加载的类能够访问父加载器所加载的类;
而父加载器所加载的类无法访问子加载器所加载的类。
测试18

public class MyTest18{
    public static void main(String[] args){
        System.out.println(System.getProperty("sun.boot.class.path"));//根加载器路径
        System.out.println(System.getProperty("java.ext.dirs"));//扩展类加载器路径
        System.out.println(System.getProperty("java.calss.path"));//应用类加载器路径
    }
}

测试18_1

public class MyTest18_1{
    public static void main(String[] args){
        MyTest16 loader1=new MyTest16("loader1");
        loader1.setPath("C:\Users\weichengjie\Desktop");
        
        //把MyTest1.class文件放入到根类加载器路径中,则由根类加载器加载MyTest1
        Class<?> clazz= loader1.loadClass("MyTest1");
        
        System.out.println("clazz:"+clazz.hashCode());
        System.out.println("class loader:"+clazz.getClassLoader());
        
    }
}

测试19

/**
    各加载器的路径是可以修改的,修改后会导致运行失败,ClassNotFoundExeception
*/
public class MyTest19{
    public static void main(String[] args){
        AESKeyGenerator aesKeyGenerator=new AESKeyGenerator();
        System.out.println(aesKeyGenerator.getClass().getClassLoader());//输出扩展类加载器
        System.out.println(MyTest19.class.getClassLoader());//输出应用类加载器
    }
}

测试20

 class Person{
    private Person person;
    public setPerson(Object object){
        this.person=(Person)object;
    }
 }
 
 public class MyTest20{
    public static void main(String[] args){
        MyTest16 loader1=new MyTest16("loader1");
        MyTest16 loader2=new MyTest16("loader2");
        
        Class<?> clazz1=load1.loadClass("Person");
        Class<?> clazz2=load1.loadClass("Person");
        //clazz1和clazz均由应用类加载器加载的,第二次不会重新加载,结果为true
        System.out.println(clazz1==clazz2);
        
        Object object1=clazz1.getInstance();
        Object object2=clazz2.getInstance();
        
        Method method=clazz1.getMethod("setPerson",Object.class);
        method.invoke(object1,object2);
        
    }
 }

测试21

 public class MyTest21{
    public static void main(String[] args){
        MyTest16 loader1=new MyTest16("loader1");
        MyTest16 loader2=new MyTest16("loader2");
        loader1.setPath("C:\Users\weichengjie\Desktop");
        loader2.setPath("C:\Users\weichengjie\Desktop");
        //删掉classpath下的Person类
        Class<?> clazz1=load1.loadClass("Person");
        Class<?> clazz2=load1.loadClass("Person");
        //clazz1和clazz由loader1和loader2加载,结果为false
        System.out.println(clazz1==clazz2);
        
        Object object1=clazz1.getInstance();
        Object object2=clazz2.getInstance();
        
        Method method=clazz1.getMethod("setPerson",Object.class);
        //此处报错,loader1和loader2所处不用的命名空间
        method.invoke(object1,object2);
    }
 }
类加载器双亲委托模型的好处:

(1)可以确保Java和核心库的安全:所有的Java应用都会引用java.lang中的类,也就是说在运行期java.lang中的类会被加载到虚拟机中,如果这个加载过程如果是由自己的类加载器所加载,那么很可能就会在JVM中存在多个版本的java.lang中的类,而且这些类是相互不可见的(命名空间的作用)。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动根加载器去加载,从而确保了Java应用所使用的的都是同一个版本的Java核心类库,他们之间是相互兼容的;
(2)确保Java核心类库中的类不会被自定义的类所替代;
(3)不同的类加载器可以为相同名称的类(binary name)创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器去加载即可。相当于在Java虚拟机内部建立了一个又一个相互隔离的Java类空间。
父亲委托机制的优点是能够提高软件系统的安全性。因此在此机制下,用户自定义的类加载器不可能加载应该由父类加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父类加载器加载的可靠代码。例如,java.lang.Object类是由跟类加载器加载,其他任何用哪个户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。
测试22

 public class MyTest22{
    static{
        System.out.println("MyTest22 init...");
    }
    public static void main(String[] args){
        System.out.println(MyTest22.class.getClassLoader());
        
        System.out.println(MyTest1.class.getClassLoader());
    }
 }

扩展类加载器只加载jar包,需要把class文件打成jar

测试23

/*
    在运行期,一个Java类是由该类的完全限定名(binary name)和用于加载该类的定义类加载器所共同决定的。如果同样名字(完全相同限定名)是由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件字节码相同,并且从相同的位置加载亦如此。
    在oracle的hotspot,系统属性sun.boot.class.path如果修改错了,则运行会出错:
    Error occurred during initialization of VM
    java/lang/NoClassDeFoundError: java/lang/Object
*/
 public class MyTest23{
    public static void main(String[] args){
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.calss.path"));
        
        System.out.println(ClassLoader.class.getClassLoader);
        System.out.println(Launcher.class.getClassLoader);
        
        //下面的系统属性指定系统类加载器,默认是AppClassLoader
        System.out.println(System.getProperty("java.system.class.loader"));
    }
 }
类加载器本身也是类加载器,类加载器又是谁加载的呢??(先有鸡还是现有蛋)

类加载器是由启动类加载器去加载的,启动类加载器是C++写的,内嵌在JVM中
内嵌于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类。当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器以及系统类加载器,这块特殊的机器码叫做启动类加载器。
启动类加载器并不是java类,其他的加载器都是java类。
启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。
OpenJDK
grepcode.com
源码分析
Launcher类
Class.forName();

Thread.getContextClassLoader()
当前类加载器(Current ClassLoader)
每个类都会尝试使用自己的类加载器去加载依赖的类。
线程上下文类加载器(Context ClassLoader)
线程上下文加载器 @ jdk1.2
线程类中的 getContextClassLoader() 与 setContextClassLoader(ClassLoader c)
如果没有通过setContextClassLoader()方法设置,线程将继承父线程的上下文类加载器,java应用运行时的初始线程的上下文类加载器是系统类加载器。该线程中运行的代码可以通过该类加载器加载类和资源。

线程上下文类加载器的作用:

SPI:Service Provide Interface
父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所制定的ClassLoader加载的类,这就改变了父加载器加载的类无法使用子加载器或是其他没有父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。
在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托父加载器进行加载。但是对于SPI来说,有些接口是Java核心库所提供的的(如JDBC),Java核心库是由启动类记载器去加载的,而这些接口的实现却来自不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

 public class MyTest24{
    public static void main(String[] args){
        System.out.println(Thread.currentThread().getContextClassLoader());
        //
        System.out.println(Thread.class.getClassLoader());
    }
 }

测试25

 public class MyTest25 implement Runable{
    private Thread thread;
    public MyTest25(){
        thread =new Thread(this);
        thread.start();
    }
    
    public void run(){
        ClassLoader classLoader=this.thread.getContextLoader();
        this.setContextLoader(classLoader);
        
        System.out.println("Class:"+classLoader.getClass());
        System.out.println("Parent:"+classLoader.getParent().getClass());
    }
    
    public static void main(String[] args){
        new MyTest25();
    }
 }

测试26

/*
    线程上下文类加载器的一般使用模式:(获取-使用-还原)*/
        //伪代码:
        ClassLoader classLoader=Thread.currentThread().getContextLoader();
        try{
            Thread.currentThread().setContextLoader(targetTccl);
            myMethod();
        }finally{
            Thread.currentThread().setContextLoader(classLoader);
        }

在myMethod中调用Thread.currentThread().getContextLoader()做某些事情
ContextClassLoader的目的就是为了破坏类加载委托机制

在SPI接口的代码中,使用线程上下文类加载器就可以成功的加载到SPI的实现类。

当高层提供了统一的接口让底层去实现,同时又要在高层加载(或实例化)底层的类时,就必须通过上下文类加载器来帮助高层的ClassLoader找到并加载该类。

public class MyTest26{
   public static void main(String[] args){
   
   //一旦加入下面此行,将使用ExtClassLoader去加载Driver.class, ExtClassLoader不会去加载classpath,因此无法找到MySql的相关驱动。
//Thread.getCurrentThread().setContextClassLoader(MyTest26.class.getClassLoader().parent());    

       ServiceLoader服务提供者,加载实现的服务
       ServiceLoader<Driver> loader=ServiceLoader.load(Driver.class);
       Iterator<Driver> iterator=loader.iterator();
       while(iterator.hasNext()){
           Driver driver=iterator.next();
           System.out.println("driver:"+driver.class+
                               ",loader"+driver.class.getClassLoader());
       }
       System.out.println("当前上下文加载器"
                   +Thread.currentThread().getContextClassLoader());
       System.out.println("ServiceLoader的加载器"
                   +ServiceLoader.class.getClassLoader());
   }
}     

测试27

//跟踪代码
 public class MyTest27{
    public static void main(String[] args){
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection=DriverManager.getConnection(
  "jdbc:mysql://localhost:3306//mydb","user","password");
    }
 }
jar hell问题以及解决办法

当一个类或者一个资源文件存在多个jar中,就会存在jar hell问题。
可通过以下代码解决问题:

ClassLoader calssLoader=Thread.currnetThread().getContextClassLoader();
String resource-“java/lang/String.class;
Enumeration urls=calssLoader.getResources(resource);
while(urls.hasMoreElements()){
– URL url=urls.nextElement();
– System.out.prinln(url);
}
发布了26 篇原创文章 · 获赞 3 · 访问量 369

猜你喜欢

转载自blog.csdn.net/jiaodaguan/article/details/103570545