您的位置:首页 > 编程语言 > Java开发

java类加载机制

2015-09-18 16:44 495 查看
首先抛出几个自己写程序经常会产生疑问的问题。

1、java类在什么时候加载?

2、类加载机制?

3、如何加载自定义的java.lang.String?

类加载概述与时机

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

在java中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍耗性能,但是为java提供了高度的灵活性,java里天生可以动态扩展的特性就是依赖运行期动态加载和动态连接的特点实现的。

类从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期包括如下阶段。



其中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照该顺序。但是解析阶段则不一定,它可能在初始化执行,这是为了支持java语音的运行时绑定即动态绑定(多态)。

什么情况下需要开始类加载的第一个阶段:加载?java虚拟机规范没有强行约束,由虚拟机自由把握。但是初始化阶段,虚拟机规范严格规定只有五种情况(加载、验证、准备要在之前开始)

1)使用new实例化对象;get static(读取静态字段);put static(设置静态字段);invoke static(执行静态方法)。被final修饰的静态变量,已在编译期把结果放入常量池,不会初始化类。

2)使用java.lang.reflect

3)初始化一个类的时候,其父类还没有初始化,会先初始化其父类

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

5)使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

我们用如下代码简单的测试下。

父类:

public class SuperClass {
    static{
        System.out.println("SuperClass init!");
    }
    public static int val1 = 123;
    public final static int val2=123;
}

子类:

public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }
}

测试类:

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.val1);
//      SuperClass[] sca = new SuperClass[10];
//      System.out.println(SubClass.val2);

    }
}

第一行打印代码,只会输出“SuperClass init”,而不会初始化子类。对于静态字段,只有直接定义该字段的类才会实例化。第二行打印代码和第三行打印代码都没有初始化相应的类。第二行没有初始化SuperClass是因为数组也是一种类型,它实际上初始化的是虚拟机自动生成的数组类,有数组的属性(如length)。第三行因为是常量 。

类加载的过程

1、“加载”是“类加载”过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事

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

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

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

2、验证,验证是连接的第一步。确保Class文件的字节流中包含的信息复核当前虚拟机的要求,并且不会危害虚拟机的安全。

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

此时进行内存分配的近包括类变量(被static修饰的变量),而不包括实例变量,实例变量将在对象实例化时随州对象一起分配在java堆中。其次,通常初始值时数据类型的零值(final修饰的静态变量,会在该阶段赋值)。如:

public static int value = 123;

变量value在准备阶段过后的初始值是0而不是123,把value赋值为123的动作将在初始化阶段才会执行。

4、解析,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

5、初始化,初始化是类加载的最后一步,前面的类加载过程,除了加载阶段用户可以通过自定义类加载器参与,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正执行java代码。

类加载器

类加载器的作用:通过一个类的全限定名来获取描述此类的二进制字节流。

通常我们将类加载器分为3种

1、启动类加载器(BootStrap ClassLoader) 2、扩展类加载器(Extension ClassLoader) 3、应用程序类加载器(Application ClassLoader)



类加载器的委托机制

当java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?

1)首先当前线程的类加载器去加载线程中的第一个类

2)如果A类中引用了B类,java虚拟机将使用加载类A的类加载器去加载类B

3)还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。

每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的委托模式。类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装载,那就应报告ClassNotFoundException异常。

使用委托机制的优点:使java类加载具有层次优先级,防止java类被不同的类加载器加载,保证了内存中每个类只会被加载一次,增加了程序的安全性和稳定性。

自定义类加载器

类加载器主要是通过依次执行loadClass,findClass,defineClass这3个方法

下面我们定义一个简单的类加载器,该代码参考《深入理解java虚拟机》

public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException{
        System.out.println("name:"+name);
        try{
            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";

            InputStream is = getClass().getResourceAsStream(fileName);
            if(is == null){
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        }catch(IOException e){
            throw new ClassNotFoundException();
        }
    }
}

测试代码

public class ClassLoaderTest {  
  
    public static void main(String[] args) throws Exception {
        excute1();
    }
    static void excute1() throws Exception{
        ClassLoader myLoader = new MyClassLoader();
        Object obj = myLoader.loadClass("com.base.classloading.item3.ClassLoaderTest").newInstance();
        System.out.println(obj instanceof com.base.classloading.item3.ClassLoaderTest);
        ((ClassLoaderTest) obj).printMsg();
    }
    public void printMsg(){
        System.out.println("测试");
    }
}

