JVM类加载机制

概述

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

java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为java应用程序提供高度灵活性。

1、类加载的时机

类从被加载到内存中开始,到卸载出内存为止,他的整个生命周期包括:加载(Loding)、验证、准备、解析、初始化、使用和卸载7个阶段。
其中验证、准备、解析3个部分统称为连接。

也就是简化为:加载->连接->初始化->使用->卸载

通常这些阶段都是相互交叉地混合进行,通常会在一个阶段中调用、激活另一个阶段。他们的开始顺序一般是这样。解析阶段有点例外,有些情况下解析阶段会在初始化之后再开始,这是为了支持java语言的运行时绑定。

加载时机
java虚拟机规范并没有进行强制约束类加载的时机,这点交给各个虚拟机自由把握。
但对于类初始化明确规定了有且只有这5中情况必须进行初始化(其他加载、验证、准备都在初始化之前完成)

  1. 遇到new、genetic、putstatic或invokestatic这四条字节码指令时,如果类还没有初始化,则需要先触发其初始化。生成这四条字节码最常见的java代码场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。
  3. 初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个类。
  5. 当时用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这句柄所对应的类没有进行过初始化,则需要先触发其初始化。

注意:

  • 通过子类引用父类的静态字段,不会导致子类初始化
  • 通过数组定义来引用类,不会触发此类的初始化
Test t[] = new Test[];// 不会导致Test() 类的初始化
  • 常量在编译阶段存入了常量池,所以也不会引起类初始化。

2.详细解析类加载的各个过程

1)、加载

加载阶段虚拟机需要完成以下三件事

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 讲这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

类加载时只是要求获取此类的二进制字节流,并没有明确要求一定从Class文件获取

  • 从zip包获取,这就是jar文件
  • 从网络获取,applet
  • 运行时计算生成,这种场景最多的就是动态代理技术
  • 由其他文件生成,jsp
  • 数据库中读取

2)、验证

验证主要目的是为了确保Class文件的字节流中包含的信息符合虚拟机要求,不会危害虚拟机自身安全

验证阶段主要完成4个阶段的动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

1、文件格式验证

验证字节码流是否符合Class文件格式规范,这个阶段验证是基于二进制字节流进行验证,经过这个验证字节流才会进内存的方法区进行存储,后边3个阶段验证都是基于方法区存储结构进行,不会直接操作字节流。

2、元数据验证

这个阶段是对字节码描述的信息进行语义分析,保证符合java语法要求,如:

  • 这个类是否有父类(所有类父类都为Object)
  • 这个类有没有继承不被允许继承的类(比如继承了final类)
  • 这个类有没有实现要求实现的方法(比如是否实现了接口方法)
  • 类中字段是否与父类矛盾(比如:覆盖了final方法)

3、字节码校验

这部分是最复杂的,主要是对方法体中语句进行校验分析,保证这些方法运行时不会危害虚拟机

4、符号引用验证

这个阶段发生在虚拟机将符号引用转为直接引用时,这个转化动作将在连接的第三阶段——解析中发生。符号引用验证就是确保能通过这些常量池中的自定义的各种字符串定位到对应的方法获证变量地址。

3)、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量使用的内存都在方法区分配。

这里需要注意分配内存的对象都是static修饰的,不包括普通的实例变量,实例变量是新建类时,一起在堆内存中分配的。

private static int value = 123;

变量value在准备阶段就会在方法区分配到内存,初始值为0,因为还没有执行赋初值方法。把value赋值123的指令为putstatic ,这条指令编译时就会被放在构造方法<clinit>()中,赋初值是在执行这个方法时进行。

但是static final 类型的数据会在准备阶段就赋值。

4)、解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用过程。即将自定义的类名,接口名,方法名等常量替换为对应的内存地址。

5)、初始化

初始化阶段主要是给类变量(即:静态变量)初始化赋值阶段。

初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static {})中的语句合并生成的,编译器收集的顺序是有语句在源文件中出现的顺序所决定的,静态语句中只能访问到静态语句之前的变量,定义在他之后的变量,在前边的静态语句块可以赋值,但不能访问。

static{
    i = 0;    // 可以赋值
    Systom.out.print(i);   // 出错  不能访问
}
static int i=0;

<clinit>()方法与类的构造函数不同,他不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行<clinit>()方法的肯定时Object类。也就是说父类的静态代码块和静态赋值字段先于子类运行。

<clinit>()方法对于类或者接口也不是必须的,当一个类没有静态字段,也就没有对变量的赋值操作,那么编译器就可以不生成<clinit>()方法。

接口中虽然不能使用静态代码块,但接口中也有变量初始化赋值操作,因此接口与类一样也会生成<clinit>()方法。但接口与类不一样的是,接口执行<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当使用父接口定义的变量时,父接口才会初始化。另外,实现接口的类在初始化时一样也不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时初始化一个类,则只会有一个线程去执行,其余阻塞。

3、类加载器

通过一个类的全限定名来获取此类的二进制字节流,这个动作放在虚拟机外部实现,以便让程序自己决定如何去获取所需要的类。实现这个动作的代码称为类加载器。

对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。
当两个类来自同一Class文件,但只要类加载器不一样,那么这两个类就不相等。这里的不相等指的是:Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括instanceof

双亲委派模型

