所谓类加载机制就是JVM虚拟机把Class文件加载到内存,并对数据进行校验,转换解析和初始化,造成虚拟机能够直接使用的Jav类型,即Java.lang.Class。java
类加载的过程主要有装载(Load)、连接(Link)、初始化(Initialize)程序员
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。数据库
类加载器并不须要等到某个类被“首次主动使用”时再加载它,JVM规范容许类加载器在预料某个类将要被使用时就预先加载它,若是在预先加载的过程当中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)若是这个类一直没有被程序主动使用,那么类加载器就不会报告错误。api
加载.class文件的方式数组
从本地系统中直接加载
经过网络下载.class文件
从zip,jar等归档文件中加载.class文件
从专有数据库中提取.class文件
将Java源文件动态编译为.class文件
连接这一过程又能够分为验证(Validate)、准备(Prepare)、解析(Resolve)三个阶段tomcat
验证(Validate)
保证被加载类的正确性。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。安全
准备(Prepare)
为类的静态变量分配内存,并将其初始化为默认值网络
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有如下几点须要注意数据结构
这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
这里所设置的初始值一般状况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段事后的初始值为0,而不是3,由于这时候还没有开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,因此把value赋值为3的动做将在初始化阶段才会执行。多线程
这里还须要注意如下几点
对基本数据类型来讲,对于类变量(static)和全局变量,若是不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来讲,在使用前必须显式地为其赋值,不然编译时不经过。
对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,不然编译时不经过;而只被final修饰的常量则既能够在声明时显式地为其赋值,也能够在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
对于引用数据类型reference来讲,如数组引用、对象引用等,若是没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
若是在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值
若是类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。假设上面的类变量value被定义为: public static final int value = 3;,编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。咱们能够理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
解析(Resolve)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,能够是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
对类的静态变量,静态代码块执行初始化操做。准备阶段和初始化阶段看似有点矛盾,实际上是不矛盾的,若是类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行连接的验证这一步骤,验证经过后准备阶段,给a分配内存,由于变量a是static的,因此此时a等于int类型的默认初始值0,即a=0,而后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
声明类变量是指定初始值,也就是直接给类别量一个值
使用静态代码块为类变量指定初始值
初始化,主要是执行类的类构造器< clinit>()方法,JVM会将类中的静态代码块和静态变量的赋值语句放在该方法里面。
JVM初始化步骤
一、假如这个类尚未被加载和连接,则程序先加载并连接该类
二、假如该类的直接父类尚未被初始化,则先初始化其直接父类
三、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会致使类的初始化,类的主动使用包括如下六种:
– 建立类的实例,也就是new的方式
– 访问某个类或接口的静态变量,或者对该静态变量赋值
– 调用类的静态方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某个类的子类,则其父类也会被初始化
– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
类初始化方法clinit:JVM经过Classload进行类型加载时,若是在加载时须要进行类的初始化操做时,则会调用类型、的初始化方法。 clinit方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句中能够赋值,可是不能访问。
clinit方法对于类或接口来讲并非必须的,若是一个类中没有静态语句块,也没有对类变量的赋值操做,那么编译器能够不为这个类生成clinit方法。
接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操做,所以接口与类同样会生成clinit方法。可是接口鱼类不一样的是:执行接口的clinit方法不须要先执行父接口的clinit方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也同样不会执行接口的clinit方法。
虚拟机会保证一个类的clinit方法在多线程环境中被正确地加锁和同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit方法,其余线程都须要阻塞等待,直到活动线程执行clinit方法完毕。若是在一个类的clinit方法中有耗时很长的操做,那就可能形成多个线程阻塞,在实际应用中这种阻塞每每是很隐蔽的。
说到 clinit方法,就不得不说一下对象实例化方法init。
对象实例化方法init:Java对象在被建立时,会进行实例化操做,给成员变量赋值。该部分操做封装在init方法中,而且子类的init方法中会首先对父类init方法的调用。
clinit 方法和init 方法的区别
init和clinit方法执行时机不一样
init是对象构造器方法,也就是说在程序执行new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
init和clinit方法执行目的不一样
init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化
clinit 和init方法的数量不一样
编译器最多只为一个类生成一个clinit方法,若是类中没有静态成员或者代码块的话,就不有clint方法。而init方法,类中一个构造函数就对应一个init方法
类加载器负责加载全部的类,其为全部被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个惟一的标识同样,一个载入JVM的类也有一个惟一的标识。在Java中,一个类用其全限定类名(包括包名和类名)做为标识;但在JVM中,一个类用其全限定类名和其类加载器做为其惟一标识。例如,若是在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不一样的、它们所加载的类也是彻底不一样、互不兼容的。
JVM预约义有三种类加载器,当一个 JVM启动的时候,Java开始使用以下三种类加载器:
启动类加载器(Bootstrap ClassLoader):负责加载存放在JDKjrelib(JDK表明JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,而且能被虚拟机识别的类库(如rt.jar,全部的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是由C++实现的,没有对应的Java对象,所以在Java中只能用null代替。
扩展类加载器(Extension ClassLoader):负责加载java平台中扩展功能的一些jar包,包括JDK/jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。,开发者能够直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。
自定义类加载器 Custom ClassLoader: 经过继承java.lang.ClassLoader根据自身须要自定义ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
几种类加载器的层次关系以下图所示
这种层次关系称为类加载器的双亲委派模型。咱们把每一层上面的类加载器叫作当前层类加载器的父加载器,固然,它们之间的父子关系并非经过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在JDK1.2期间被引入并普遍应用于以后几乎全部的Java程序中,但它并非一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
双亲委派模型的工做流程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即没法完成该加载,子加载器才会尝试本身去加载该类。
双亲委派机制的优点:采用双亲委派模式的是好处是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系,经过这种层级关能够避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设经过网络传递一个名为java.lang.Integer的类,经过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会从新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样即可以防止核心API库被随意篡改。