深入理解java类加载机制
来源:互联网 发布:php 优惠券使用代码 编辑:程序博客网 时间:2024/05/29 10:01
文章引自博主小腊月
一、为什么学习类加载机制
Java的核心是什么?当然是JVM了,所以说了解并熟悉JVM对于我们理解Java语言非常重要,不管你是做Java还是Android,熟悉JVM是我们每个Java、Android开发者必不可少的技能。如果你现在觉得Android的开发到了天花板的地步,那不妨往下走走,一起探索JAVA层面的内容。如果我们不了解自己写的代码是如何被执行的,那么我们只是一个会写代码的程序员,我们知其然不知其所以然。看到很多人说现在工作难找,真是这样吗?如果我们足够优秀,工作还难找吗?如果我们底子足够深,需要找工作吗?找不到工作多想想自己的原因,总是抱怨环境是没有用的,因为你没办法去改变坏境。如果我们一直停留在框架层面,停留在新的功能层面,那么我们的优势在哪里呢?所以说,我们不仅要学会写代码,还要知道为什么这样写代码,这才是我们的核心竞争力之一。这样我们的差异化才能够体现出来,不信?我们走着瞧……我们第一个差异化就是对JVM的掌握,而今天的内容类加载机制是JVM比较核心的部分,如果你想和别人不一样,那就一起仔细研究研究这次的内容吧。二、引子
public class Singleton{ private static Singleton singleton = New Singleton(); public static int counter1; public static int counter2 = 0; private Singleton(){ counter1++; counter2++; } public sttic Singleton getSingleton(){ return singleton; }}
上面是一个Singleton类,有3个静态变量,下面是一个测试类,打印出静态属性的值,就是这么简单。
public class testSingleton(){ public static void main(String[] args){ Singleton singleton = Singleton.getSingleton(); system.out.println("counter1:"+ singleton.counter1); system.out.println("counter2:"+ singleton.counter2); }}
在往下看之前,大家先看看这道题的输出是啥?如果你清楚知道为什么,那么说明你掌握了类的加载机制,往下看或许有不一样的收获;如果你不懂,那就更要往下看了。我们先不讲这道题,待我们了解了类的加载机制之后,回过头看看这道题,或许有恍然大悟的感觉,或许讲完之后你会怀疑自己是否真正了解Java,或许你写了这么多年的Java都不了解它的执行机制,是不是很丢人呢?不过没关系,马上你就不丢人了。 三、关于加载
1.什么是类的加载?
/* *Constructor. Only the Java virtual Mechine creates class *objects*/private class(){}
从上面的Class类的构造方法源码中,我们知道这个构造器是私有的,并且只有虚拟机才能创建这个类的对象。 2.什么时候对类进行加载?
Java虚拟机有预加载功能。类加载器并不需要等到某个类被”首次主动使用”时再加载它,JVM规范规定JVM可以预测加载某一个类,如果这个类出错,但是应用程序没有调用这个类, JVM也不会报错;如果调用这个类的话,JVM才会报错,(LinkAgeError错误)。其实就是一句话,Java虚拟机有预加载功能。四、类加载器
什么是类加载器?类加载器负责对类的加载。1.java自带有三种类加载器
1) 根类加载器,使用c++编写(BootStrap),负责加载rt.jar
2) 扩展类加载器,java实现(ExtClassLoader)
3) 应用加载器,java实现(AppClassLoader) classpath
static class ExtClassLoader extends URLClassLoader
同时我们看看AppClassLoader,它也是在Launcher中static class AppClassLoader extends URLClassLoader
他们同时继承一个类URLClassLoader。关于这种层次关系,看起来像继承,其实不是的。我们看到上面的代码就知道ExtClassLoader和AppClassLoader同时继承同一个类。同时我们来看下ClassLoader的loadClass方法也可以知道,下面贴出源代码:源码没有全部贴出,只是贴出关键代码。从上面代码我们知道首先会检查class是否已经加载了,如果已经加载那就直接拿出,否则再进行加载。其中有一个parent属性,就是表示父加载器。这点正好说明了加载器之间的关系并不是继承关系。2.双亲委派机制
关于类加载器,我们不得不说一下双亲委派机制。听着很高大上,其实很简单。比如A类的加载器是AppClassLoader(其实我们自己写的类的加载器都是AppClassLoader),AppClassLoader不会自己去加载类,而会委ExtClassLoader进行加载,那么到了ExtClassLoader类加载器的时候,它也不会自己去加载,而是委托BootStrap类加载器进行加载,就这样一层一层往上委托,如果Bootstrap类加载器无法进行加载的话,再一层层往下走。上面的源码也说明了这点。if(parent != null){ c = parent.loadClass(name,false);}else{ c = findBootstrapClassOrNull(name);}
3.为何要双亲委派机制?
关于类加载器,我们不得不说一下双亲委派机制。听着很高大上,其实很简单。比如A类的加载器是AppClassLoader(其实我们自己写的类的加载器都是AppClassLoader),AppClassLoader不会自己去加载类,而会委ExtClassLoader进行加载,那么到了ExtClassLoader类加载器的时候,它也不会自己去加载,而是委托BootStrap类加载器进行加载,就这样一层一层往上委托,如果Bootstrap类加载器无法进行加载的话,再一层层往下走。上面的源码也说明了这点。大家可以看看输出的是什么?我们自己定义了一个类加载器,让它去加载我们自己写的一个类,然后判断由我们写的类加载器加载的类是否是MyClassLoader的一个实例。答案是否定的。为什么?因为jvm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加载器加载的,而我们却指定了自己的加载器,当然加载出来的类就不相同了。不信,我们将他的父类加载器都打印出来。在上面代码中加入下面代码:对比一下下面的代码:第一个是我们自己加载器加载的类,第二个是直接new的一个对象,是由App类加载器进行加载的,我们把它们的父类加载器打印出来了,可以看出他们的加载器是不一样的。很奇怪为何会执行classloader==null这句话。其实classloader==null表示的就是根类加载器。我们看看Class.getClassLoader()方法源码:从注释中我们知道了,如果返回了null,表示的是bootstrap类加载器。五、类的连接类的连接有三步,分别是验证,准备,解析。下面让我们一一了解。 1.验证阶段 验证阶段主要做了以下工作-将已经读入到内存类的二进制数据合并到虚拟机运行时环境中去。-类文件结构检查:格式符合jvm规范-语义检查:符合java语言规范,final类没有子类,final类型方法没有被覆盖-字节码验证:确保字节码可以安全的被java虚拟机执行.二进制兼容性检查:确保互相引用的类的一致性.如A类的a方法会调用B类的b方法.那么java虚拟机在验证A类的时候会检查B类的b方法是否存在并检查版本兼容性.因为有可能A类是由jdk1.7编译的,而B类是由1.8编译的。那根据向下兼容的性质,A类引用B类可能会出错,注意是可能。 2.准备阶段java虚拟机为类的静态变量分配内存并赋予默认的初始值.如int分配4个字节并赋值为0,long分配8字节并赋值为0; 3.解析阶段解析阶段主要是将符号引用转化为直接引用的过程。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。六、初始化时机类的加载时机5.2中我们提到了“首次主动使用”这个词语,那什么是“主动使用”呢?主动初始化的6种方式:- 创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。
- 调用类的静态属性或者为静态属性赋值
- 调用类的静态方法
- 通过class文件反射创建对象
- 初始化一个类的子类:使用子类的时候先初始化父类
- java虚拟机启动时被标记为启动类的类:就是我们的main方法所在的类只有上面6种情况才是主动使用,也只有上面六种情况的发生才会引发类的初始化。
- 在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了.这里多说一点,为什么只初始化一次呢?因为我们上面讲到过类加载的最终结果就是在堆中存有唯一一个Class对象,我们通过Class对象找到类的相关信息。唯一一个Class对象说明了类只需要初始化一次即可,如果再次初始化就会出现多个Class对象,这样和唯一相违背了。
- 在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化;
- 在编译时无法确定下来的静态变量(运行时常量),会对类进行初始化;
- 如果这个类没有被加载和连接的话,那就需要进行加载和连接
- 如果这个类有父类并且这个父类没有被初始化,则先初始化父类.
- 如果类中存在初始化语句,依次执行初始化语句.
上面和下面的例子大家对比下,然后自己看看输出的是什么?
第一个输出的是
2
第二个输出的是
FinalTest2 static block
61(随机数)
为何会出现这样的结果呢?
参考上面的Tips2和Tips3,第一个能够在编译时期确定的,叫做编译常量;第二个是运行时才能确定下来的,叫做运行时常量。编译常量不会引起类的初始化,而运行常量就会。
那么将第一个例子的final去掉之后呢?输出又是什么呢?
这就是对类的首次主动使用,引用类的静态变量,输出的当然是:
FinalTest static block
2
那么在第一个例子的输出语句下面添加
FinalTest.x =3;
又会输出什么呢?
大家不妨试试!提示(Tips1)。
七、类的初始化步骤
讲到这里我们应该对类的加载-连接-初始化有一个全局概念了,那么接下来我们看看类具体初始化执行步骤。我们分两种情况讨论,一种是类有父类,一种是类没有父类。(当然所有类的顶级父类都是Object)
没有父类的情况:
- 类的静态属性
- 类的静态代码块
- 类的非静态属性
- 类的非静态代码块
- 构造方法
有父类的情况:
- 父类的静态属性
- 父类的静态代码块
- 子类的静态属性
- 子类的静态代码块
- 父类的非静态属性
- 父类的非静态代码块
- 父类构造方法
- 子类非静态属性
- 子类非静态代码块
- 子类构造方法
在这要说明下,静态代码块和静态属性是等价的,他们是按照代码顺序执行的。
类的初始化内容这样看起来还是挺多的,包括“主动使用”大家可以自己去写一些demo去验证一下。
八、结束JVM进程的几种方式
(1) 执行System.exit()
(2) 程序正常结束
(3) 程序抛出异常,一直向上抛出没处理
(4) 操作系统异常,导致JVM退出
JVM有上面4种结束的方式,我们一一了解下:
(1)我们先来看看第一种方式,找到源代码我们发现:
上面的代码解释了System.exit()方法的作用就是:是中断当前运行的java虚拟机。这是自杀方式。
(2)第二种程序正常结束的方式,我们在运行main方法的时候,运行状态按钮由绿色变红色再变绿色的过程就是程序启动-运行-结束的过程。 那么,我们来看看Android的程序,同样,安卓也有自己的启动方式,也是一个main方法。那么我们的android程序能够一直运行的前提就是我们的main方法一直被执行着,一旦main方法执行完毕,程序就是kill。我们找找源代码才能有更好的说服力;我们找到ActivityThread的main方法
上面的代码都不用看,直接看最后两行代码。执行完Looper.loop()之后,直接抛出了异常。但是我们并没有见到这个异常,说明我们的Looper一直在执行这样保证我们的app不被kill掉。Android就是用这种方式来保证我们的app一直运行下去的。
(3)第三种方式不用过多解释,一直没有处理被抛出的异常,这样导致了程序崩溃。
(4)第四种方式是系统异常导致了jvm退出。其实jvm就是一个软件,如果我们的操作系统都出现了错误,那么运行在他上面的软件(jvm)必然会被kill。
九、结束并回顾
到这里,我们基本都清楚了类的加载机制。那么我们在第一篇文章中开头提到一个例子,我们这里来讲讲输出的是什么,并且为何如此输出,大家坐稳。
下面是我们的测试类TestSingleton
输出是:
counter1=1
counter2=0
why?我们一步一步分析:
1 执行TestSingleton第一句的时候,因为我们没有对Singleton类进行加载和连接,所以我们首先需要对它进行加载和连接操作。在连接阶-准备阶段,我们要讲给静态变量赋予默认初始值。
singleton =null
counter1 =0
counter2 =0
2 加载和连接完毕之后,我们再进行初始化工作。初始化工作是从上往下依次执行的,注意这个时候还没有调用Singleton.getSingleton();
首先 singleton = new Singleton();这样会执行构造方法内部逻辑,进行++;此时counter1=1,counter2 =1 ;
接下来再看第二个静态属性,我们并没有对它进行初始化,所以它就没办法进行初始化工作了;
第三个属性counter2我们初始化为0,因此此时的counter2 =1 ;
3 初始化完毕之后我们就要调用静态方法Singleton.getSingleton(); 我们知道返回的singleton已经初始化了。
那么输出的内容也就理所当然的是1和0了。这样一步一步去理解程序执行过程是不是让你清晰的认识了java虚拟机执行程序的逻辑呢。
那么我们接下来改变一下代码顺序,将
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
又会输出什么呢?为什么这样输出呢?
这个问题留给大家去思考,主要还是理解为什么这样输出才是最重要的。
- 深入理解java虚拟机【类加载机制】
- 深入理解Java类加载机制
- 【深入理解Java虚拟机】类加载机制
- 深入理解java类加载机制
- 深入理解java:类加载机制 和 反射机制
- 深入理解类加载机制
- 深入理解类加载机制
- java虚拟机类加载机制---《深入理解java虚拟机》读书笔记
- Java虚拟机类加载机制---深入理解Java虚拟机
- 深入理解Java虚拟机--Java虚拟机类加载机制
- java类加载机制--《深入理解java虚拟机》
- 深入理解java虚拟机-4 虚拟机类加载机制
- 深入理解Java:类加载机制及反射
- 深入理解Java虚拟机----(六)类加载机制
- 深入理解Java:类加载机制及反射
- 深入理解JVM(四)-Java虚拟机类加载机制
- 深入理解Java:类加载机制与反射
- 深入理解和探究Java类加载机制
- jsp获取session中的值出错
- android 低功耗蓝牙BLE的春天
- 记录web中常见的几种菜单效果
- 算法-Maximum Subarray: a simpler solution
- Centos安装RabbitMQ
- 深入理解java类加载机制
- 《CLR via C#》读书笔记---07 常量和字段
- Prometheus 实战于源码分析之storage
- sublime3中的 Error trying to parser file
- JavaWeb日记——当Shiro遇上Spring
- Java的switch是否支持String作为参数,还支持哪些类型?
- 双目相关
- OKHttpUtils使用介绍
- drawable2