从java虚拟机角度来说,类加载器一共有两种,一种是启动类加载器,这个类加载器用C++实现,是虚拟机一部分;另一种就是所有其他类加载器,这些类加载器都由java语言实现,独立与虚拟机外,并且全部继承抽象类java.lang.ClassLoader。

从Java开发人员的角度可以分为以下几种:

  1. 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在JAVA-HOME\lib目录中的,或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。(HotSpot中这部分是由C++实现)开发者不能直接使用,只能通过双亲委托方式使用。如果用户需要由启动类加载器加载,则只需要使用null代替。
  2. 扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA-Home\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的类路径中的类库,开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器:这个加载器是由java.misc.Launcher$AppClassLoader实现。负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自己定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

这里类加载器之间的父子关系一般不会以继承的方式实现,而都是以组合的方式来引用父类加载器代码。

双亲委派模型
如果一个类加载器收到了类加载请求,首先它不会自己加载这个类,而是把这请求委托给父类加载器去完成,每一层都是如此,最终将会委托给启动类加载器,只有当父类加载器无法完成加载时,子加载器才会尝试自己去加载。
实现双亲委派的代码都在java.lang.ClassLoader的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) {
                    // 父类没有加载成功,使用自己的类加载方法 findClass(name)
                    // 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;
        }
    }

字节定义类加载器加载类,通过覆盖ClassLoader中的loadClass(String name);方法。

/**
 * 将同一class加载两次使用不同的类加载器
 */

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String filename = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream rs = getClass().getResourceAsStream(filename);
                    if(rs==null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[rs.available()];
                    rs.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = myLoader.loadClass("com.wsk.a9.Main");

        System.out.println(obj instanceof com.wsk.a9.Main);
    }
}

根据ClassLoader中的loadClass(String name)方法源码可以看出,要实现自己的类加载器,可以覆盖loadClass(String name)方法,也可以覆盖findClass(name);方法实现,推荐后者。

对于一个类来说只有Class文件一样加载Class文件的类加载器一样,这样类才是一样的,这里的加载类的类加载器指的是最终确定实际加载类的类加载器。双亲委派模型就是从最顶端的父类开始尝试加载Class,最终哪层的类实际加载了Class,这个类就是这个Class的类加载器,而这种从上到下的加载就保证了同一Class加载到内存中的类加载器是一样的,也保证了Class的唯一性。

如下验证了两个不同的类加载器和默认类加载器加载的类是否相等,MyLoader类是自己实现的非双亲委托的类加载器,MyLoader2是自己实现的实现了双亲委托的加载器。使用MyLoader2加载类和Class.forName(className);加载类都使用了默认的类加载器即应用程序类加载器,MyLoader2自己实现的类加载代码并没有执行,实际在内存中只加载了一个com.wsk.a9.SampleClass对象,所以他们加载进来的Class一样。而使用MyLoader加载类,没有使用默认类加载器,使用了自己实现的代码,所以内存中MyLoader加载进来的Class和另外几个加载的Class不一样。

public class Main2 {

    public static void main(String[] args) {
        Main2 m = new Main2();
        m.testClassIdentity();
    }

    public void testClassIdentity() { 
           // 构建两个不同的类加载实例,实现双亲委派模型
            MyLoader2 fscl1 = new MyLoader2(); 
            MyLoader2 fscl2 = new MyLoader2(); 
           MyLoader myloader = new MyLoader();//自定义的类加载器:违反双亲委派模型
           String className = "com.wsk.a9.Sample";// 需要加载的类的全名    
           try { 
               Class<?> class0 = Class.forName(className);// 使用默认的类加载器
               // 同一个类使用两个不同的类加载器实例加载
               Class<?> class1 = fscl1.loadClass(className); // 使用自定义的第一个类加载器实例加载类
               Object obj1 = class1.newInstance(); // 获得这个加载类的实例
               Class<?> class2 = fscl2.loadClass(className); // 使用第二个类加载器实例加载类
               Object obj2 = class2.newInstance(); // 获得第二个加载进来类的实例
               Class<?> loadClass = myloader.loadClass(className);// 使用违反双亲模型的类加载器加载

               System.out.println(class1+"  "+class2+"  "+class1.equals(class2));
               System.out.println(class0+"  "+class0.equals(class1));
               System.out.println(loadClass+"  "+loadClass.equals(class1)+"  "+class0.equals(loadClass));
           } catch (Exception e) { 
               e.printStackTrace(); 
           } 
        }
}

class Sample { 
       private Sample instance; 

       public void setSample(Object instance) { 
           this.instance = (Sample) instance; 
       } 
}

// 该类加载器未实现双亲委派模型
class MyLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String filename = name.substring(name.lastIndexOf(".")+1)+".class";
            InputStream rs = getClass().getResourceAsStream(filename);
            if(rs==null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[rs.available()];
            rs.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            throw new ClassNotFoundException(name);
        }
    }
}

// 该类加载器实现双亲委派模型
class MyLoader2 extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            String filename = name.substring(name.lastIndexOf(".")+1)+".class";
            InputStream rs = getClass().getResourceAsStream(filename);
            if(rs==null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[rs.available()];
            rs.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            throw new ClassNotFoundException(name);
        }
    }
}

运行结果

class com.wsk.a9.Sample  class com.wsk.a9.Sample  true
class com.wsk.a9.Sample  true
class com.wsk.a9.Sample  false  false
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值