• Java 类加载


    从一个诡异的问题说起

    测试案例一:

    package ecut.classloader;
    
    public class Sun {
        
        protected static int a = 100 ;
        protected static int b  ;
        
        protected static Sun instance = new Sun() ;
        
        public Sun() {
            a++ ;
            b++ ;
        }
        
    }
    package ecut.classloader;
    
    public class SunTest {
    
        @SuppressWarnings("unused")
        public static void main(String[] args) {
            
            Sun s = Sun.instance;
            
            System.out.println( Sun.a );
            System.out.println( Sun.b );
    
        }
    
    }

    运行结果如下:

    101
    1

    测试案例二:

    package ecut.classloader;
    
    public class Moon {
        
        protected static Moon instance = new Moon() ;
        
        protected static int a = 100 ;
        protected static int b  ;
        
        public Moon() {
            a++ ;
            b++ ;
        }
        
    }
    package ecut.classloader;
    
    public class MoonTest {
    
        @SuppressWarnings("unused")
        public static void main(String[] args) {
            
            Moon s = Moon.instance;
            
            System.out.println( Moon.a );
            System.out.println( Moon.b );
    
        }
    
    }

    运行结果如下:

    100
    1

     类的生命周期

    1、 JVM 的生命周期 ( 在线程部分 ) :当一个 Java 程序执行时,将启动一个 JVM 进程 ,当程序执行结束或抛出异常时 JVM 退出。

    2、对象的声明周期: 当使用 new 关键字 创建一个类的实例时,一个对象(实例)的生命周期即宣告开始

          Student s = new Student();  // 将导致创建一个Student实例并对该实例中的实例属性进行初始化
                           
                           实例属性的初始化:
                           
                               private int  id = 0 ;
                               
                               private String studentNo ;
                               
                               {
                                   studentNo = "ECUT-00000000" ;
                               }
                               
                               private String name ;
                               
                               public Student( String name ){
                                   this.name = name ;
                               }
                   
    使用对象 ( 使用对象的 属性 、方法 等 )
    当某个对象不再被任何一个引用变量所引用时,它可能会被GC回收,如果被回收,它的生命周期将宣告结束

    3、类的生命周期

    java.lang.Object 是整个 Java 类继承体系的根类

    •  java.lang.Class 类 也继承了 Object 类

    java.lang.Class 类的实例表示正在运行的 Java 应用程序中的类和接口

    • java.lang.Object.class 表示正在运行的 Java 程序中的 那个 Object 类 对应的 Class 类型的对象
    • Java语言中万事万物都可以当作对象来对待,即使是一个类,也可以当对对象对待。

    类的加载

    • 将 字节码文件( .class ) 读入到 JVM 所管理的内存中
    • 将 字节码文件对应的类的数据结构 保存在方法区
    • 最后生成一个与该类对应的 java.lang.Class 类型的对象 ( 在堆区 )

    类的链接

    • 连接是把已读入到内存的类的二进制数据合并到Java运行时环境(JRE)中去。
    • 连接又分为三个阶段:验证、准备、解析。
    • 验证:验保证类有正确的内部结构,并且与其它类协调一致如果JVM 检查到错误,就会抛出Error 对象。

        类文件的结构检查: 确保文件遵循Java 文件的固定格式
        语义检查: 确保类本身符合Java 语言的语法规定
        字节码验证: 确保字节码流可以被JVM 安全地执行
          » 字节码流代表Java 方法(含静态和非静态),它是被称作操作码的单字节指令组成的序列,每个操作码后都跟着一个或多个操作数
          » 字节码验证会检查每个操作码是否合法,即是否有合法的操作数二进制兼容的验证: 确保相互引用的类之间协调一致
          » 比如A 类中调用B 类的b() 方法,检查B 中是否有b() 方法存在

    • 准备:  在准备阶段,JVM 为类的静态变量分配内存,并设置默认值(byte 、short 、int 默认值都是 0,long 默认值是 0L,float 默认值是 0.0F,double 默认值是 0.0,boolean 默认值是 false,char 默认值是 u0000,  引用类型的默认值是 null)。
    • 解析: 将符号引用解析为直接引用

    类的初始化

    • 初始化阶段,JVM执行类的初始化语句,为静态变量赋予初始值
    • 静态变量的初始化途径:在静态变量的声明处进行初始化,在静态代码块中进行初始化
    • 初始化代码可能是(声明变量时的赋值语句): protected static int a = 100 ;也可以是(静态代码块):

         static {

        a = 10000 ;
               }

    • 类初始化的一般步骤

        如果该类还没有被加载和连接,那么先加载和连接该类
        如果该类存在直接父类,但该父类还未初始化,则先初始化其直接父类
        如果该类中存在初始化语句,则依次执行这些初始化语句

    • 类的初始化时机

        JVM 只有在首次主动使用某个类或接口时才会初始化它
        被动使用不会导致本类的初始化

    诡异的问题 解析测试案例:

    package ecut.classloader;
    
    public class Sun {
        
        protected static int a = 100 ;//链接(准备):0//初始化: a:100
        protected static int b  ;//链接(准备):0//初始化: b:0
        
        protected static Sun instance = new Sun() ;//链接(准备):null//初始化: a:101  b:1    
        
        public Sun() {
            a++ ;
            b++ ;
        }
        
    }
    package ecut.classloader;
    
    public class Moon {
        
        protected static Moon instance = new Moon() ;//链接(准备):null//初始化: a:1  b:1
        
        protected static int a = 100 ;//链接(准备):0//初始化: a:100
        protected static int b  ;//链接(准备):0//初始化: b:1
        
        public Moon() {
            a++ ;
            b++ ;
        }
        
    }

    类的使用

    •     主动使用 会导致 类被初始化

        a>、创建类的实例 ( new 、反射、反序列化 、克隆 )
        b>、调用类的静态方法
        c>、访问类 或 接口的 静态属性 ( 非常量属性 )  ( 取值 或 赋值 都算 )
          访问类 或 接口 的 非编译时常量,也将导致类被初始化:
                          public static final long time = System.currentTimeMillis();
        d>、调用反射中的某个些方法,比如 Class.forName( "edu.ecut.Student" );
        e>、初始化某个类时,如果该类有父类,那么父类将也被初始化
        f>、被标记为启动类的那些类(main)

    • 被动使用 不会导致类被初始化

        a>、程序中对编译时常量的使用视作对类的被动使用
             对于final 修饰的变量,如果编译时就能确定其取值,即被看作编译时常量
            » 编译时常量如: public static final int a = 2 * 3 ;
            » JVM 的加载和连接阶段,不会在方法区内为某个类的编译时常量分配内存

          对于final 修饰的变量,如果编译时就不能确定其取值,则不被看作编译时常量
            » 非编译时常量如: public static final long time = System.currentTimeMillis() ;
            » 使用该类型的静态变量将导致当前类被初始化( 主动使用)
        b>、JVM初始化某个类时,要求其所有父类都已经被初始化,但是 该规则不适用 于 接口 类型
          一个接口不会因为其子接口或实现类的初始化而初始化,除非使用了该接口的静态属性
        c>、只有当程序访问的静态变量或静态方法的确在当前类或接口定义时,
                          才能看作是对类或接口的主动使用:
          比如使用了 Sub.method() ,而 method() 是继承自 Base ,则只初始化 Base 类
          d>、调用 ClassLoader 的 loadClass( ) 加载一个类,不属于对类的主动使用

    主动使用和被动使用测试案例一:

    package ecut.classloader;
    
    public class Panda {
        
        // 编译时常量(对于final 修饰的变量,如果编译时就能确定其取值,即被看作编译时常量)
        public static final String HOMETOWN = "中国" ;
        
        // 非编译时常量(对于final 修饰的变量,如果编译时就不能确定其取值,则不被看作编译时常量)
        public static final long time = System.currentTimeMillis();
    
        public static int a ;
        
        static {
            System.out.println( "static code , a = " + a  );
             a = 100 ;
             System.out.println( "static code , a = " + a  );
        }
    }
    package ecut.classloader;
    
    public class PandaTest {
    
        public static void main(String[] args) {
            
            System.out.println( Panda.HOMETOWN ); // 编译时常量被动使用
            
            //System.out.println( Panda.a ); // 访问静态变量(不是常量) 主动使用
            //使用该类型的静态变量将导致当前类被初始化( 主动使用)
            System.out.println( Panda.time );//非编译时常量主动使用,静态代码块只执行一次因为初始化操作只执行一次
            
            System.out.println( Panda.time );//只有第一次使用才完成初始化操作,所以值是固定的不变的
    
        }
    
    }

    运行结果如下:

    中国
    static code , a = 0
    static code , a = 100
    1522485492485
    1522485492485

    主动使用和被动使用测试案例二:

    package ecut.classloader;
    
    public class InitTest {
        public static void main(String[] args) {
            //比如使用了 Child.hometown ,而 hometown是继承自 Father ,则只初始化 Father 类
            //System.out.println(Child.hometown);
            //初始化某个类时,如果该类有父类,那么父类将也被初始化
            //System.out.println(Child.name);
            //new Father();
            //new Father();
            new Child();
            new Child();
        }
    }
    
    class Father {
        protected static String hometown ;
        static{
            System.out.println( "Father : static code block." );
            hometown = "Sinaean" ;
        }//new Fater()时静态代码块最先执行,只执行一次
        { System.out.println( "Father : non-static code block." );}//每一次new Fater()都执行,仅此静态代码块执行
        public Father(){
            System.out.println( "Father construction." );
        }//每一次new Fater()都执行,最后执行
    }
    
    class Child extends Father {
        protected static String name ;
        static{
            System.out.println( "Child : static code block." );
            name = "Child" ;
        }
        { System.out.println( "Child : non-static code block." );}
        public Child(){
            System.out.println( "Child construction." );
        }
    }

    运行结果如下:

    Father : static code block.
    Child : static code block.
    Father : non-static code block.
    Father construction.
    Child : non-static code block.
    Child construction.
    Father : non-static code block.
    Father construction.
    Child : non-static code block.
    Child construction.

    类的卸载:当一个类不再被任何对象所使用时,JVM会卸载该类。

     类加载器

    1、类加载器用来把类加载到JVM 中

    从JDK 1.2 版本开始,类的加载过程采用父亲委托机制

    • 设loader 要加载A 类,则loader 首先委托自己的父加载器去加载A 类,如果父加载器能加载A 类则由父加载器加载,否则才由loader 本身来加载A类。
    • 这种机制能更好地保证Java 平台的安全性

    父亲委托机制中,每个类加载器都有且只有一个父加载器,除了JVM 自带的根类加载器( Bootstrap Loader )

    2、JVM 的三种主要类加载机制

    全盘负责

    • 当一个类加载器负责加载某个类时,该类所依赖和引用的其它类也将由当前的类加载器负责载入,除非显式使用了另外一个类加载器来载入

    父类委托

    • 先让父加载器加载某个类,只有父加载器无法加载该类时子加载器才加载
    • 当需要加载某个类时,加载这个类的类加载器会将加载操作委托给父加载器
    • JVM 提供了 根 加载器 : Bootstrap Loader ,它 是 JVM 的一个组成部分 ( 由JVM的实现着实现 )

    缓存机制

    • 使用缓存把所有的被加载过的类缓存起来,当程序中需要用到某个类时,类加载器先从缓存中搜寻该类,如果缓存中不存在该类,系统将读取该类对应的二进制数据并转换成Class 对象并存入cache 中
    • 这正是修改源文件后只有重启一个JVM 才能看到修改后的执行效果的原因

    3、JVM 自带的类加载器

    根类加载器(BootstrapLoader)

    • 负责加载虚拟机的核心类库,比如java.lang.* 等
    • 从系统属性sun.boot.class.path 所指定的目录中加载类库
    • 该加载器没有父加载器,它属于JVM 的实现的一部分(用C++实现)

    扩展类加载器(ExtClassLoader)

    • 其父加载器为BootstrapLoader 类的一个实例
    • 该加载器负责从java.ext.dirs 系统属性所指定的目录中加载类库或者从JDK_HOME/jre/lib/ext 目录中加载类库
    • 该加载器对应的类是纯Java 类,其父类是java.lang.ClassLoader

    系统类加载器(AppClassLoader)

    • 也称作应用类加载器,其父加载器默认为ExtClassLoader 类的一个实例
    • 负责从CLASSPATH 或系统属性java.class.path 所指定的目录中加载类库
    • 它是用户自定义类加载器的默认父加载器
    • 其父类也是java.lang.ClassLoader

    ClassLoader测试案例一:

    package ecut.classloader;
    
    import java.util.ArrayList;
    
    public class ClassLoaderTest1 {
    
        public static void main(String[] args) {
    
            Class<?> c = String.class; // java.lang.String
            ClassLoader loader = c.getClassLoader();
            System.out.println(loader); // null ( Bootstrap Loader )
    
            Object o = new ArrayList<>(); // java.util.ArrayList
    
            c = o.getClass();
            loader = c.getClassLoader();
            System.out.println(loader); // null ( Bootstrap Loader )
            
            c = ClassLoaderTest1.class;
            loader = c.getClassLoader(); // 获得 ClassLoaderTest1 这个类的类加载器
            System.out.println(loader); // AppClassLoader
            
            // 获得 loader 这个 "类加载器" 的 父加载器
            ClassLoader parent = loader.getParent();
            System.out.println(parent); // ExtClassLoader
            
            ClassLoader root = parent.getParent();
            System.out.println( root ); // null ( Bootstrap Loader )
    
        }
    
    }

    运行结果如下:

    null
    null
    sun.misc.Launcher$AppClassLoader@73d16e93
    sun.misc.Launcher$ExtClassLoader@15db9742
    nul

    4、类加载器的层次

    注意这里的层次关系不是类与类的继承关系

    各层次的类加载器加载的类

    ClassLoader测试案例二:

    package ecut.classloader;
    
    import java.util.Iterator;
    import java.util.Properties;
    import java.util.Set;
    
    public class ClassLoaderTest2 {
    
        public static void main(String[] args) {
    
            Properties props = System.getProperties();
            System.out.println(props);
            System.out.println("~~~~~~~~~~~~~~~~~~~~~~~");
    
            Set<Object> keys = props.keySet();
            for (Object key : keys) {
                Object value = props.get(key);
                System.out.println(key + " : " + value);
            }
            System.out.println("~~~~~~~~~~~~~~~~~~~~~~~");
            Iterator<Object> it = keys.iterator();
            while (it.hasNext()) {
                Object key = it.next();
                Object value = props.get(key);
                System.out.println(key + " : " + value);
            }
            System.out.println("~~~~~~~~~~~~~~~~~~~~~~~");
    
            System.out.println(System.getProperty("java.ext.dirs"));
    
            System.out.println(System.getProperty("java.class.path"));
    
        }
    
    }

    运行结果如下:

    ..........
    ~~~~~~~~~~~~~~~~~~~~~~~
    C:Program FilesJavajdk1.8.0_121jrelibext;C:WindowsSunJavalibext
    D:java_workspaceJavaJavaAdvancedin

     5、自定义类加载器

    JVM 允许开发者开发自己的类加载器

    • 扩展java.lang.ClassLoader 类即可
    • 重写其中的方法

    ClassLoader 中的关键方法

    • Class loadClass(String name )该方法为ClassLoader 的入口点,根据指定二进制名称来加载类
    • Class findClass( String name )根据二进制名称来查找类(一般重写该方法即可)
    • Class defineClass(String name, byte[] b, int off, int len)根据加载到的二进制数据返回一个Class 对象。

    部分源码:

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
        
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 首先,检查类是否已经加载。
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);//看父类加载器有没有加载该类(父委托机制)
                        } else {
                            c = findBootstrapClassOrNull(name);//父类加载器为空,看根加载器(Bootstrap Loader)有没有加载
                        }
                    } catch (ClassNotFoundException e) {
    
                        //如果类没有发现抛出ClassNotFoundException 
                    }
    
                    if (c == null) {
                        //如果仍然没有找到,然后调用findClass为了找到类。 
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // 这是定义类装入器;记录统计数据
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
        
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
        }

    除了 Bootstrap Loader 之外,其它的所有的类加载器对应的类的父类都是 java.lang.ClassLoader,loadClass方法最终调用的是findClass方法,因此自定义加载器时应该继承java.lang.ClassLoader并重写findClass方法。

    自定义加载器测试案例:

    用记事本新建一个Student.java,再使用命令行生成Student.class文件

    Student.java有包名,直接运行Java命令会无法加载主类,因为用Javac 虽然可以编译但是没有生成正确的目录结构,包结构不对,main方法无法执行,应该带着包一起编译。并且运行java 命令需要在包名的上级目录下运行,且带上完整类名(包名.类名)

    错误的编译方式:

    正确的编译方式:

     

    package ecut.classloader.entity;
    
    public class Student{
    
        private String name;
        
        private  int id;
        
        public void setName(String name){
            this.name = name;
        }
        
        public String getName(){
            return name;
        }
        
        public void setId (int id){
            this.id = id;
        }
        
        public int getId(){
            return id;
        }
        
        public static void main(String[] args) {
             
            System.out.println("Hello World");
            
        }
    
    }
    package ecut.classloader;
    
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    
    /**
     * 除了 Bootstrap Loader 之外,
     * 其它的所有的类加载器对应的类的父类都是 java.lang.ClassLoader
     */
    public class EcutClassLoader extends ClassLoader {
        
        private String path ;
        
        public EcutClassLoader(String path) {
            super();
            this.path = path;
        }
    
        @Override
        protected Class<?> findClass(final String name) throws ClassNotFoundException {
            Class<?> c = null ;
            System.out.println( "将要加载的类: " + name );
            String s = name.replace( '.', '/' ) + ".class";
            
            Path p = Paths.get( path ,  s );
            
            if( Files.exists( p ) ){
                try {
                    
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    
                    InputStream in = Files.newInputStream( p  );
                    int n ;
                    byte[] bytes = new byte[1024];
                    while( ( n = in.read( bytes ) ) != -1 ){
                        baos.write( bytes , 0 , n );
                    }
                    
                    final byte[] byteCode = baos.toByteArray(); // 获得 ByteArrayOutputStream 内部的数据
                    
                    c = this.defineClass( name , byteCode ,  0 , byteCode.length );
                    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                throw new ClassNotFoundException( "类: " + name  + " 未找到." );
            }
            
            return c ;
        }
    
    }
    package ecut.classloader;
    
    import java.lang.reflect.Field;
    
    public class EcutClassLoaderTest {
    
        public static void main(String[] args) throws Exception {
            
            final String path = "D:/Amy" ;
            
            // 创建一个自定义的类加载器 ( 实例 )
            EcutClassLoader loader = new EcutClassLoader( path );
            
            final String className = "ecut.classloader.entity.Student" ;
            Class<?> c = loader.loadClass( className );
            
            System.out.println( c );
            
            System.out.println( c.getName() );
            
            System.out.println( c.getSimpleName() );
            
            System.out.println( "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
            
            Object o = c.newInstance();
            
            System.out.println( o );
            
            Field idField = c.getDeclaredField( "id" );
            idField.setAccessible( true );
            Object value = idField.get( o ); // o.id
            System.out.println( value );
            
            idField.set( o , 250 ); // o.id = 250 ;
            
             value = idField.get( o ); // o.id
             System.out.println( value );
            
        }
    
    }

    运行结果如下:

    将要加载的类: ecut.classloader.entity.Student
    class ecut.classloader.entity.Student
    ecut.classloader.entity.Student
    Student
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ecut.classloader.entity.Student@4e25154f
    0
    250

    java.net.URLClassLoader 类

    • 是ClassLoader 的URL 版实现
    • 该类也是系统类加载器类和扩展类加载器类的父类,这两个类继承了该类,该类又继承了java.security.SecureClassLoader,而java.security.SecureClassLoader 则继承了ClassLoader
    • URLClassLoader 功能比较强大,可以从本地文件系统中获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类

    常用构造

    • URLClassLoader( URL[] urls )
    • URLClassLoader( URL[] urls , ClassLoader parent )

    获得实例的静态方法

    • static URLClassLoader newInstance(URL[] urls)
    • static URLClassLoader newInstance(URL[] urls, ClassLoader parent)

    待解决问题

    URLClassLoader

    转载请于明显处标明出处

    http://www.cnblogs.com/AmyZheng/p/8647217.html

  • 相关阅读:
    Myeclipse如何使用自带git工具向远程仓库提交代码
    myEclipse配置java版本(环境、项目、编译)
    新搭建项目时需要修改的内容
    干锅土豆
    SpringMVC MongoDB之“基本文档查询(Query、BasicQuery)”
    史上最全web.xml配置文件元素详解
    Web.xml配置详解之context-param
    史上最全的maven的pom.xml文件详解
    MongoDB 进阶模式设计
    备忘整理
  • 原文地址:https://www.cnblogs.com/AmyZheng/p/8647217.html
Copyright © 2020-2023  润新知