目录
一、类加载的基础
二、类加载的过程
三、类加载器:分类
四、类加载器:双亲委托模型
五、类加载器:补充
六、初始化时机/主动引用和被动引用【关于实例初始化,参考《Java编程思想05-初始化与清理》】
七、参考
一、类加载的基础
1、类加载的作用:将描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型。
2、动态加载:Java中类型的加载和连接是在运行期完成的,因为称为动态加载;动态加载是JSP、OSGi、动态代理等技术的基础。
3、类的生命周期
(1)加载、验证、准备、解析、初始化、使用、卸载
(2)加载、验证、准备、初始化、卸载:开始的顺序确定(而不是进行或完成的顺序);解析的顺序不确定。
4、加载(类加载的第一阶段)时机:JVM规范并没有强制约束,JVM实现自己把握。
二、类加载的过程
1、加载
“加载”(Loading)阶段是“类加载”(Class Loading)过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。这一步非常灵活,除了通过Class文件获取,还可以:从jar、war包获取;从网络获取;运行时计算生成(如动态代理技术);由其他文件生成(如JSP文件)等
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存生成一个代表这个类的java.lang.Class对象,作为(程序访问)方法区中的这些数据的访问入口。Hotspot实现中,Class对象虽然是对象,但不在Java堆中,而是存放在方法区里面。
加载阶段注意事项:
(1)加载阶段即可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器来完成;数组类的加载与非数组类不同,略。
(2)加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
2、验证 【与准备、解析同属连接阶段】
(1)作用:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。Java语言本身是相对安全的语言,使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,编译器将拒绝编译。但是,Class文件并不一定是由Java源码编译而来,可以使用任何途径。如果直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。
(2)不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证 :文件格式验证、元数据验证、字节码验证和符号引用验证。
文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。
元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法,类中的字段、方法是否与父类矛盾(如覆盖了final字段、出现了不符合规范的重载,如返回值类型不同)……元数据验证验证Class文件的元数据,字节码验证验证Class文件的方法体。
字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如保证操作数栈的数据类型与指令能配合工作(如需要一个int,栈顶是个long,可能有问题),保证跳转指令不会跳转到方法体以外,保证类型转换有效等。由于数据流验证(不是控制流验证)非常复杂,类型推导效率可能较低;HotSpot推出了一个优化,即使用StackMapTable将类型推导转变为类型检查提高效率,具体略。
如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。
符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生;符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。验证符号引用中通过字符串描述的权限定名是否能找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问
(3)验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。
3、准备
(1)准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
(2)public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。
(3)如果类字段的字段属性表中存在ConstantValue属性(static+final+类型要求),则准备阶段便会赋值成ConstantValue属性值。
4、解析
(1)解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。
(2)时机:并未明确规定,在16个用于操作符号引用的字节码指令之前,必须先将所用的符号引用解析;所以虚拟机可以决定在类加载时解析或用到时解析。
16个指令:对字段的4个、对方法的5个invoke、创建3个(new/anewarray/multianewarray)、instanceof、checkcast、ldc、ldc_w
(3)缓存:除invokedynamic,虚拟机可以将第一次解析的结果缓存(运行时常量池中记录直接引用,并标记为已解析),后面发起解析请求时可以直接使用。invokedynamic是动态的,必须在运行到这条指令时才能解析,略。
(4)过程:解析针对的类型包括:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定(即动态)、字符串等。除字符串外,其他类型的解析过程各不相同;可能会引起其他类的加载。
5、初始化
(1)类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
(2)初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动 收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的;收集的顺序由语句在源文件中出现的顺序决定,静态语句块对定义在之前的静态变量可以赋值或访问,对之后的只能赋值不能访问。
public class Test{
static{
i = 0;//没有问题
System.out.println(i);//编译报错
}
static int i = 1;
}
(3)虚拟机保证子类的<clinit>()执行之前,父类的<clinit>()已经执行完毕;因此虚拟机中第一个执行<clinit>()的类肯定是Object。
(4)<clinit>()非必须:类中没有static语句块或对static变量赋值,则不会生成。
(5)接口:不能使用static语句块,但可以有static变量赋值,因此可能生成<clinit>();但执行子接口的<clinit>()不需要先执行父接口的<clinit>(),且执行实现类的<clinit>()也不需要先执行接口的<clinit>()。
(6)多线程:虚拟机保证类的<clinit>()在多线程环境中正确加锁、同步;线程安全但是可能阻塞。
三、类加载器:分类
1、Bootstrap ClassLoader:启动类加载器,负责加载核心类库;是最顶层的类加载器。
Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写(对于HotSpot而言),已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。我们无法访问或控制这个类。
该加载器加载<JAVA_HOME>\lib目录及-Xbootclasspath参数所指定目录中的特定名字的类库;通过如下两个方法,可以查看该加载器加载的类的路径。
//方法1
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
//方法2
System.out.println(System.getProperty("sun.boot.class.path"));
//打印结果
file:/D:/Program%20Files/jdk1.7.0-71/jre/lib/resources.jar
file:/D:/Program%20Files/jdk1.7.0-71/jre/lib/rt.jar
file:/D:/Program%20Files/jdk1.7.0-71/jre/lib/sunrsasign.jar
file:/D:/Program%20Files/jdk1.7.0-71/jre/lib/jsse.jar
file:/D:/Program%20Files/jdk1.7.0-71/jre/lib/jce.jar
file:/D:/Program%20Files/jdk1.7.0-71/jre/lib/charsets.jar
file:/D:/Program%20Files/jdk1.7.0-71/jre/lib/jfr.jar
file:/D:/Program%20Files/jdk1.7.0-71/jre/classes
2、Extension ClassLoader:扩展类加载器,负责加载Java的扩展类库,加载jar的位置如下所示。该加载器加载<JAVA_HOME>\lib\ext目录及-Djava.ext.dirs参数所指定目录中的类库。
System.out.println(System.getProperty("java.ext.dirs"));
//输出
D:\Program Files\jdk1.7.0-71\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
除Bootstrap ClassLoader外,其他加载器,包括Ext、App、自定义等加载器,都直接或间接继承自ClassLoader;ClassLoader是抽象类。ExtClassLoader和AppClassLoader,是Launcher的内部类,外部无法直接访问;只能访问其基类,即ClassLoader。
3、App ClassLoader:系统类加载器,有些地方叫做System ClassLoader,负责加载来自在命令java中的-classpath/-cp或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。
ClassLoader.getSystemClassLoader();//可以得到系统类加载器
System.out.println(System.getProperty("java.class.path"));//可以得到系统类加载器加载库的目录位置
4、自定义ClassLoader:如果没有特别指定,则用户自定义的任何类加载器都将系统类加载器作为它的父加载器;无论定义的加载器继承自ClassLoader、URLClassLoader还是其他子类。
通常,在自定义加载类时,不直接实现ClassLoader,而是继承URLClassLoader,因为后者这个类帮我们做了大部分工作,我们只要简单修改即可。
四、类加载器:双亲委托模型
1、父加载器:除Bootstrap外,所有ClassLoader都有一个父加载器;Bootstrap也可以作为其他ClassLoader的父加载器。父加载器的概念,并不是继承的关系,而是语义上的概念,在双亲委托模型中使用。
ClassLoader c = Test9.class.getClassLoader();
System.out.println(c);
ClassLoader c1 = c.getParent();
System.out.println(c1);
ClassLoader c2 = c1.getParent();
System.out.println(c2);
输出结果为:
sun.misc.Launcher$AppClassLoader@48ffb301
sun.misc.Launcher$ExtClassLoader@b412c18
null
需要注意,ExtClassLoader的getParent()得到的是null,因为Bootstrap ClassLoader并不是普通的Java类;但是语义上来讲,Bootstrap ClassLoader仍是父加载器。
2、双亲委托模型
(1)当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器;这样就形成了一个自上而下的加载机制。
(2)加载过程:首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
(3)保证了类加载器的优先级:如java.lang.Object一定是由启动类加载器加载。
(4)被破坏:略
3、缓存机制:如果 缓存中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入缓存,这就是为什么修改了Class但是必须重新启动JVM才能生效的原因。
五、类加载器:补充
1、作用
(1)通过一个类的全限定名来获取定义此类的二进制字节流
(2)类的唯一标识:JVM中,类的唯一标识是(类名,包名,加载器),由不同加载器加载的类,即使具有相同的类名和包名(同一个Class文件),也不会被认为是同一个实例,不能相互转换。相等,包括Class对象的equals()/isAssignableFrom()/isInstance()方法及instanceof关键字的结果。
2、加载时机:显式加载与隐式加载
(1)显式加载:如classLoader.loadClass()或Class.forName(),或自己实现的findClass()方法等。下例所示,结果很奇怪,两个loadClass并不会打印,原因不详;猜测是某种延迟技术?
public class Test9{
public static void main(String[] args) throws Exception {
ClassLoader.getSystemClassLoader().loadClass("Test8");//不打印
Test9.class.getClassLoader().loadClass("Test8");//不打印
Class.forName("Test8");//打印
}
}
class Test8 {
static{
System.out.println("Test8 加载啦");
}
}
(2)隐式加载:没有显式调用的加载;如加载的类中引用了其他需要加载的类。
(3)当通过loadClass()等加载时,一般是混合使用的:引用的类JVM是隐式加载的。
3、代码编写实践
(1)ClassLoader的主要方法
findClass:将类的字节码加载到内存中。ClassLoader的findClass没有定义加载规则(直接抛异常),因此很多自定义类直接继承URLClassLoader,省掉了很多工作。
defineClass:将byte字节流解析成JVM能够识别的Class对象;
resolveClass:defineClass之后并没有链接;可以调用resolveClass链接,也可以等JVM自动链接
(2)异常举例
ClassNotFoundException:通常是因为显式加载时找不到类
NoClassDefFoundError:通常是因为隐式加载时找不到类
UnsatisfiedLinkError:通常是因为解析native标志的方法时,JVM找不到库文件
(3)获取当前类的加载器的方法
//普通方法中
this.getClass().getClassLoader();
//静态方法
Test.class.getClassLoader();
4、Tomcat的Servlet的加载器:详情略
WebappClassLoader
org.apache.catalina.loader.StandardClassLoader@3a1af7aa
sun.misc.Launcher$AppClassLoader@413f9276
sun.misc.Launcher$ExtClassLoader@34a8a271
5、自定义加载类的一些应用场景
(1)加载自定义的路径下的class文件
(2)加载自定义格式的class文件,如需要先解密或解码等
(3)动态加载类/热部署(如JSP):一般情况下,如果class文件被修改了,重启JVM才能够生效。
改善:为了实现热部署,可以定义加载机制,如果已经加载的class文件被修改了,就重新加载这个类。
问题:已创建的实例难以在不同的类型之间切换。
改善:对象被创建、使用后,立马销毁,jsp更改后不需要重启,就是因为用了动态加载类/热部署。当然,这种用法限制很多,使用场景较少。
【有时,动态加载还有另一个含义:类在使用时加载称为动态加载(Java),与静态加载相对(C++)】
六、初始化时机/主动引用和被动引用
当引用类时,该类未必会初始化。如果引用导致类初始化,称为主动引用;如果不会初始化,称为被动引用。虚拟机规范严格地规定了有且仅有5种情况是对类的主动引用,即必须立即对类进行初始化。
主动引用
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。
(1)new关键字实例化对象
(2)读取类的静态成员变量(ConstantValue除外)
(3)设置类的静态成员变量(ConstantValue除外)
(4)调用类的静态方法
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
3、当一个类初始化的时候,如果发现其父类还没有初始化,则需要先对其父类进行初始化。【接口没有这一点要求,其余4条,接口与类相同】
4、当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
5、jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
疑问:Class.forName()方法为什么会触发初始化呢?虽然指令中有invokestatic,但是是Class对象的static方法啊!
下例中,main()中的每一条语句单独执行时,都会引发Test8的初始化。
public class Test9{
public static void main(String[] args) throws Exception {
Test8 t = new Test8();
int i = Test8.i;
Test8.i = 10;
Test8.f();
Class c1 = Class.forName("Test8");
Test88 test88 = new Test88();
}
}
class Test8 {
public static void f(){};
public static int i;
static{
System.out.println("Test8 初始化啦");
}
}
class Test88 extends Test8{}
被动引用
1、通过子类引用父类的的静态字段,不会导致子类初始化。
2、通过数组定义来引用类,不会触发此类的初始化。
3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。普通static final是不行的。
4、这个是我自己加的:ClassName.class得到Class类型的对象时,不会触发初始化。
下例中,main()中的每一条语句执行时,都不会引发Test8的初始化。
public class Test9{
public static void main(String[] args) throws Exception {
int value = Test8.value;
Test8[] t = new Test8[10];
int j = Test8.j;
Class c2 = Test8.class;
}
}
class Test8 extends Test88{
public final static int j = 10;
static{
System.out.println("Test8 初始化啦");
}
}
class Test88 {
public static int value;
}
七、参考
《深入理解Java虚拟机》第7章
《深入分析Java Web技术内幕》第6章
http://www.codeceo.com/article/java-classloader.html
http://www.tuicool.com/articles/QZnENv
- JVM内存结构 JVM的类加载机制
JVM内存结构: 1.java虚拟机栈:存放的是对象的引用(指针)和局部变量 2.程序计数器:每个线程都有一个程序计数器,跟踪代码运行到哪个位置了 3.堆:对象.数组 4.方法区:字节流(字节码文件) ...
- JVM之类加载机制
JVM之类加载机制 JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 类加载五部分 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这 ...
- JVM的类加载机制全面解析
什么是类加载机制 JVM把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这就是JVM的类加载机制. 如果你对Class文件的结 ...
- 大白话谈JVM的类加载机制
前言 我们很多小伙伴平时都是做JAVA开发的,那么作为一名合格的工程师,你是否有仔细的思考过JVM的运行原理呢. 如果懂得了JVM的运行原理和内存模型,像是一些JVM调优.垃圾回收机制等等的问题我们才 ...
- 一文教你读懂JVM的类加载机制
Java运行程序又被称为WORA(Write Once Run Anywhere,在任何地方运行只需写入一次),意味着我们程序员小哥哥可以在任何一个系统上开发Java程序,但是却可以在所有系统上畅通运 ...
- JVM的类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 类加载的过程: 包括加载.链接(含验证.准备 ...
- 【JVM】类加载机制
原文:[深入Java虚拟机]之四:类加载机制 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 类加 ...
- 深入理解JVM(3)——类加载机制
1.类加载时机 类的整个生命周期包括了:加载( Loading ).验证( Verification ).准备( Preparation ).解析( Resolution ).初始化( Initial ...
- (转) JVM——Java类加载机制总结
背景:对java类的加载机制,一直都是模糊的理解,这篇文章看下来清晰易懂. 转载:http://blog.csdn.net/seu_calvin/article/details/52301541 1. ...
- JVM虚拟机—JVM的类加载机制
1 什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...
随机推荐
- VUE进阶(路由等)
初级教程:http://www.cnblogs.com/dmcl/p/6137469.html VUE进阶 自定义指令 http://cn.vuejs.org/v2/guide/custom-dire ...
- struts2之拦截器
1. 为什么需要拦截器 早期MVC框架将一些通用操作写死在核心控制器中,致使框架灵活性不足.可扩展性降低, Struts 2将核心功能放到多个拦截器中实现,拦截器可自由选择和组合,增强了灵活性,有利于 ...
- js原型二
function Box(name,age){ this.name = name; this.age = age; this.family = ['哥哥',‘姐姐’,‘妹妹’]: } Box.prot ...
- React服务器渲染最佳实践
源码地址:https://github.com/skyFi/dva-starter React服务器渲染最佳实践 dva-starter 完美使用 dva react react-router,最好用 ...
- pt-online-schema-change的原理解析与应用说明
PERCONA提供了若干管理维护MySQL的小工具,集成在 PERCONA Toolkit工具中,有慢查询分析.主从差异对比.主从差异修复及在线表结构修改等工具,个人觉得挺好用的.本文简单 ...
- 蓝桥杯-趣味算式-java
/* (程序头部注释开始) * 程序的版权和版本声明部分 * Copyright (c) 2016, 广州科技贸易职业学院信息工程系学生 * All rights reserved. * 文件名称: ...
- copyWithZone 的使用方法
1.简单复制只能实现浅拷贝:指针赋值,使两个指针指向相同的一块内存空间,操作不安全. 2. Foundation类已经遵守了<NSCopying>和 <NSMutableCopyin ...
- 使用java API操作hdfs--拷贝部分文件到本地
要求:和前一篇的要求正好相反.. 在HDFS中生成一个130KB的文件: 代码如下: import java.io.IOException; import org.apache.hadoop.conf ...
- JAVA虚拟机系列文章
本系列文章主要记录自己在学习<深入理解Java虚拟机-JVM高级特性与最佳实践>的知识点总结,文章内容都是基于周志明所著书籍的总结. 1.Java内存区域与溢出 2.垃圾收集器与内存分配策 ...
- jQuery手风琴制作
jQuery手风琴制作 说起手风琴,想必大家应该都知道吧,简单的来说就是可以来回收缩的这么一个东西,接下来,我就给大家演示一下用jQuery制作一个手风琴菜单! 写jQuery前,我们需要引用一个jQ ...