执行结果:

name:com.base.classloading.item3.ClassLoaderTest
name:java.lang.Object
name:java.lang.ClassLoader
name:com.base.classloading.item3.MyClassLoader
false
Exception in thread "main" java.lang.ClassCastException: com.base.classloading.item3.ClassLoaderTest cannot be cast to com.base.classloading.item3.ClassLoaderTest
    at com.base.classloading.item3.ClassLoaderTest.excute1(ClassLoaderTest.java:11)
    at com.base.classloading.item3.ClassLoaderTest.main(ClassLoaderTest.java:5)

通过运行结果我们可以看到,MyClassLoader除了加载ClassLoaderTest还会在加载相关的类。但是使用自定义的加载器加载并实例化的对象做类型检查返回false,也不能调用所属的方法。

这是因为,系统中存在了两个ClassLoaderTest,一个是MyClassLoader加载的,一个是AppClassLoader加载的。不同的类加载器加载的类,不相等。

现在我们用这个类加载器加载下java.lang.String。因为这个类加载器只能加载同包下的类,需要改写下程序

public Class<?> loadClass(String name) throws ClassNotFoundException{
        System.out.println("name:"+name);
        try{
            String fileName = "/"+name.replace(".", "/")+".class";
            InputStream is = getClass().getResourceAsStream(fileName);
            if(is == null){
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        }catch(IOException e){
            throw new ClassNotFoundException();
        }
    }

运行

ClassLoader myLoader = new MyClassLoader();
        myLoader.loadClass("java.lang.String");

返回结果

name:java.lang.String
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(Unknown Source)
    at java.lang.ClassLoader.defineClassCond(Unknown Source)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at com.base.classloading.item3.MyClassLoader.loadClass(MyClassLoader.java:38)
    at com.base.classloading.item3.ClassLoaderTest.excute1(ClassLoaderTest.java:9)
    at com.base.classloading.item3.ClassLoaderTest.main(ClassLoaderTest.java:5)

可见我们自己定义的类加载器是没有权限加载java.lang下面的类的。

现在还有个问题,我们前面说了,类加载使用了委托机制,既然如此,我们自定义的类加载器加载类的时候,应该会委托到上一层了,为什么会有如上两种异常的情况。

这里主要原因是,这个类加载器,是有些问题的。它太暴力了,直接在loadClass()这个方法强制加载类了。

Jdk1.2之后已不提倡用户再去覆盖loadClass()方法了,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合委托机制的。

我们去掉之前的loadClass方法,重写父类的findClass()方法,在重写实现ClassLoader

@Override
    protected Class<?> findClass(String name)throws ClassNotFoundException {
        InputStream is = null;
        try {
            is = new FileInputStream(name);
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(b,0,b.length);
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            try{
                is.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return super.findClass(name);
    }

现在我们在测试一下之前的代码,发现都能正常的通过了,其实是这些类委托给了AppClassLoader。如果我们要加载这些类,我们可以将这些类放到的class文件放到某一目录,如同tomcat那样。

现在我们测试下把java.lang.String这个class文件放到某一目录,然后加载它

Object obj = myLoader.loadClass("H:/CodeLover/CommonJavaExample/temp/class/ClassLoaderTest.class").newInstance();
        System.out.println(obj.getClass().getClassLoader());
        myLoader.loadClass("H:/CodeLover/CommonJavaExample/temp/class/String.class");

说实话,我一直很希望这是能加载的,因为每次面试我都是这样说可以加载的,面试官笑而不语。结果是

com.base.classloading.item4.MyClassLoader@72093dcd
java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(Unknown Source)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at com.base.classloading.item4.MyClassLoader.findClass(MyClassLoader.java:15)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at com.base.classloading.item4.ClassLoaderTest.excute1(ClassLoaderTest.java:22)
    at com.base.classloading.item4.ClassLoaderTest.main(ClassLoaderTest.java:10)

可见类加载器应该是从class码中判断类的根路径,我们是无法加载java.lang.String的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: