JVM之类加载器

源码解析

 1.ClassLoader:
    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {//判断该类加载是否注册了并行能力
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
        }
    }
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());//是否启动安全管理器,然后检测创建类加载的权限,获取系统类加载器
    }
    @CallerSensitive
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();//初始化系统类加载器
        if (scl == null) {//若系统类加载器为空,则直接返回null
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }

    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {//系统类加载器是否也被初始化
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader();//获取系统类加载器
                try {
                    /**
                     *  Java默认不打开安全检查,如果不打开,本地程序拥有所有权限;若打开了,则Java程序会检验权限
                     *  java.security.AccessController提供了一个默认的安全策略执行机制,它使用栈检查来决定潜在不安全的操作是否被允许。
                     *  每一个栈帧代表了由当前线程调用的某个方法,每一个方法是在某个类中定义的,每一个类又属于某个保护域,每个保护域包含一些权限。
                     *
                     *  AccessController主要用于以下三个目的:

                        1.  根据当前有效的安全策略决定是否允许或拒绝对关键资源的访问。

                        2.  将代码标记为"特权",从而影响后续访问决定。(即被标记的方法被无权限类所调用时,可以被正常调用)

                        3.  获取当前调用上下文的“快照”,这样便可以相对于已保存的上下文作出其他上下文的访问控制决定。

                        如:当开启权限管理,若core.jar有权限,而当前所调用core包的类无权限,且被调用的方法体中没有使用AccessController.doPrivileged标记资源特权,
                        那么会抛出java.security.AccessControlException: access denied
                     */
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));//将系统类加载器作为自定义类加载器父加载器
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }
    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 ClassLoader)去加载
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }

                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);//如果都没有找到,则通过自定义实现的findClass去查找并加载

                        // this is the defining class loader; record the stats
                        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 Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {//如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }

 2.SystemClassLoaderAction:
    public ClassLoader run() throws Exception {
        String cls = System.getProperty("java.system.class.loader");//获取自定义类加载器
        if (cls == null) {
            return parent;
        }
        //true即加载时需要初始化
        Constructor ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class[] { ClassLoader.class });//通过反射将系统类加载器(parent)传入,作为其父加载器
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });
        Thread.currentThread().setContextClassLoader(sys);//将自定义类加载器作为当前线程上下文类加载器
        return sys;
    }

 3.Launcher:(采用饿汉式)
    public Launcher() {
         Launcher.ExtClassLoader var1;
         try {
             var1 = Launcher.ExtClassLoader.getExtClassLoader();//获取拓展类加载器(Extension ClassLoader)
         } catch (IOException var10) {
             throw new InternalError("Could not create extension class loader", var10);
         }

         try {
             this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);//获取应用类加载器(Application ClassLoader)即系统类加载器并将扩展类加载器作为其父加载器
         } catch (IOException var9) {
             throw new InternalError("Could not create application class loader", var9);
         }

         Thread.currentThread().setContextClassLoader(this.loader);//将系统类加载器设置为当前线程类加载器
         String var2 = System.getProperty("java.security.manager");//是否启用安全管理,会加载默认安全策略
         if (var2 != null) {
             SecurityManager var3 = null;
             if (!"".equals(var2) && !"default".equals(var2)) {
                 try {
                     var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                 } catch (IllegalAccessException var5) {
                 } catch (InstantiationException var6) {
                 } catch (ClassNotFoundException var7) {
                 } catch (ClassCastException var8) {
                 }
             } else {
                 var3 = new SecurityManager();
             }

             if (var3 == null) {
                 throw new InternalError("Could not create SecurityManager: " + var2);
             }

             System.setSecurityManager(var3);
         }

     }

  4. ExtClassLoader:(采用懒汉式)
    static class ExtClassLoader extends URLClassLoader {
        private static volatile Launcher.ExtClassLoader instance;

         public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            if (instance == null) {
                Class var0 = Launcher.ExtClassLoader.class;
                synchronized(Launcher.ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }

            return instance;
        }
        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);//父加载为null
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
    }
  5.AppClassLoader:(多例)
    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }

    public Class loadClass(String var1, boolean var2) throws ClassNotFoundException {
        int var3 = var1.lastIndexOf(46);
        if (var3 != -1) {
            SecurityManager var4 = System.getSecurityManager();
            if (var4 != null) {
                var4.checkPackageAccess(var1.substring(0, var3));//包权限检测
            }
        }

        if (this.ucp.knownToNotExist(var1)) {//是否已经在缓存中
            Class var5 = this.findLoadedClass(var1);
            if (var5 != null) {
                if (var2) {
                    this.resolveClass(var5);
                }

                return var5;
            } else {
                throw new ClassNotFoundException(var1);
            }
        } else {
            return super.loadClass(var1, var2);//父加载器加载
        }
    }

