上篇分析完一个class文件后,我们再来回答几个问题
一、面试题
1.什么是类的加载
2.什么情况会触发类的加载
3.JVM是如何加载一个类的
4.什么是JVM的类加载机制
5.可以打破双亲委派机制吗?
最后再加一个和安卓有关的小延伸
6.你知道Instant Run的原理吗
二、Java类加载机制
当java文件经过编译器转化为Class文件后,Class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的class文件,并创建对应的class对象.
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构
三、Java类加载过程
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后
1加载
在加载阶段,虚拟机主要完成三件事
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
2校验
此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全
- 1.文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理.这里面主要对魔数、主版本号、常量池等等的校验.
- 2.元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等.
- 3.字节码验证:主要通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的.保证类的方法在运行时不会做出危害虚拟机安全的事
- 4.符号引用验证:主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。
我们还是通过上一篇的Class文件看一下验证失败会出现什么样的报错.通过更改魔数的字母将cafe改成bafe
可以看到 程序报错 ClassFormatError
3准备
为类变量(static)分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)
即在方法区中分配这些变量所使用的内存空间。例如:
public static int value = 123;
此时在准备阶段过后的初始值为0而不是123;
数据类型 | 默认值 |
int | 0 |
boolean | false |
long | 0L |
float | 0.0f |
short | 0(short) |
double | 0.0d |
将value赋值为123的putstatic指令是程序被编译后,存放于类构造器方法之中.特例:
public static final int value = 123;
此时value的值在准备阶段过后就是123。
- 1.给类变量(static)分配内存,但是实例变量不会
- 2.初始值只是数据类型的默认值,而不是代码中被显示赋予的值.
4解析
主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。
5初始化
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
public static final int value = 123;
这一步之后的value的值变为123
java中,对于初始化阶段,有且只有以下五种情况才会要求类
立刻“初始化”(加载,验证,准备,自然需要在此之前开始):
- 当在字节码层面遇到以下指令时,new(对象都要生成了,肯定要初始化了),get/put static(使用静态变量了,肯定要赋值了),invoke static(调用静态方法了都,肯定要为静态量赋值);
- 反射调用。当使用java。lang。reflect中的方法对类进行反射调用;
- 初始化一个类的时候,发现父类还有初始化,那么需要先初始化其父类,(父接口不用立即初始化,只有使用到其常量时,才需要将其初始化);
- 虚拟机需要一个入口,因此主类需要初始化;
- 动态方法解析,解析出方法是其他类的静态方法,那么需要将其初始化。
四、类加载器1、三个类加载器
java语言系统有三个类加载器
Bootstrap ClassLoader
- 主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的.这个类加载器负责加载存放在 \lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够 识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中.
扩展(Extension)类加载器
- 这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩展机制.
应用程序类加载器(Application Class Loader)
- 这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem- ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
一张图来看一下他们的关系
使用代码验证一下:
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(contextClassLoader);
System.out.println(contextClassLoader.getParent());
System.out.println(contextClassLoader.getParent().getParent());
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null
由于BootstrapLoader是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null
2、类加载的三种方式
认识了这三种类加载器,接下来我们看看类加载的三种方式。
- (1) 通过命令行启动应用时由JVM初始化加载含有main()方法的主类。
- (2)通过Class.forName()方法动态加载,会默认执行初始化块
static{}
但是
Class.forName(name,initialize,loader)
中的initialze可指定是否要执行初始化块。
- (3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
下面代码来演示一下
首先我们定义一个TestOutput类
public class TestOutput {
static {
System.out.println("我是静态代码块。。。。");
}
}
然后我们看一下如何去加载
1、使用ClassLoader.loadClass()来加载类,不会执行初始化块
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
loader.loadClass("TestOutput");
2、使用Class.forName()来加载类,默认会执行初始化块
Class.forName("TestOutput");
我是静态代码块。。。。
3、使用Class.forName()来加载类,指定ClassLoader,初始化时不执行静态块
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
Class.forName("TestOutput", false, loader);
上面使用不同的方式去加载类,结果是不一样的。
五、双亲委派机制
双亲委派的原文是"parents delegate"。parents在英文中是“父母”、“双亲”的意思.
如果一个类加载器收到了类加载器的请求.它首先不会去自己去尝试加载这个类,而是把这个请求委派给父加载器去完成.每个层次的类加载器都是如此.因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载.
我们来看下ClassLoader中的loadClass(String name)方法的代码
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else { // 递归终止条件
// 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
// parent == null就意味着由启动类加载器尝试加载该类,
// 即通过调用 native方法 findBootstrapClass0(String name)加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
// 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
1、使用双亲委派的好处
- 比如很多处于rt.jar包中的系统类如java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象
- 避免了重复加载,父类加载过的类,子类不需要再次加载
- 对于核心api避免了用户自己定义,更加安全
2、自定义类加载器
当我们上线了一个新功能,但是class文件却不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象,这时候使用自定义类加载器就可以满足我们的需求了
先使用javac 编译一个Student.class
public class Student {
public void saySth() {
System.out.println("Hello World!");
}
}
将Student.class读取到内存中,生成byte[]数组
private byte[] loadClassFile(String clazzPath) throws IOException {
FileInputStream fis = new FileInputStream(clazzPath);
BufferedInputStream bis = new BufferedInputStream(fis);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 256];
int ch = 0;
while ((ch = bis.read(buffer, 0, buffer.length)) != -1) {
baos.write(buffer, 0, ch);
}
return baos.toByteArray();
}
接下来自定义ClassLoader,使用defineClass(String name, byte[] b, int off, int len)。参数分别是类名称,class文件对应的字节数组,起始位置和终止位置。
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class> c = findLoadedClass(name);
if (c == null) {
c = defineClass(name, data, 0, data.length);
}
return c;
}
但是程序会报错
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at MyClassLoader.loadClass(MyClassLoader.java:52)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at MyClassLoader.loadClass(MyClassLoader.java:52)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at MyClassLoader.main(MyClassLoader.java:14)
因为它的父类是禁止是使用自定义加载器加载的,因此我们要做一下判断.
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class> c = findLoadedClass(name);
if (name.equals("java.lang.Object")) {
ClassLoader parent = getParent();
c = parent.loadClass(name);
}
if (c == null) {
c = defineClass(name, data, 0, data.length);
}
return c;
}
此时,我们利用反射读出所有的方法,同时将java.lang包下的方法交给系统类加载器
接着我们利用反射读出这个对象的所有方法
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
String name = methods[i].getName();
System.out.println(name);
if (name.equals("saySth")) {
methods[i].invoke(student);
}
Class>[] params = methods[i].getParameterTypes();
for (int j = 0; j < params.length; j++) {
System.out.println(params[j].toString());
}
}
因此如果我们
(1)需要遵守双亲委派模型,只需要继承ClassLoader,重写findClass()方法。
(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。
通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
六、总结
现在我们回答一下开头的问题
1、什么是类的加载
JVM把通过类名获得类的二进制流之后,把类放入方法区,并创建入口对象的过程被称为类的加载。经过加载,类就被放到内存里了。
2、哪些情况会触发类的初始化
类在5种情况下会被初始化:
第一,假如这个类是入口类,他会被初始化。
第二,使用new创建对象,或者调用类的静态变量,类会被初始化。不过静态常量不算。
第三,通过反射获取类,类会被初始化
第四,如果子类被初始化,他的父类也会被初始化。
第五,使用jdk1.7的动态语言支持时,调用到静态句柄,也会被初始化。
3、JVM是如何加载一个类的
主要有加载、连接、初始化、使用、卸载
4、什么是JVM的类加载机制
双亲委派机制,类加载器会先让自己的父类来加载,父类无法加载的话,才会自己来加载。
5、可以打破双亲委派机制吗?
可以打破,比如JDBC使用线程上下文加载器打破了双亲委派机制。原因是JDBC只提供了接口,并没有提供实现。