类的加载机制

 

1.1类的加载过程

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接

其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

1.1加载

 “加载”(Loading)阶段是“类加载”(Class Loading)过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事情:

  1. 通过类的全名产生对应类的二进制数据流。(如果没找到对应类文件,只有在类实际使用时才抛出错误。二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生成(JSP应用)等)
  2. 分析并将这些二进制数据流转换为方法区(JVM 的架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类(除了 Obecjt 类)。
  3. 创建对应类的 java.lang.Class 实例,作为对方法区中这些数据的访问入口。(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链链接!)。

1.2验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

       Java语言本身是相对安全的语言,使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,编译器将拒绝编译。但是,Class文件并不一定是由Java源码编译而来,可以使用任何途径,包括用十六进制编辑器(如UltraEdit)直接编写。如果直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。

    不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。

    1. 文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。
    2. 元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法……
    3. 字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。
    4. 符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。验证符号引用中通过字符串描述的权限定名是否能找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问

验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

1.3准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

        public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。

 

1.4解析

解析的过程就是确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的Java类被加载。

为类、接口、方法、成员变量的符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。
这一步是可选的。可以在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即使运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!
另外,这一步与之后的类初始化是不冲突的,并非一定要所有的解析结束以后才执行类的初始化。不同的 JVM 实现不同

看下面一段代码:

public class LinkTest {  

   public static void main(String[] args) {      

      ToBeLinked toBeLinked = null;      

      System.out.println("Test link.");  

   }

}

类 LinkTest引用了类ToBeLinked,但是并没有真正使用它,只是声明了一个变量,并没有创建该类的实例或是访问其中的静态域。如果把编译好的ToBeLinkedJava字节代码删除之后,再运行LinkTest,程序不会抛出错误。这是因为ToBeLinked类没有被真正用到。链接策略使得ToBeLinked类不会被加载,因此也不会发现ToBeLinked的Java字节代码实际上是不存在的。如果把代码改成ToBeLinked toBeLinked = new ToBeLinked();之后,再按照相同的方法运行,就会抛出异常了。因为这个时候ToBeLinked这个类被真正使用到了,会需要加载这个类。

1.5初始化

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

        初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

2.类什么时候需要加载

什么情况下需要开始类加载过程的第一个阶段:"加载"。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。

  1. 创建类的实例
  2. 访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
  3. 访问类的静态方法
  4. 反射如(Class.forName("my.xyz.Test"))
  5. 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
  6. 虚拟机启动时,定义了main()方法的那个类先初始化

以上情况称为称对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用”

接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。

2.1被动引用

  1. 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。。对于静态字段,只有直接定义这个字段的类才会被初始化.
  2. 通过数组定义来引用类,不会触发类的初始化
  3. 访问类的常量,不会初始化类
class SuperClass {

    static {

        System.out.println("superclass init");

    }

    public static int value = 123;

}



class SubClass extends SuperClass {

    static {

        System.out.println("subclass init");

    }

}



public class Test {

    public static void main(String[] args) {

        System.out.println(SubClass.value);// 被动应用1

        SubClass[] sca = new SubClass[10];// 被动引用2

    }

}

程序输出:

superclass init

123

从上面的输入结果证明了被动引用1与被动引用2






class ConstClass {

    static {

        System.out.println("ConstClass init");

    }

    public static final String HELLOWORLD = "hello world";

}



public class Test {

    public static void main(String[] args) {

        System.out.println(ConstClass.HELLOWORLD);// 调用类常量

    }

}

程序输出:

hello world

从上面的输入结果证明了被动引用3

3.ClassLoader

Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中,JVM在加载类的时候,都是通过ClassLoaderloadClass()方法来加载class的,loadClass使用双亲委派模式

3.1 ClassLoader源码解析

4.3.1.1loadClass方法

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;

        }

    }

大致内容:

使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String)方法检查这个类是否被加载过 使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。 ClassLoader的子类最好覆盖findClass(String)而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)