双亲加载

加载过程

类加载即将class文件信息包括将类中所有信息(运行时常量,类型信息,字段信息,方法信息,属性信息,类加载器引用类信息,对应class实例引用)都放置到方法区。注:文件加载顺序,由当前文件系统来决定文件的顺序,因此不一定是按着字母排序。
1.加载:(虚拟机规范中并未规定何时开始加载一个类)
   1)通过一个类的全限定名来获取定义此类的二进制字节流。(可以从本地文件,网络中,运行时动态生成,数据库文件等等地方获取类字节流)
   2)将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
   3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
   注:对于数组类而言,情况有所不同,数组类本身不通过类加载创建,它是由Java虚拟机直接创建的。但是数组类里面的数据类型的加载就与类加载器有关了:
   若是数组的组件类型(ComponentType)是引用类型,那么就采用常规类加载器加载这个类,该数组将在该组件类型的类加载器的类名称空间上被标识。
   若是组件类型不是引用类型(eg: int[]),java虚拟机将会把该数组标记为与引导类加载器关联(Bootstarp classLoader)用来确定一个类的唯一性。

2.验证:确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,
   1)文件格式:验证字节流是否符合Class文件格式的规(如:.class文件开头为CAFE BABE),并且能够被当前虚拟机处理
   2)元数据:字节语法分析
   3)字节码:验证字节码文件方法中的Code结构
   4)符号引用:验证引用是否有访问权限(符号引用,比如main()替换为指向数据所存内容的指针或句柄等)也叫静态链接)

3.准备:为类的静态变量分配内存并设置初始值,在这个阶段把这些变量所使用的内存在方法区进行分配,这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化

4.解析:JVM将常量池内的符号引用替换为直接引用的过程

5.初始化:执行类构造器()方法(阻塞方法),若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化而不是赋值),该方法是由编译器自动收集当前类所有类变量的复制动作和静态语句块 static{} 中的语句合并产生的,编译器收集的顺序是有语句在源文件中出现的顺序决定的,*静态语句块只能访问到定义在静态语句块之前的变量*,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问(编译会通不过),如:
  public class Test{
      static{
          i = 0;//赋值
          System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前应用)
          //合并i = 1语句赋值,因此最终i为1
      }
      static int I = 1; //准备阶段:初始化为0
  }
虚拟机会保证一个类的类构造器  () 在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器(),其他线程都需要阻塞等待,直到活动线程执行 () 方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行 () 方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行 () 方法,因为 在同一个类加载器下,一个类型只会被初始化一次。此外,类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器 () 执行之前,父类的类构造 () 执行完毕。由于父类的构造器 () 先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器()对于类或者接口来说并不是必需的,*如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器 ()*。


验证、准备、解析通称为链接过程

优缺点

优点

1.保证java核心类库的安全,即保证由引导类加载器加载的类不能被用户随便替换,否则否则会抛出ClassCastException
2.使得一个类的不同版本可以共存在jvm中,带来了极大的灵活性,OSGi技术的实现就是得益于此(动态性:代码热替换、模块热部署等)

不足

