什么是类加载器
类加载器与类的”相同“判断
类加载器种类
双亲委派模型
类加载过程
自定义类加载器
JAVA热部署实现面试
负责读取 Java 字节代码,并转换成java.lang.Class
类的一个实例;数据库
类加载器除了用于加载类外,还可用于肯定类在Java虚拟机中的惟一性。编程
即使是一样的字节代码,被不一样的类加载器加载以后所获得的类,也是不一样的。数组
通俗一点来说,要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,不然这个两个类不“相同”。
这里指的“相同”,包括类的Class对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法、instanceof
关键字等判断出来的结果。安全
启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类
扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类
应用程序类加载器,Application ClassLoader,加载ClassPath中的类库
自定义类加载器,经过继承ClassLoader实现,通常是加载咱们的自定义类网络
类加载器 Java 类如同其它的 Java 类同样,也是要由类加载器来加载的;除了启动类加载器,每一个类都有其父类加载器(父子关系由组合(不是继承)来实现);
所谓双亲委派是指每次收到类加载请求时,先将请求委派给父类加载器完成(全部加载请求最终会委派到顶层的Bootstrap ClassLoader加载器中),若是父类加载器没法完成这个加载(该加载器的搜索范围中没有找到对应的类),子类尝试本身加载。
双亲委派好处
类加载分为三个步骤:加载,链接,初始化;
以下图 , 是一个类从加载到使用及卸载的所有生命周期,图片来自参考资料;
根据一个类的全限定名(如cn.edu.hdu.test.HelloWorld.class)来读取此类的二进制字节流到JVM内部;
将字节流所表明的静态存储结构转换为方法区的运行时数据结构(hotspot选择将Class对象存储在方法区中,Java虚拟机规范并无明确要求必定要存储在方法区或堆区中)
转换为一个与目标类型对应的java.lang.Class对象;
验证
验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证;
准备
为类中的全部静态变量分配内存空间,并为其设置一个初始值(因为尚未产生对象,实例变量将再也不此操做范围内);
解析
将常量池中全部的符号引用转为直接引用(获得类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段能够在初始化以后再执行。
在链接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员本身写的逻辑去初始化类变量和其余资源,举个例子以下:
public static int value1 = 5; public static int value2 = 6; static{ value2 = 66; }
在准备阶段value1和value2都等于0;
在初始化阶段value1和value2分别等于5和66;
什么时候触发初始化
要建立用户本身的类加载器,只须要继承java.lang.ClassLoader类,而后覆盖它的findClass(String name)方法便可,即指明如何获取类的字节码流。
若是要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。
例子:
package classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; class TestClassLoad { @Override public String toString() { return "类加载成功。"; } } public class PathClassLoader extends ClassLoader { private String classPath; public PathClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getData(String className) { String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try { InputStream is = new FileInputStream(path); ByteArrayOutputStream stream = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int num = 0; while ((num = is.read(buffer)) != -1) { stream.write(buffer, 0, num); } return stream.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } public static void main(String args[]) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader pcl = new PathClassLoader("D:\\ProgramFiles\\eclipseNew\\workspace\\cp-lib\\bin"); Class c = pcl.loadClass("classloader.TestClassLoad");//注意要包括包名 System.out.println(c.newInstance());//打印类加载成功. } }
首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是经过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,以后就能够建立该类的实例。默认的虚拟机行为只会在启动时加载类,若是后期有一个类须要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。若是要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,从新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
另外一种友好的方法是建立本身的 classloader 来加载须要监听的 class,这样就能控制类加载的时机,从而实现热部署。
热部署步骤:
一、销毁自定义classloader(被该加载器加载的class也会自动卸载);
二、更新class
三、使用新的ClassLoader去加载class
JVM中的Class只有知足如下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类全部的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方经过反射访问该类的方法
延伸出来问题进行分析:
看到这个题目,不少人会以为我写个人java代码,至于类,JVM爱怎么加载就怎么加载,博主有很长一段时间也是这么认为的。随着编程经验的日积月累,愈来愈感受到了解虚拟机相关要领的重要性。闲话很少说,老规矩,先来一段代码吊吊胃口。
public class SSClass { static { System.out.println("SSClass"); } } public class SuperClass extends SSClass { static { System.out.println("SuperClass init!"); } public static int value = 123; public SuperClass() { System.out.println("init SuperClass"); } } public class SubClass extends SuperClass { static { System.out.println("SubClass init"); } static int a; public SubClass() { System.out.println("init SubClass"); } } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); } }
SSClass
SuperClass init!
123
答案答对了嚒?
也许有人会疑问:为何没有输出SubClass init。ok~解释一下:对于静态字段,只有直接定义这个字段的类才会被初始化,所以经过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
上面就牵涉到了虚拟机类加载机制。若是有兴趣,能够继续看下去。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为链接(Linking)。如图所示。
加载、验证、准备、初始化和卸载这5个阶段的顺序是肯定的,类的加载过程必须按照这种顺序循序渐进地开始,而解析阶段则不必定:它在某些状况下能够在初始化阶段以后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。如下陈述的内容都已HotSpot为基准。
在加载阶段(能够参考java.lang.ClassLoader的loadClass()方法),虚拟机须要完成如下3件事情:
加载阶段和链接阶段(Linking)的部份内容(如一部分字节码文件格式验证动做)是交叉进行的,加载阶段还没有完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动做,仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的前后顺序。
验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
验证阶段大体会完成4个阶段的检验动做:
验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在堆中。其次,这里所说的初始值“一般状况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段事后的初始值为0而不是123.由于这时候还没有开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,因此把value赋值为123的动做将在初始化阶段才会执行。
至于“特殊状况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,因此标注为final以后,value的值在准备阶段初始化为123而非0.
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备极端,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿经过程序制定的主管计划去初始化类变量和其余资源,或者说:初始化阶段是执行类构造器<clinit>()
方法的过程.
<clinit>()
方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句块能够赋值,可是不能访问。以下:
public class Test { static { i=0; System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用) } static int i=1; }
那么去掉报错的那句,改为下面:
public class Test { static { i=0; // System.out.println(i); } static int i=1; public static void main(String args[]) { System.out.println(i); } }
输出结果是什么呢?固然是1啦~在准备阶段咱们知道i=0,而后类初始化阶段按照顺序执行,首先执行static块中的i=0,接着执行static赋值操做i=1,最后在main方法中获取i的值为1。
()方法与实例构造器<init>()
方法不一样,它不须要显示地调用父类构造器,虚拟机会保证在子类<init>()
方法执行以前,父类的<clinit>()
方法方法已经执行完毕,回到本文开篇的举例代码中,结果会打印输出:SSClass就是这个道理。
因为父类的<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操做。
<clinit>()
方法对于类或者接口来讲并非必需的,若是一个类中没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生产<clinit>()
方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操做,所以接口与类同样都会生成<clinit>()
方法。但接口与类不一样的是,执行接口的<clinit>()
方法不须要先执行父接口的<clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()
方法。
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确的加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()
方法完毕。若是在一个类的<clinit>()
方法中有耗时很长的操做,就可能形成多个线程阻塞,在实际应用中这种阻塞每每是隐藏的。
package jvm.classload; public class DealLoopTest { static class DeadLoopClass { static { if(true) { System.out.println(Thread.currentThread()+"init DeadLoopClass"); while(true) { } } } } public static void main(String[] args) { Runnable script = new Runnable(){ public void run() { System.out.println(Thread.currentThread()+" start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread()+" run over"); } }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); } }
运行结果:(即一条线程在死循环以模拟长时间操做,另外一条线程在阻塞等待)
Thread[Thread-0,5,main] start Thread[Thread-1,5,main] start Thread[Thread-0,5,main]init DeadLoopClass
须要注意的是,其余线程虽然会被阻塞,但若是执行()方法的那条线程退出()方法后,其余线程唤醒以后不会再次进入()方法。同一个类加载器下,一个类型只会初始化一次。
将上面代码中的静态块替换以下:
static { System.out.println(Thread.currentThread() + "init DeadLoopClass"); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }
运行结果:
Thread[Thread-0,5,main] start Thread[Thread-1,5,main] start Thread[Thread-1,5,main]init DeadLoopClass (以后sleep 10s) Thread[Thread-1,5,main] run over Thread[Thread-0,5,main] run over
虚拟机规范严格规定了有且只有5中状况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备天然须要在此以前开始):
开篇已经举了一个范例:经过子类引用付了的静态字段,不会致使子类初始化。
这里再举两个例子。
public class NotInitialization { public static void main(String[] args) { SuperClass[] sca = new SuperClass[10]; } }
运行结果:(无)
public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } }
运行结果:
hello world
参考:类加载机制
参考:Java虚拟机类加载机制