虚拟机类加载机制
Posted mibloom
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟机类加载机制相关的知识,希望对你有一定的参考价值。
1.类的生命周期
- 验证、准备、解析统称为 连接。
- 加载、验证、准备、初始化、卸载这5个阶段开始执行的顺序固定,但往往是交叉执行,并不会执行完一个再执行下一个。
- 解析某些情况下会在初始化之后,这是为了支持Java的动态绑定。
2.初始化被触发的情况
- 初始化只有在主动引用类时才会被触发,这种情况只有5种。
- 初始化前自然要完成加载、验证、准备这三个动作。
- 遇到关键词 new(实例化一个对象)、getstatic(读取静态字段)、putstatic(设置静态字段,除被final修饰的静态字段,因为他已经在编译阶段就设置了值)、invokestatic(调用静态方法)。
- 对类进行反射调用。
- 父类优先于子类初始化。
- jvm启动时需要一个执行的主类就是含有main方法的类,首先会初始化了该类。
- 使用jdk1.7动态语言时,如果遇到java.lang.invoke.MethodHandle实例解析后结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且该方法所在类没有被初始化过。
- 除主动引用外,其他引用类的方式都不会触发初始化,这称为被动引用。
- 如果子类调用父类的静态字段,那么只会初始化父类,子类不会初始化。
- People[ ] p = new People[10] 这段代码执行并不会初始化People类,而是初始化了一个数组类,数组类由jvm生成。
- 如果使用了另一个类的静态字符串常量,那么在编译期会进行优化,将该字符串放入自己的常量池中,转化为对自身常量的引用,编译之后自身不存在对另一个类的关于该字符串的符号引用。
- 接口初始化和类差不多,但5种主动引用情况中有所不同,接口父类不要求在子接口之前初始化,只有真正使用了父类接口中的成员(如定义的成员)才会初始化父接口。
3.类加载过程
- 类加载全过程分为:加载、验证、准备、解析、初始化这5个阶段所执行的具体操作。
- 所以不触发初始化就不会进行类加载,懒加载也由此而来。
- 加载:这只是类加载的一个步骤,需要完成3件事情
- 通过类的全限定名来获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转化为方法区运行时的数据结构(如字节流中的常量分配到方法区中的常量池中)。
- 在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。(所以Class对象在方法区)
- 验证:验证字节流是否符合class文件格式。
- 准备:为类变量(静态成员)分配内存,设初始值为默认0,null,false值,不会为实例变量分配内存。
- 解析:将常量池中的符号引用替换为直接引用。
- 如 类文件中有个 a.f() 加载时把a放入方法区的常量池中,此时a只是一个字符,解析就是把它和某个类的方法a联系起来。(稍后就会知道这个解析a.f()有可能是发生在初始化开始之后在运行过程中绑定的)
- 虚拟机可以根据需求来判断是类被加载器加载是就对常量池中的某个符号解析还是等到一个符号引用要被使用时在去解析,自然后者发生在初始化开始之后在运行过程中。这里引申出,动态绑定概念。
- 初始化:为各种类变量赋值的过程也可以看成是类构造器的<clinit>方法执行的过程。
- 初始化不会为实例变量赋值,实例变量赋值发生在实例构造器执行<init>方法时,实例构造器执行需要一些关键字触发,如new 既会触发类初始化又会触发执行实例构造器,创建实例对象。
- <cliniit>方法是由编译器把所有静态变量和静态块合并产生,合并前后位置顺序不会变。可以看成Class对象的构造器,他和实例构造器<init>方法不同。
- 由于准备阶段已经为静态变量分配了内存,所以在静态变量定义不一定要在赋值之前发生,但要在静态块中调用的静态变量,则调用一定要在赋值之后。静态语句块只能访问到定义在其之前的变量。
static { i = 5; //System.out.println(i); } static int i;
4.类加载器
- 类加载器做的就是加载这个功能,他把二进制字节流变为一个Class对象。
- 虚拟机设计团队把 从哪里加载class文件这个功能放到了虚拟机外部实现。
- 自定义类加载器可以让我们从任何地方加载class文件,或者在加载之前对class文件执行一些其他的操作。
- 只有相同的class文件经过同一个类加载器加载生成的对象才是相同的对象,也就是
- 对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。
- 类加载器之间的父子关系不是继承而是组合。
-
双亲委派模型
- 双亲委派是一种安全机制,所有的类只加载一次,防止恶意修改jdk中的类。具备了一种带有优先级的层次关系加载过程不会混乱。
- 源码
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; } }
- 首先检查当前加载器是否加载过该类 Class<?> c = findLoadedClass(name);如果加载过则返回Class对象。
- 如果没有加载过则交给父类继续查找 c = parent.loadClass(name, false);父类也采用同样的策略,如果父类还没有查找到则继续交给父类的父类查找。
- 一直查找到BootstrapClassLoad 启动类加载器,他是用C++写的没有父类,于是他去执行c = findBootstrapClassOrNull(name); 本地方法加载类。
- 如果BootstrapClassLoad 不能加载也就是在他所加载的路径下找不到该Class文件,则会返回null,最后return c=null 。回到ExtClassLoader执行findClass(name)方法加载。
- 如果ExtClassLoader不能加载那么返回null让AppletClassLoader继续执行findClass(name)加载,如果还不能加载且没有子加载器也就是没有自定义加载器则抛出ClassNotFoundException异常。
- 如果有自定义类加载器则执行自定义类加载器findClass(name)方法去加载。
-
自定义类加载器
- 自定义类加载器默认父类是AppletClassLoader
- 也可以把自定义加载器下挂在ExtClassLoader下,这样有一点好处就是我们在IDE下创建类后使用自定义加载器加载时不会被AppletClassLoader误加载,因为自定义类编译后默认在classpath路径下由AppletClassLoader加载。
public class MyClassLoader extends ClassLoader { public MyClassLoader() { } public MyClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try{ /** * defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class * @bytes 是class文件生成的byte[] * @ */ Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { } return super.findClass(name); } }
- 带参构造器可以指定父加载器是哪个加载器。ClassLoader.getSystemClassLoader()是AppletClassLoader ,AppletClassLoader .getParent() 是ExtClassLoader。
- 如果父加载器是ExtClassLoader整个加载链就没有AppletClassLoader所以不会被它误加载。
- 继承ClassLoad抽象类,实现findClass方法,返回Class对象,如果找不到或者抛了异常则调用ClassLoad的findClass方法,此方法会抛出ClassNotFoundException异常或者自己实现抛出异常。
-
使用自定义类加载器
public static void main(String[] args) throws Exception{ //使用forName来指定类加载器创建对象 MyClassLoader mcl = new MyClassLoader(); Class<?> clazz = Class.forName("People", true, mcl); Object people =clazz.newInstance(); System.out.println(people); System.out.println(people.getClass().getClassLoader()); //直接使用自定义类加载器来得到Class对象,loadClass会调用findClass方法返回Class对象 Class<?> clazz1 = mcl.loadClass("People"); // 加载一个版本的类 Object obj = clazz1.newInstance(); // 创建对象 System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); }
注意 :这里clazz.newInstance();不能强转 People people = (People)clazz.newInstance(),这是因为clazz.newInstance()是由MyClassLoader()加载,(People)是由AppClassLoader加载,jvm认为他们不是同类型所以报ClassCastException异常
-
破坏双亲委派
- loadClass 是双亲委派的逻辑,只要重写loadClass方法破坏递归调用按自己的需要选择使用哪一个加载器加载即可。
- 如果父类加载器加载的类可能回调子类加载器才能加载的类那怎么办?
- 双亲委派保证了不同层次的类由不同的加载器加载。
- 而且如果当前类中调用了其他类那么只能用当前类所在的类加载器开始加载被调用的类。
- 这也就有可能出现父类加载器所能加载的类中回调了子类加载器才能加载的类,这就十分尴尬。
- 因为任何加载器根本无法加载不在他加载权限范围之外的类。这也正是保障安全的所在之处。
- 所以产生了第二种破坏双亲委派的方法.
- 使用线程上下文类加载器,调用Thread的getContextClassLoader()方法就可以的到一个类加载器,可以通过setContextClassLoader()设置哪个类加载器。而他默认就是应用程序类加载器。有了它就可以加载服务商提供的api.
- Java中所有SPI(Service Provider Inteface服务提供商接口) 都是采取这种方法如JNDI,JDBC,JCE,JAXB,JBI等等。
- 还一种是Java模块化 OSGi 技术 ,每一模块都有一个类加载器,可以实现热替换,类加载器不是树状而是网状。
以上是关于虚拟机类加载机制的主要内容,如果未能解决你的问题,请参考以下文章