java核心类库中定义的类是不能使用系统类加载器定义的类,Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。 而**线程上下文类加载器(TCCL)**就是用来解决类的双亲委托模型的缺点,而在默认情况下线程上下文类加载器就是应用类加载器,类是通过java.util.ServiceLoader类来加载。如JDBC案例分析:
     //加载Class到AppClassLoader(系统类加载器),然后注册驱动类
     //Class.forName("com.mysql.jdbc.Driver").newInstance();
     String url = "jdbc:mysql://localhost:3306/testdb";
     // 通过java库获取数据库连接
     Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
     //Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。
     JDBC中的DriverManager的加载Driver的步骤顺序依次是:
     1)通过System.getProperty("jdbc.drivers")获取设置,然后通过系统类加载器加载。
     2)通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
     参考:DriverManager#loadInitialDrivers
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。
实质:采用静态容器存储,如:
public class Manager {
    private static Map bs = new ConcurrentHashMap();

    public static void register(Driver d){
        bs.put(d.getClass().getName(),d);
    }

    public static Driver getDriver(String className) {
        return bs.get(className);
    }
}

类加载类型

1.Bootstrap ClassLoader
启动类加载器,又叫引导类加载器,native 实现的类加载器,负责将 ${JAVA_HOME}/lib 下面的核心库 rt.jar,或者 -Xbootclasspath 选项指定的 jar 包加载到内存。因为引导类加载器涉及到了虚拟机本地实现细节,是用 C++ 写的,不是 ClassLoader 的子类,所以开发者无法获取到引导类加载器的引用,不允许通过引用直接操作。

2. Extension ClassLoader
扩展类加载器,由 sun.misc.Launcher$ExtClassLoader 实现。负责将 ${JAVA_HOME}/lib/ext 或者 -Djava.ext.dir 指定位置中的类库加载到内存中。

3. System ClassLoader
系统类加载器,由 sun.misc.Launcher$AppClassLoader 实现,负责将 系统类路径 java -classpath 或 -Djava.class.path 变量指定的目录下的类库或当前工程类路径下.class文件加载到内存中。

显式和隐式加载

1.显式:即动态加载
通过手动调用类加载器来加载指定的class,对于JVM提供了两种方式:
1)ClassLoader.loadClass(className) :只执行装载过程。
2)Class.forName(className):执行类的装载和链接过程,而对于初始化过程,可以通过参数initialize控制是否需要初始化。(实质也是调用ClassLoader.loadClass)
  className:类的全路径,如:
    普通类:java.lang.String <包名>.<类名>
    匿名类: java.net.URLClassLoader$1 <包名>.<类名>.<匿名内部类名>
    内部类:java.concurrent.locks.AbstractQueuedSynchronizer$Node <包名>.<类名>$<内部类名>

2.隐式:new即会由JVM自动控制类的加载

安全管理

 一般而言,Java程序启动时并不会自动启动安全管理器,若启动,则会按照策略文件来执行操作,如当读取文件时,路径不在安全策略中,则读取失败。即它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作是什么,以及是否是在允许执行该操作的安全上下文中执行它。
1.启动/关闭SecurityManager
  1)通过命令启动SecurityManager:java -Djava.security.manager  #本质也是代码,若为空,则创建默认(参考Launcher构造方法中代码)
  2)通过编码启动SecurityManager:
     System.setSecurityManager(new SecurityManager());
     System.setSecurityManager(null); //关闭