**protected Class loadClass(String name, boolean resolve)** 该方法的访问控制符是\`protected\`,也就是说该方法\*\*同包内和派生类中可用\*\* 返回值类型`Class

,这里用到**泛型**。这里使用通配符?作为泛型实参表示对象可以 接受任何类型(类类型)。因为该方法不知道要加载的类到底是什么类,所以就用了通用的泛型。String name要查找的类的名字,boolean resolve,一个标志,true表示将调用resolveClass(c)`处理该类

throws ClassNotFoundException 该方法会抛出找不到该类的异常,这是一个非运行时异常

synchronized (getClassLoadingLock(name)) 看到这行代码,我们能知道的是,这是一个同步代码块,那么synchronized的括号中放的应该是一个对象。

3.1.2getClassLoadingLock方法

protected Object getClassLoadingLock(String className) {

        Object lock = this;

        if (parallelLockMap != null) {

            Object newLock = new Object();

            lock = parallelLockMap.putIfAbsent(className, newLock);

            if (lock == null) {

                lock = newLock;

            }

        }

        return lock;

    }

以上是getClassLoadingLock(name)方法的实现细节,我们看到这里用到变量parallelLockMap ,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock);方法来给刚刚创建好的对象赋值,这个方法的作用我们一会讲。那么这个parallelLockMap变量又是哪来的那

3.1.3 parallelLockMap 变量和构造方法

parallelLockMap 变量:

private final ConcurrentHashMap<String, Object> parallelLockMap;

ClassLoader的构造函数:

private ClassLoader(Void unused, ClassLoader parent) {

        this.parent = parent;

        if (ParallelLoaders.isRegistered(this.getClass())) {

            parallelLockMap = new ConcurrentHashMap<>();

            package2certs = new ConcurrentHashMap<>();

            domains =

                Collections.synchronizedSet(new HashSet<ProtectionDomain>());

            assertionLock = new Object();

        } else {

            // no finer-grained lock; lock on the classloader instance

            parallelLockMap = null;

            package2certs = new Hashtable<>();

            domains = new HashSet<>();

            assertionLock = this;

        }

    }

这里我们可以看到构造函数根据一个属性ParallelLoadersRegistered状态的不同来给parallelLockMap 赋值。 我去,隐藏的好深,好,我们继续挖,看看这个ParallelLoaders又是在哪赋值的呢?我们发现,在ClassLoader类中包含一个静态内部类private static class ParallelLoaders,在ClassLoader被加载的时候这个静态内部类就被初始化。这个静态内部类的代码我就不贴了,直接告诉大家什么意思,sun公司是这么说的:Encapsulates the set of parallel capable loader types,意识就是说:封装了并行的可装载的类型的集合。

3.2ClassLoader详解

上面这个说的是不是有点乱,那让我们来整理一下: 首先,在ClassLoader类中有一个静态内部类ParallelLoaders,他会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么他就给parallelLockMap定义,就是new一个 ConcurrentHashMap<>(),那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么parallelLockMap就不是Null,这个时候,我们判断parallelLockMap是不是Null,如果他是null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象,getClassLoadingLock方法直接返回this,就是当前的加载器的一个实例。如果这个parallelLockMap不是null,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的Object对象,调用parallelLockMapputIfAbsent(className, newLock)方法,这个方法的作用是:首先根据传进来的className,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把他关联的值返回,如果没有关联过值的话,那就把我们传进来的Object对象作为value值,className作为Key值组成一个map返回。然后无论putIfAbsent方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object对象。 这个时候,我们来简单说明一下getClassLoadingLock(String className)的作用,就是: 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。

Class c = findLoadedClass(name); 在这里,在加载类之前先调用findLoadedClass方法检查该类是否已经被加载过,findLoadedClass会返回一个Class类型的对象,如果该类已经被加载过,那么就可以直接返回该对象(在返回之前会根据resolve的值来决定是否处理该对象,具体的怎么处理后面会讲)。 如果,该类没有被加载过,那么执行以下的加载过程

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

}

如果父加载器不为空,那么调用父加载器的loadClass方法加载类,如果父加载器为空,那么调用虚拟机的加载器来加载类。

如果以上两个步骤都没有成功的加载到类,那么

c = findClass(name);

调用自己的findClass(name)方法来加载类。

这个时候,我们已经得到了加载之后的类,那么就根据resolve的值决定是否调用resolveClass方法。resolveClass方法的作用是:链接指定的类。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,类这个被将按照Java™规范中的Execution描述进行链接。

4.总结

4.1java的中的类大致分为三种

  1. 系统类
  2. 扩展类
  3. 由程序员自定义的类

4.2类装载方式,有两种

  1. 隐式装载,程序在运行过程中当碰到通过新的等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
  2. 显式装载,通过class.forname()等方法,显式加载需要的类

4.3类加载的动态性体现

一个应用程序总是由ň多个类组成,Java的程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到JVM中,其它类等到JVM用到的时候再加载,这样的好处是节省了内存的开销,因为java的最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是Java的动态性的一种体现

4.4Java的类装载器

 

的Java中的类装载器实质上也是类,功能是把类载入JVM中,值得注意的是JVM的类装载器并不是一个,而是三个,层次结构如下:

为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型,下面会谈到该模型

4.5类加载器之间是如何协调工作的(双亲委托机制)

JAVA中有三个类加载器,问题就来了,碰到一个类需要加载时,它们之间是如何协调工作的,即Java的是如何区分一个类该由哪个类加载器来完成呢。在这里java采用了双亲委托模型机制,这个机制简单来讲,就是“ 类装载器有载入类的需求时,会先请示其父使用其搜索路径帮忙载入,如果父找不到,那么才由自己依照自己的搜索路径搜索类

image02

下面举一个例子来说明,为了更好的理解,先弄清楚几行代码:

Public class Test{
    Public static void main(String[] arg){
        ClassLoader c  = Test.class.getClassLoader();  //获取Test类的类加载器
        System.out.println(c); 
        ClassLoader c1 = c.getParent();  //获取c这个类加载器的父类加载器
        System.out.println(c1);
        ClassLoader c2 = c1.getParent();//获取c1这个类加载器的父类加载器
        System.out.println(c2);
  }
}

运行结果:

。。。AppClassLoader。。。

。。。ExtClassLoader。。。

Null

可以看出Test是由AppClassLoader加载器加载的,AppClassLoader的Parent加载器是ExtClassLoader,但是ExtClassLoaderParentnull是怎么回事呵,朋友们留意的话,前面有提到Bootstrap Loader是用C ++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java程序代码里试图打印出其内容时,我们就会看到输出为null

4.6类装载器类加载器(一个抽象类)描述一下JVM加载类文件的原理机制

类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在Java的中类装载器把一个类装入JVM,经过以下步骤:

1,装载:查找和导入类文件
2,链接:其中解析步骤是可以选择的
    检查:检查载入的类文件数据的正确性
    准备:给类的静态变量分配存储空间
    解析:将符号引用转成直接引用
3,初始化:对静态变量,静态代码块执行初始化工作

装载类工作由ClassLoder和其子类负责JVM在运行时会产生三个类加载器:根装载器,ExtClassLoader(扩展类装载器)和AppClassLoader,其中根装载器不是类加载器的子类,由C ++编写,因此在java的中。看不到他,负责装载JRE的核心类库,如JRE目录下的rt.jar中,charsets.jar等ExtClassLoader的英文ClassLoder的子类,负责装载JRE扩展目录下的子类包; AppClassLoader负责装载类路径路径下的类包,这三个类加载器存在父子层级关系,即根加载器是ExtClassLoader的父加载器,ExtClassLoader是AppClassLoader的父加载器。在其他情况下使用AppClassLoader加载应用程序的类

1.JVM的类加载机制主要有如下3种。

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
     

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值