以前的《java基础:内存模型》当中,咱们大致了解了在java当中,不一样类型的信息,都存放于java当中哪一个部位当中,那么有了对于堆、栈、方法区、的基本理解之后,今天咱们来好好剖析一下,java当中的类加载机制(其实就是在美团的二面的时候,被面试官问的懵逼了,特意来总结一下,省得下次再那么丢人 T-T)。html
咱们都知道,在java语言当中,猴子们写的程序,都会首先被编译器编译成为.class文件(又称字节码文件),而这个.class文件(字节码文件)中描述了类的各类信息,字节码文件格式主要分为两部分:常量池和方法字节码。那么java的编译器生成了这些.class文件以后,又是怎么将它们加载到虚拟机当中的呢?接下来咱们就好好讨论一下这个事情。java
参考链接:http://www.cnblogs.com/xrq730/p/4844915.html (感受这个博主写的很适合greenHand看,因此就参考着本身总结了一份)程序员
类的生命周期:面试
首先咱们来看看,在java当中一个类的完整的生命周期,主要包括了如下七个部分:1.加载、2.验证、3.准备、4.解析、5.初始化、6.使用、7.卸载。在这7个阶段当中,前5个阶段加起来,就是类加载的全数组
过程,如图所示。而验证、准备、解析,三个阶段又能够被称为链接阶段。除此以外,类加载过程中的五个阶段,除了解析阶段,其余都是顺序开始的,但不是顺序执行的,也就是说在过程中是能够并行的,好比在验证开始后,还未结束,可能就会开始准备阶段。而解析阶段不必定在这个顺序当中的缘由是由于,它在某些状况下能够初始化阶段以后在开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。网络
注意:这里出现了一个新概念,叫绑定,简单解释了一下什么叫绑定吧,在java当中的绑定定义为:指的是把一个方法的调用与方法所在的类(方法主体)关联起来,主要分为了静态绑定和动态绑定。多线程
静态绑定:即在程序执行方法以前就已经被绑定,简单来讲再编译期就进行绑定,在java当中被final、static、private修饰的方法,以及构造方法都是属于静态绑定,即编译期绑定。eclipse
动态绑定:又称运行时绑定,在运行时根据具体对象的类型进行绑定,在java当中,几乎除了知足静态绑定的方法以外,全部方法都是动态绑定的(java当中运行时多态的重要实现根据)ide
1.加载函数
在java的类加载的过程中的加载,通常分为两种:第一种,预加载,指的是虚拟机启动的时候,加载JDK路径下的 lib/rt.jar 下的.class文件,在这个jar包当中包含着基础的java.lang.*、java,util.*等基础包,他们随着虚拟机一块儿被加载。第二种,运行时加载,指虚拟机在须要用到某一个类的时候,会先去内存当中查看有没有这个类对应的.class文件,若是没有会按照类的全限定名来加载这个类。而在咱们的文章当中,主要讨论第二种运行时加载。
注意:这里提到了一个全限定名,指的是包含着这个类所在的包的名称,即好比 bjtu.wellhold.test.testclass 这样的名称,有包含绝对路径的含义。
其实在加载阶段,主要作了三件事情:
1.获取.class文件的二进制字节流。
2.将类信息,静态变量,方法字节码,常量等这些.class文件中的内容放入到方法区当中(在《java基础:内存模型》当中已经讲解过)
3.在堆当中生成一个表明这个.class文件的java.lang.Class对象,做为方法区这个类的各类数据的访问入口。(HotSpot虚拟机比较特殊,这个Class类放在了方法区当中)
而程序员来讲,最可控的地方就在于第一件事,因为虚拟机并无规定二进制字节流要从哪而来,因此再这个部分,二进制字节流的来源能够来自如下源:
1)从jar、war格式等来,
2)从网络当中来,如Applet
3)运行时计算获得,如动态代理技术。
4)由其余文件生成,如JSP。
2.验证
因为在加载阶段的过程中,并无严格规定二进制字节流须要经过Java源码编译而来,因此验证阶段的主要目的是在加载阶段获取获得的二进制字节流中包含的信息符合当前虚拟机的要求,而且不会致使虚拟机收到危害。主要分为了如下几种形式的验证:
1.文件格式验证:提一点,也许在安装某个开源中间件的时候,须要JDK多少版本以上,这是由于在文件格式验证的过程中,有一个部分就是对.class文件的版本号,高版本的JDK能够向下兼容之前版本的.class文件,可是低版本的JDK则不能运行高版本的.class文件,即便文件格式为发生任何变化,虚拟机也会拒绝执行。
2.元数据验证。
3.字节码验证。
4.符号引用验证。
注意:验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3.准备
这个阶段当中,会正式的为类当中那些被 static修饰的变量分配内存,而且设置其初始值,而这些变量都会存在方法区当中。
注意:
1)这个时候分配内存的,都是静态变量,即被Static修饰的变量,而非实例变量。
2)这个阶段赋初始值的变量指的是那些不被final修饰的static变量,好比"public static int value = 123;",value在准备阶段事后是0而不是123,给value赋值为123的动做将在初始化阶段才进行;好比"public static final int value = 123;"就不同了,在准备阶段,虚拟机就会给value赋值为123。
4.解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。主要针对 类或接口的全限定名、字段的名称和描述符、方法的名称和描述符 的符号引用进行。
注意:这里提到了一个符号引用和直接引用的概念,简单解释一下。
1.符号引用:符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时可以无歧义的定位到目标便可。符号引用与虚拟机的内存布局无关,引用的目标并不必定加载到内存中。好比org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,所以只能使用符号org.simple.Language来表示Language类的地址。
2.直接引用:1)直接指向目标的指针,好比Class对象、static变量,static方法,都是指向方法区的指针。2)相对偏移量(从对象的映像开始算起到这个实例变量位置的偏移量。实例方法的直接引用多是方法表的偏移量)。3)一个能间接定位到目标的句柄
5.初始化
初始化是类加载过程中的最后一步,在这个过程中才真正执行类中定义的java程序代码(或者说字节码),其实简单来讲,初始化阶段作的事就是给static变量赋予用户指定的值以及执行静态代码块。它是一个执行类构造器<clinit>()方法的过程。
这里简单说明下<clinit>()方法的执行规则:
Java虚拟机规范严格规定了有且只有5种场景必须当即对类进行初始化,如下介绍4种场景,也称为对一个类进行主动引用(有一种因为参考的帖子没有写出,本身也就并无得知)
一、使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰的静态字段除外)、调用一个类的静态方法
二、使用java.lang.reflect包中的方法对类进行反射调用的时候
三、初始化一个类,发现其父类尚未初始化过的时候
四、虚拟机启动的时候,虚拟机会先初始化用户指定的包含main()方法的那个类
注意:经过数组定义引用类,不会触发此类的初始化
class A { public static int a=1; static { a=2; System.out.println("father class:"+a); } } class B extends A { static{ System.out.println("this is the B"); } public static int b=a; } public class ClassLoad { static { System.out.println("this is the main"); } public static void main(String[] args) { System.out.println("B class :"+B.b); } }
结合这个例子,咱们来分析一下上述当中所说的主动引用和<clinit>()方法的规则,在这个例子当中,JVM首先会初始化ClassLoad这一个类,由于该类包含了main函数(主动引用第四种场景),以后在main函数当中,执行B.b这一行代码的时候,因为涉及到了对static变量的赋值,因此B类也会被初始化,可是并非当即初始化,而是查看B所继承的父类,即A类是否被初始化(<clinit>规则2),这时候发现A类并无被初始化,则首先初始化A类,因此能够看到上述初始化的顺序为:ClassLoad-》A-》B,运行结果以下:
this is the main father class:2 this is the B B class :2
自定义类加载器
要本身实现类加载器以前,咱们首先看看在java jdk1.8当中的ClassLoad是怎么实现loadClass方法的:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { //首先,先查找当前全限定名的类是否已经被加载。 Class c = findLoadedClass(name); //若是没有被加载 if (c == null) { try { //当前加载器还有父类加载器(若是父加载器不是null,不是Bootstrap //ClassLoader),则经过委托父类去加载 if (parent != null) { c = parent.loadClass(name, false); } //一直递归找到最上级的父类加载器,再先经过父类去加载 else { c = findBootstrapClass0(name); } } //若是父类加载不到,则再经过本身来加载 catch (ClassNotFoundException e) { c = findClass(name); } } //根据需求解析。 if (resolve) { resolveClass(c); } return c; }
整个JDK的loadclass的源码和源码解读都在注释中体现了。那么咱们要本身实现一个类加载器,能够有如下两种方式:
一、若是不想打破双亲委派模型,那么只须要重写findClass方法便可
二、若是想打破双亲委派模型,那么就重写整个loadClass方法
固然,咱们自定义的ClassLoader不想打破双亲委派模型,因此自定义的ClassLoader继承自java.lang.ClassLoader而且只重写findClass方法。
首先咱们作一个实体类,叫Person:
public class Person { public String toString() { return "I am a person, my name is " + name; } }
将这个实体类编译出来的.class文件放到D盘根目录下(eclipse当中的工程bin目录下,能够找到这个类的.class文件)
而后在手动编写一个自定义类加载器MyClassLoad,它继承了ClassLoad:
package wellhold.bjtu.classload; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; public class MyClassLoader extends ClassLoader { public MyClassLoader() { // TODO Auto-generated constructor stub } public MyClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); } return super.findClass(name); } private File getClassFile(String name) { File file=new File("d:/Person.class"); return file; } private byte[] getClassBytes(File file) throws Exception { FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } }
主要包括了从磁盘当中读取.class文件,而后重写了findClass方法,方法当中经过defineclass方法,将io读取到的Byte流转换成Class对象。以后再看看咱们的测试方法:
public class TestMyClassLoader { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { // TODO Auto-generated method stub MyClassLoader mcl = new MyClassLoader(); Class<?> c1 = Class.forName("wellhold.bjtu.classload.Person", true, mcl); Object obj = c1.newInstance(); System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); } }
在测试方法当中,经过反射当中的forName方法,指定类的全限定名和类加载器,而且将初始化设定为TRUE,加载到类后,打印:
this is the Person wellhold.bjtu.classload.MyClassLoader@6d06d69c
说明咱们的类被成功的加载进来了。自定义类加载器完成了任务。