JVM学习笔记(二)——虚拟机类加载机制

类加载的时机


  • 类加载的生命周期:
  1. 加载
  2. 验证、准备、解析(统称为连接),其中解析过程不像其他过程那样按部就班,它可以在初始化阶段之后再开始,也就是动态分派的基础,动态绑定。
  3. 初始化
  4. 使用
  5. 卸载
  • 什么时候开始类加载,共只有五种情况
  1. 遇到new,getstatic,putstatic,invokestatic四条字节码指令时。从字面意义上我们就很好理解:(1)new 代表最普遍的new一个对象实例的时候。(2)get/put static表示获取或设置静态属性的时候。(3)invoke,调用静态方法的时候。 这三种情况都会让类初始化。
  2. 在使用java.lang.reflect,对类进行反射调用的时候,如果类没有进行初始化,则需要先触发初始化。举例:比如JDBC那个,打头的肯定是Class.forName("....");调用某某数据库
  3. 当初始化一个类的时候,如果发现父类还没有进行初始化,需要先初始化父类。这个也很正常,复习一下,类的加载顺序:父类静态域->子类静态域->父类构造方法->子类构造方法
  4. 虚拟机启动时,指定的加载主类(含有main方法),会先被初始化
  5. 在JDK 1.7的动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后解析的结果是REF_get/put Static,invokeStatic,句柄所对应的类尚未初始化,则需要先触发其初始化。

  • 接口与类加载的区别:
  1. 我们知道接口可以多继承,所以在接口并不要求其父接口全部完成了初始化,只有在真正用到父接口的时候(如引用接口中定义的常量,复习一下,接口中定义的默认是static final,即常量)才会初始化。

  • 类加载的过程

  • 加载
  1. java类加载的过程相对灵活,可以通过重写一个类加载器的loadClass()方法自定义。这样也导致了纷繁不一的技术,例如动态代理技术、JSP生成servlet的技术、Applet的技术、JAR/EAR/WAR包技术
  2. 而针对于数组类又需要特别遵守以下的规则:(1)如果数组类的组件类型(比如String[]类的组件类型就是String,多个String元素构成了这个数组)是引用类型,那么这个数组将会被在类加载器上的类名称空间被表示。类的一致性函数依赖于类,类加载器,构成联合主键。即(类,类加载器)->类的一致性。(2)如果组件类型不是引用类型(如int[]),那么数组会被标记为引导类加载器关联。(3)数组类的可见性与它的组件类型一致,如果组件类型不是引用类型,那么默认为Public
  3. 加载阶段完成后,二进制字节流就存储在方法区(JDK 1.8移除,改为元空间,下同)。然后在内存中实例化一个java.lang.Class类对象(对于HotSpot来说,Class对象虽然是对象,但存放在方法区中)。这个对象将作为程序访问方法区中的的这些类型数据的外部接口。

  • 验证
  1. 文件验证:验证字节流是否符合Class文件规范
  2. 元数据验证:对字节码描述的信息进行语义分析,是否符合java语法规范
  3. 字节码验证:最复杂的阶段。确定程序语义合法,符合逻辑,不会危害虚拟机。

  • 准备
  1. 正式为类变量分配内存并设置类变量(static修饰的,实例变量将等待对象实例化的时候在堆中初始化)为初始值的阶段。

  • 解析
  1. 类或者接口的解析(类级解析):(1)如果被解析的类/接口是不是数组类型,那么把这个解析对象的全限定名传给相应的类加载器去解析;同时由于元数据验证等操作,可能会加载父类或实现的接口。(2)如果是数组类型,且数组的元素类型为对象,那么回到第一步。(3)解析完成前进行符号引用验证,确认类加载器对类/接口是否拥有访问权限。
  2. 字段解析:(1)如果类本身就包含了需要的字段,那么直接返回直接引用。(2)否则,如果在类中实现了接口,那么会由下往上递归搜索相应的字段。(3)如果类不是Object类,那么会按照继承关系由下往上递归搜索父类是否匹配。(4)还找不到就抛出异常:NoSuchFieldError。 
    interface  A{
        int i=2;
    }
    interface B extends A{
        int i=3;
    }
    class Const implements A{
    
        public  static int i=1;
    
    }
    
    public class Test extends  Const implements B{
    //    public static int i=1;
    
    
        public static void main(String[] args) {
    
            System.out.println(Test.i);
        }
    
    
    }
    interface  A{
        int i=2;
    }
    interface B extends A{
        int i=3;
    }
    class Const implements A{
    
        public  static int i=1;
    
    }
    
    public class Test extends  Const implements B{
    //    public static int i=1;  
    //注意这个地方,上一行被注释掉的话,编译不通过,因为类本身不含有i,进入(2)/(3)步,这个步骤是没有太大先后   //次序的,检测到Test的父类Const和接口B中都有字段i,所以字段i产生了二义性,编译不通过。
    
        public static void main(String[] args) {
    
            System.out.println(Test.i);
        }
    
    
    }
    

  3. 类方法解析:(1)如果发现解析的是个接口,抛出异常。(2)在自身方法中找,(3)在父类方法中找。(4)在实现的接口及其父接口找,如果找到了说明类是一个抽象类,抛出异常。
  4. 接口方法解析:(1-3相同)只是不再寻找父类(接口哪来的父类啊。。。顶天就继承一个父接口),但是会一直找到Objcet类

  • 初始化
  1. 初始化阶段是执行类构造器方法的过程
  2. 方法是由所有类变量的赋值动作静态语句块中的语句合并产生的。
  3. 静态语句块中只能访问到定义在静态语句块之前的变量;之后的变量可以赋值,但不能访问:如下面代码所示。
    public class Test  {
        static{
            i=0;
            System.out.println(i);//这里会提示报错,非法前向引用
        }
        static int i=1;

  4. 接口中不能使用静态语句块。但仍可以有变量初始化的赋值操作,因此接口也会生成方法。只不过接口不像类那样先初始化父类,而是父类变量需要初始化才会初始化。接口的实现类在初始化的时候也不会执行接口的
  5. 在多线程环境下会被虚拟机加上互斥锁。只有一个线程能拿到这个锁,其他线程都会被阻塞。但其他线程在被唤醒后并不会再次执行初始化操作,因为同一个类加载器下,一个类只会被初始化一次。

  • 类加载器 
  1. 判断一个类是否和另一个类相等(引申到两个类实例是否相等),由(类加载器,类本身)共同决定



  • 双亲委派模型
  1. 从双亲委派模型来讲类加载器有三种(虚拟机角度是两种,即启动类和其他):启动类加载器、扩展类加载器、应用程序类加载器。
  2. 启动类加载器:HotSpot中的启动类加载器是用C++实现的。该加载器无法被java程序直接引用,但可以在需要把加载请求委派给引导类时,直接使用null代替,因为在源码中就是这么规定的。。
    /**
         * Returns the class loader for the class.  Some implementations may use
         * null to represent the bootstrap class loader. This method will return
         * null in such implementations if this class was loaded by the bootstrap
         * class loader.
    **/
    public ClassLoader getClassLoader() {
            ClassLoader cl = getClassLoader0();
            if (cl == null)
                return null;
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());	/////
            }
            return cl;
        }
    请注意,在原书中标记的那一行是sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION)笔者使用的版本是JDK 1.8 略有出入
  3. 扩展类加载器:可以直接被使用(属于其他加载器),继承自抽象类java.lang.ClassLoader()
  4. 应用程序加载器:程序中默认的类加载器
  5. 双亲委派模型要求除了顶层的启动类加载器以外的其他加载器都应当有自己的父类加载器(为Null则是指定启动类加载器为父加载器),这里的父子不是继承关系而是组合关系
  6. 双亲委派模型工作流程:收到类加载请求->委派给父类去完成->仅在父加载器反馈自己无法完成时,由子类尝试自己完成(啃老)。所以所有的加载请求最终都应该被传到顶层的启动类加载器中。
  7. 根据这个特性,Object类应该在所有类加载器中都是一致的,因为它都会被委派到顶层的启动类加载器加载。

  • 破坏双亲委派模型
  1. 原因:双亲委派模型并不是一种强制性的要求,而是一种建议的规范。
  2. 第一种破坏可能:因为双亲委派模型产生的时间是JDK 1.2,并不是java一出来就有的,所以难免在出现之前的代码逻辑不符合这个要求。以前是重写ClassLoader中的loadclass方法,在1.2以后提倡重写findClass方法,由loadClass在父加载器无法加载的情况下,加载findClass方法中的加载器。 
  3. 第二种破坏可能:双亲委派模型自身的缺陷,基础类(java的API)中需要调用用户类(比如数据库,文件资源等)。本来基础类是应该,也可以被启动类加载器加载的;但是启动类加载器是不会能处理用户类的。这咋整?
  4. 解决方案:采用线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过Thread.setContextClassLoader()来设置。如果不设置,首先从父线程中继承;父线程中也没有就默认采用系统加载器(应用程序类加载器)加载。
  5. 结果:由JNDI去调用这个类加载器加载需要的资源,当然,这就“本末倒置”了——父加载器请求子加载器去完成加载,也就破坏了双亲委派模型。
  6. 第三种情况:由于“动态性”(代码的热交换,模块热部署)

你可能感兴趣的:(JVM)