java虚拟机类加载机制

Java虚拟机知识点总结

4、虚拟机类加载机制

4.1 概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析,和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

另外需要注意的很重要的一点是:java语言中类型的加载连接以及初始化过程都是在程序运行期间完成的,这种策略虽然会使类加载时稍微增加一些性能开销,但是会为java应用程序提供高度的灵活性。java里天生就可以动态扩展语言特性就是依赖运行期间动态加载和动态连接这个特点实现的。比如,如果编写一个面向接口的程序,可以等到运行时再指定其具体实现类。

4.2 类加载的时机

enter description here 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,但解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始。

必须初始化的五种情况:(主动引用)

(1)使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,已经调用一个类的静态方法的时候。

(2)用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。

(3)当初始化一个类的时候,如果发现其父类没有被初始化就会先初始化它的父类。

(4)当虚拟机启动的时候,用户需要指定一个要执行的主类(就是包含main()方法的那个类),虚拟机会先初始化这个类;

(5)使用Jdk1.7动态语言支持的时候的一些情况。

实例:

public class Parent {
    static {
        System.out.println("父类加载...");
    }
}

public class Child extends Parent{
    static {
        System.out.println("子类加载...");
    }
    
    public static void main(String[] args) {
        System.out.println("子类运行...");
    }
}

运行结果:
父类加载...
子类加载...
子类运行...

三种被动引用(不会触发初始化)

(1)通过子类引用父类静态字段,不会导致子类初始化

(2)通过数组定义引用类,不会触发此类的初始化

(3)(static final修饰)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用定义常量的类,因此不会触发定义常量的类的初始化

public class SuperClass {
    static {
        System.out.println("SuperClass(父类)被初始化了。。。");
    }
    public static int value = 66;
}

public class Subclass extends SuperClass {
    static {
        System.out.println("Subclass(子类)被初始化了。。。");
    }
}

public class Test1 {

    public static void main(String[] args) {

        // 1:通过子类调用父类的静态字段不会导致子类初始化
        // System.out.println(Subclass.value);//SuperClass(父类)被初始化了。。。
        // 2:通过数组定义引用类,不会触发此类的初始化
        SuperClass[] superClasses = new SuperClass[3];
        // 3:通过new 创建对象,可以实现类初始化,必须把1下面的代码注释掉才有效果不然经过1的时候类已经初始化了,下面这条语句也就没用了。
        //SuperClass superClass = new SuperClass();
    }

}
4.3 类加载的过程

1、加载

“加载”“类加载” 过程的一个阶段,切不可将二者混淆 。

加载阶段主要完成以下三步工作:

(1)通过一个类的全限定名来获取定义此类的二进制字节流

(2)将这个字节流所代表的静态存储结构转化为运行时的数据结构

(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

通过类型的完全限定名,产生一个代表该类型的二进制数据流的几种常见形式:

(1)从zip包中读取,成为日后JAR、EAR、WAR格式的基础;

(2)从网络中获取,这种场景最典型的应用就是Applet;

(3)运行时计算生成,这种场景最常用的就是动态代理技术了;

(4)由其他文件生成,比如我们的JSP;

注意: 非数组类加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成。(即重写一个类加载器的loadClass()方法。

2、校验

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成4个阶段的校验工作:文件格式、元数据、字节码、符号引用。可以通过设置参数略过。

3、准备

准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。注:这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

初始值通常是数据类型的零值;对于:public static int value = 123,那么变量value在准备阶段过后的初始值为0而不是123,这时候尚未开始执行任何java方法,把value赋值为123的动作将在初始化阶段才会被执行。对于:public static final int value = 123;编译时Javac将会为value生成ConstantValue(常量)属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

4、解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

两个概念:

(1)符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是符合约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

(2)直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,引用的目标必定已经在内存中存在

5、初始化

类初始化阶段是类加载的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(或者说是字节码)。

在准备阶段,变量已经赋予过一次系统要求的变量,而初始化阶段,则根据程序员的主观计划去初始化类变量和其他资源。

() 方法:

(1)初始化阶段是执行类构造器<clinit>()方法的过程,它是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

(2)父类中定义的静态语句块要优于子类的变量赋值操作

(3)如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()方法。

(4)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

4.4 类加载器

1、概念

虚拟机设计团队把类加载阶段中**“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外设备区实现,以便让应用程序自己决定获取所需的类**。这个动作的模块代码称为“类加载器”。在类层次划分(经典的MVC三层模式),OSGi (Java动态化模块化) , 热部署(各自模块的监听类跟新),代码加密等领域大放异彩。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。如果两个类来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等

2、三种类加载器

(1)启动类加载器(Bootstrap ClassLoader)

这个类加载器负责将存放在**\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的**,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

(2)扩展类加载器(Extension ClassLoader)

这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载**\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库**,开发者可以直接使用扩展类加载器

(3)应用程序类加载器(Application ClassLoader)

这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径**(ClassPath)**上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

3、双亲委派模型

img

要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。这里父子关系通常是子类通过组合关系而不是继承关系来复用父加载器的代码。

双亲委派过程:

如果一个类加载器 收到了类加载的请求,先把这个请求委派给父类加载器去完成(所以所有的加载请求最终都应该传送到顶层的启动类加载器中),只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。

双亲委派模型的好处:使用双亲委派模型来组织类的加载器之间的关系,能使java类随着他的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此java.lang.Object类在程序的各种类加载器环境中都是同一个类

双亲委派模型代码实现 java.langClassLoader 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.
                调用自身的findClass方法
                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;
    }
}

4.4 破坏双亲委派模型

双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。
这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值