该文使用Hotspot JDK1.7
一、类加载器
1、什么是类加载器
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。完成类加载的家伙就是类加载器。
2、都有哪些类加载器
1 package com.jalja.org.base.classLoader; 2 3 public class Test01 { 4 public static void main(String[] args) { 5 ClassLoader loader=Test01.class.getClassLoader(); 6 while(loader!=null){ 7 System.out.println(loader.getClass().getName()); 8 loader=loader.getParent();//获取父类加载器 9 } 10 System.out.println(loader); 11 } 12 }
结果:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null
由输出结果可以看出ExtClassLoader是AppClassLoader的父类,而ExtClassLoader的父类却是null,原因是ExtClassLoader的父类加载器是BootStrap,BootStrap是JVM最底层的引导类加载器用C语言编写的,所以找不到一个确定的返回父Loader的方式,于是就返回null。
为什么要有这个引导类加载器:
类加载器也是java类,他们也需要类加载器加载进入内存,显然必须要有第一个不是java类的类加载器,来完成这个工作,这个正是BootStrap。
JVM中类加载器的结构:
3、各个类加载器的作用
BootStrap ClassLoader(启动类加载器):负责加载存放在D:Program Files (x86)Javajdk1.7.0_79jrelib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
Extension ClassLoader(扩展类加载器):该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载D:Program Files (x86)Javajdk1.7.0_79jrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
Application ClassLoader(应用程序类加载器):该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
4、JVM类加载机制(JVM需要加载一个类时,到底会派出哪个类加载器去执行?)
•全盘负责,当前线程的类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用CLassLoader.loadClass()指定类加载器来载入
•父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。所以我们在开发中尽量不要使用与JDK相同的类(例如自定义一个java.lang.System类),因为父类加载器中已经有一份java.lang.System类了,它会直接将该类给程序使用,而你自定义的类压根就不会被加载。
双亲委派模型:
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,
只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。 双亲委派机制: 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。 2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrap ClassLoader去完成。 3、如果BootStrap ClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载; 4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
双亲委派模型意义:
-系统类防止内存中出现多份同样的字节码
-保证Java程序安全稳定运行
•缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
5、自定义类加载器
二、类的加载
1、类的加载:
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
字节码(.class)文件来源:
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将Java源文件动态编译为.class文件
2、类加载的过程
JVM将javac编译好的class文件加载到内存中,并对该数据进行验证、解析和初始化,最终形成JVM可以直接使用的JAVA类型的过程。
(1)、加载:加载阶段其实就是JVM通过一个类的全限定名来获取其定义的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构且在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。在该阶段我们开发人员可以干预,例如:我们可以指定类加载器来加载该字节数组或者自定义类加载器来加载。
(2)、链接:将java类的二进制代码合并到JVM的运行状态中的过程
a、验证:验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
b、准备:该阶段是在方法区中为类变量(static变量)分配内存并设置类变量初始值。例如:public static int flag=1;该阶段初始化值为0。
c、解析:虚拟机将常量池中的符号引用替换为直接引用的过程。(直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄)
(3)、初始化:初始化为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
- 初始化阶段就是执行类构造器<clinit>()的过程,类构造器<clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先初始化其父类。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
- 当访问一个java 类的静态域时,只有正真申明这个域的类才会被初始化。
类的初始化时机:
- 当虚拟机启动则一定会加载main方法所在的类
package com.jalja.org.base.classLoader; public class A { public static int width=100; static{ System.out.println("静态代码块"); } public static void main(String[] args) { } }
结果:
静态代码块
- 当初始化一个类时,如果其父类未被初始化则一定先初始化其父类
package com.jalja.org.base.classLoader; public class Demo { public static void main(String[] args) { A a=new A(); } } class A extends FatherA{ static{ System.out.println("静态代码块A"); } } class FatherA{ static{ System.out.println("静态代码块FatherA"); } }
结果:
静态代码块FatherA
静态代码块A
- new一个对象的时候类被加载
- 调用类的静态方法和静态成员变量(除了final常量 常量在编译阶段就存入调用类的常量池中,所以无需初始化类)
- 使用java.lang.reflect包中的方法进行反射调用类会被初始化
类的被动引用不会初始化类:
- 调用final常量不会初始化应为 常量在编译阶段就存入调用类的常量池中,所以无需初始化类。
- 通过数组定义的类引用不会初始化类
package com.jalja.org.base.classLoader; public class Demo { public static void main(String[] args) { A[] as=new A[10]; } } class A{ static{ System.out.println("静态代码块A"); } }
无结果输出
- 当访问一个静态变量时,只有正真声明这个静态变量的类才会被初始化(通过子类调用父类的静态变量,子类不会被初始化)
package com.jalja.org.base.classLoader; public class Demo { public static void main(String[] args) { System.out.println(A.width); } } class A extends FatherA{ static{ System.out.println("静态代码块A"); } } class FatherA{ public static int width=100; static{ System.out.println("静态代码块FatherA"); } }
结果:
静态代码块FatherA
100
(4)、卸载
Java虚拟机将结束生命周期的时机:
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止