读 - 深入理解java虚拟机 - 笔记(五-2) - 虚拟机类加载机制(7章)-类加载器

时间:2022-12-21 09:51:21

关于类加载器,需要先说明一下一个知识点。

对于每一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,因此比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它的类加载器不同,那这两个类必定不相等。

这边涉及到相等的概念,有必要驻足研究一下额外的东西~~~~比如instanceof 关键字,至于equals就不去管理,烂大街了都。

首先这个关键字我实在是不清楚底层到底是个啥样,能看见最深的文档就是java虚拟机规范,其实它就和<,>这些符号一样,是一种操作符,就是比较此对象是否是这个类型,你说<,>底层是个什么样的?没几个人说得清吧,哎。

在此贴两个能找到的讲的比较好的链接:

知乎回答,比较深

java se7虚拟机规范

接着贴一下中文翻译,细度,细读

读 - 深入理解java虚拟机 - 笔记(五-2) - 虚拟机类加载机制(7章)-类加载器

读 - 深入理解java虚拟机 - 笔记(五-2) - 虚拟机类加载机制(7章)-类加载器

总结下加深记忆和理解:

例子 : S objectref instanceof T

1. objectref  如果是 null的话,永远返回false,至于原理不去深究了,我只是觉得我这个阶段有点太深了

2. objectref 非null,主要分成3种大情况讨论。

S是类类型

2.1 S是类类型 ,那么T也需要是类类型,S和T必须是同意类类型,或者S是T的子类,这个很好理解,就是平常写的那些个类,new操作,比较操作,总的来说,还是我们以前的认知,两个类比较,要么就是该类类型的实例,要不就是它的子类的实例。正常。

public class Father {

    public static int NUM = 1;

    public static int get(){
        System.out.println("father get");
        return 1;
    }
    static {
        System.out.println("father init");
    }

}
public class Son extends Father{

    static {
        System.out.println("Son init");
    }

}

/**
 * @author: wayne
 * @desc: 被动引用-直接访问final变量
 * @date: 2017/11/15 11:01
 * @version: 1.0
 */
public class Test {

    public static void main(String[] args) {

        Father father = new Son();  //这边是多态的体现,父类的引用指向子类的对象
                                    //在编译期时,father这个引用的类型是Father,但是运行期时,它指向的对象是Son类型的对象
        Son son = new Son();
        System.out.println(son.getClass());
        System.out.println(father.getClass());
        System.out.println(son instanceof Father);
        System.out.println(son instanceof Son);
        System.out.println(father instanceof Father);
        System.out.println(father instanceof Son);

    }
}
father init
Son init
class com.wayne.personal.Son
class com.wayne.personal.Son
true
true
true
true
2.2 T如果是接口类型,那么S必须要实现它才能是T类型
/**
 * @author: wayne
 * @desc:
 * @date: 2017/11/16 9:49
 * @version: 1.0
 */
public interface Fa {

}
/**
 * @author: wayne
 * @desc: 子类
 * @date: 2017/11/15 11:23
 * @version: 1.0
 */
public class Son implements Fa{

}
/**
 * @author: wayne
 * @desc: 被动引用-直接访问final变量
 * @date: 2017/11/15 11:01
 * @version: 1.0
 */
public class Test {

    public static void main(String[] args) {

        Son son = new Son();
        Fa fa = new Son();  //这个像不像我们现在用的studentService和studentServiceImpl,面向接口编程

        System.out.println(son.getClass());
        System.out.println(fa.getClass());
        System.out.println(son instanceof Fa);
        System.out.println(fa instanceof Son);
        System.out.println(fa instanceof Fa);
    }
}
class com.wayne.personal.Son
class com.wayne.personal.Son
true
true
true
S是接口类型,其实接口类型可以不用刻意关注啦~~~

3.1如果T是类类型,只能是Object类型

3.2 如果T是接口类型,那么T和S应当是相同接口,或者T是S的父接口。

S是数组类型,这个需要去关注下,假设S为SC[] 的形式,这个数组的组件类型为SC
4.1如果T是类类型,只能是Object类型

4.2 如果T也是数组类型,假设为TC【】,那么这个组件的类型就是TC,那么就需要满足下面两条规则

       TC和SC是同一个原始类型

TC和SC都是引用类型,并且SC和TC能匹配


至此分析完instanceof的本质在比较什么,以及它最原始的规范是在比较什么,有些什么规定,相信以上的解释足够你去应对比人的一些问题了。

其实上面所有的实例都是在一个类加载器加载的结果下比较的,在此先不细究,先看完双亲委派模型再说。


双亲委派模型

说到双亲委派模型,就必须说下它的工作过程:

一个类加载器收到类加载的请求,它不会首先去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,最终所有的加载请求最终都应该传到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(就是它在自己的搜索范围没有找到所需要的类)时,子类加载器才会自己去加载。

从虚拟机角度只存在两种类加载器:

1.启动类加载器(Bootstrap ClassLoader),存在于虚拟机自身,负责加载JAVA_HOME/lib下加载到虚拟机内存。

2.其他类加载器,全部集成java.lang.ClassLoader。

更细一点可以分成3类:启动类和扩展类一般加载系统需要的类库,而应用程序类加载器负责加载用户类上所指定的类库,一般如果没有指定类加载器,这个就是默认的类加载器。

读 - 深入理解java虚拟机 - 笔记(五-2) - 虚拟机类加载机制(7章)-类加载器

好处:

使用双亲委派模型使得java类随着它的加载器一起具备了一种带有优先级的层级关系,最明显的就是Object这个类,无论是哪一个类需要被加载,最终都是委托了最顶层的启动类加载器进行加载,所以Object都是只有一个,如果不存在这个模型,那么用户可以定义自己的Object类,系统中会出现很多Object类,违反了最基础的行为规定。

看下类加载器的loadClass方法:不长,需要注意下synchronized放在了方法里面,以前的版本我没亲自去看,不过据说是放在loadClass方法上的,这就导致了当有一个类在加载的时候,其他的类肯定需要等待了,不能做到并行加载的效果,

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } 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);

                    // 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;
        }
    }
接着看加锁的地方以及加锁的方式:传参是一个name,返回结果是一个对象。
synchronized (getClassLoadingLock(name))
protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }
getClassLoadingLock返回两种值:

1. 当前的ClassLoad对象,也就是没有进入parallelockMap逻辑段,直接返回了。

2.返回一个和特定类className相关联的对象,也就是只要name不一样,就能并行加载了,锁的粒度放到这个上面了,这个与parallelockMap有关。

private final ConcurrentHashMap<String, Object> parallelLockMap;
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }
parallelockMap是ClassLoad的一个私有变量,在ClassLoad的构造函数内进行初始化,初始化时调用了isRegistered()方法来判断这个类加载器是否进行过注册,

关于ParallelLoaders是什么,他是ClassLoad内的一个静态内部类,

private static class ParallelLoaders {
        private ParallelLoaders() {}

        // the set of parallel capable loader types
        private static final Set<Class<? extends ClassLoader>> loaderTypes =
            Collections.newSetFromMap(
                new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
        static {
            synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
        }

        /**
         * Registers the given class loader type as parallel capabale.
         * Returns {@code true} is successfully registered; {@code false} if
         * loader's super class is not registered.
         */
        static boolean register(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
                if (loaderTypes.contains(c.getSuperclass())) {
                    // register the class loader as parallel capable
                    // if and only if all of its super classes are.
                    // Note: given current classloading sequence, if
                    // the immediate super class is parallel capable,
                    // all the super classes higher up must be too.
                    loaderTypes.add(c);
                    return true;
                } else {
                    return false;
                }
            }
        }

        /**
         * Returns {@code true} if the given class loader type is
         * registered as parallel capable.
         */
        static boolean isRegistered(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
                return loaderTypes.contains(c);
            }
        }
    }

首先关于上面的静态内部类作用就是判断一个类加载器是否能够并行加载,其次就是给出了一个让类加载器注册成为并行加载器的方法,调用方法是register方法,注册的时候会去判断它的父加载类是否也是并行加载类,如果是,才加入,不是不加,返回false。

 protected static boolean registerAsParallelCapable() {
        return ParallelLoaders.register(getCaller(1));
    }
因此,如果一个类加载器想要成为并行类加载,那么它的父类也必须支持并行加载。

加载的过程其实不难,就是一个递归load的过程,最终调用findClass和里面的defineClass进行类的加载。






在此回答下面这个博客的几个问题:

大诚挚

1)为什么不直接用className这个字符串充当锁对象   --- 因为存在双亲委派啊,总不能说每个类加载都使用同一个name做锁撒,试想下,如果一次加载从app-ext-bootstrap,走了三次,如果以name为锁对象,那岂不是每一个之后的类加载器都会去竞争啊,不妥,不妥。

2)为什么不是直接new一个Object对象返回,而是用一个map将className和锁对象缓存起来 ---这个其实不难理解,关键在于有没有注意jdk1.7里面的锁的粒度,要注意锁的粒度降低为ClassName层面了, lock = parallelLockMap.putIfAbsent(className, newLock); parallelLockMap 都是返回的是oldValue,那么就意味着两种情况:

1. null,之前没有加过锁,直接返回null,使用newLock。

2. 原有的锁lock,那么依然使用原有的锁,这样锁的竞争只会发生在类名相同的情况下。这里对于相同的类名不去使用不同的锁,应该是有原因的,对于这一块更专业的解释需要问作者了,我只是有点明白但是说不清楚。

不过此处有没有发现其实jdk源码也有不好的地方,比如newLock这个对象,都会生成出来,其实它在map的返回值不是null时根本没有用到。可以优化下。

putIfAbsent
public V putIfAbsent(K key,V value)
如果指定键已经不再与某个值相关联,则将它与给定值关联。
if (!map.containsKey(key)) 
      return map.put(key, value);
else
      return map.get(key);

protected Object getClassLoadingLock(String className) {
    Object lock = parallelLockMap.get(className);
    if (lock != null) {
        return lock;
    }

    lock = new Object();
    Object existLock = parallelLockMap.putIfAbsent(className, lock);
    return existLock != null ? existLock : lock;
}

3)我有个疑问,这里有父类的什么事呢,光注册自己这个类就好了呀。想了半天,还是不明白,是有关于安全吗?哎,大牛就是大牛,哈哈。读者如有明白的,请直言相告。

----------------------因为双亲委派模型的存在,如果此时执行到了父加载器,而父加载器没有开启并行模式,那么它不就锁住了loadClass方法了嘛,本来注册就是想从loadClass方法粒度转移到对象的粒度上,现在由于没有了这个粒度,那么parallelockMap 为 null,getClassLoadingLock 返回的是loadClass这个对象本身,在多线程环境下,不就出现本身是加载一个classname的情况但是锁住了loadClass这个方法了。

参考博客: 阿里中间件博客

参考博客:大诚挚