2.策略文件:
  1)可以储存在无格式的ASCII文件,或Policy类的二进制文件,或数据库中,默认加载的策略文件的位置:可以从${JDK_HOME}/jre/lib/security目录下的java.security文件看到,如:
     policy.url.1=file:${java.home}/lib/security/java.policy
   也可以在启动的时候指定其他位置的策略文件:
     java -Djava.security.manager -Djava.security.policy=path
  2)语法格式:
     grant [signedBy ,] [codeBase ] {//若为路径,必须带协议,如"file:/x"
        permission    [ [, ]];
      };
      匹配模式:
        directory/ 表示directory目录下的所有.class文件,不包括.jar文件
        directory/* 表示directory目录下的所有的.class及.jar文件
        directory/- 表示directory目录下的所有的.class及.jar文件,包括子目录
        例如:
           grant codeBase "file:${{java.ext.dirs}}/*" {
                permission java.security.AllPermission;
            };
            grant {
                permission java.lang.RuntimePermission "setSecurityManager"; 
                permission java.util.PropertyPermission "java.version", "read";//"read,write"
             };
  3)权限类别:主要文件、套接字、网络、安全性、运行时、系统属性、AWT、数据库、日志、SSL、认证、音频、反射和可序列化。
    管理各种权限类别的类是java.io.FilePermission、java.net.SocketPermission、java.net.NetPermission、java.security.SecurityPermission、java.lang.RuntimePermission、java.util.PropertyPermission、java.awt.AWTPermission、java.sql.SQLPermission、java.util.logging.LoggingPermission、javax.net.ssl.SSLPermission、javax.security.auth.AuthPermission、javax.sound.sampled.AudioPermission、java.lang.reflect.ReflectPermission和 java.io.SerializablePermission。除前两个(FilePermission 和 SocketPermission)类以外的所有类都是java.security.BasicPermission 的子类,而 java.security.BasicPermission 类又是顶级权限类java.security.Permission 的抽象子类。FilePermission 和 SocketPermission 是顶级权限类 (java.security.Permission) 的子类,java.security.AllPermission属于所有权限集合

常见类冲突(或包冲突)

包冲突若报错一定属于Error子类,包冲突一定是类存在差异,从而导致引用问题或预期问题
1.版本错误(低版本运行高版本代码)java.lang.UnsupportedClassVersionError:Unsupported major.minor version 51.0
Class版本号和Java版本对应关系:
  JDK 1.8 = 52 JDK 1.7 = 51
  JDK 1.6 =50 
  JDK 1.5 = 49 
  JDK 1.4 = 48 
  JDK 1.3 = 47 
  JDK 1.2 = 46 
  JDK 1.1 = 45
将class文件转成16进制文件,内容头部包含如:
  0x0034 --> 52

2.初始化错误:
java.lang.ExceptionInInitializerError
xx. 
注:clinit表示静态初始化,init表示非静态初始化(此时不会抛ExceptionInInitializerError)
3.类加载错误:
java.lang.NoClassDefFoundError:Could not initialize class xx
原因:1)classpath即类路径有问题(如:manifest文件中的classpath属性中定义)
2)类的静态初始化错误导致(当再次访问类时)
3)缺少jar包

java.lang.ClassNotfoundException:
原因:缺少jar包(即在classpath中找不到)

两者最大区别:前者编译时正常而运行时无法加载(隐式加载),而后者编译时可能不正常可选择try…catch,一般情况下,当使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException(即显式加载,不属于包冲突)

4.方法或字段引用错误:
java.lang.NoSuchMethodError
java.lang.NoSuchFieldError
App 加载了错误版本,通常是 App 中不同组件依赖了同一个库的不同版本。
注:NoSuchMethodException等不属于包冲突

5.类型转换错误:
ClassCastException
典型情况:同一个类由不同的加载器加载的,两个类对象互相转化时

6.预期不一致:(不报错)
不同的Jar包出现了接口一致但实现逻辑又各不相同的同名类,从而引发此问题。

解决包冲突问题:
1)maven仲裁机制:
* 优先按照依赖管理元素中指定的版本声明进行仲裁,此时下面的两个原则都无效了
* 若无版本声明,则按照“短路径优先”的原则(Maven2.0)进行仲裁,即选择依赖树中路径最短的版本
* 若路径长度一致,则按照“第一声明优先”的原则进行仲裁,即选择POM中最先声明的版本
* 覆写优先子pom内声明的优先于父pom中的依赖

通过exclude排除一些间接依赖

2)自定义类加载
采用不同类加载器加载从而形成隔离

你可能感兴趣的:(JVM之类加载器)