Java类加载机制,类加载器的学习
一直想理解Java程序是如何运行的。java的一个类从.java文件转变成.class文件,之后.class文件在Java虚拟机JVM中的加载,连接,初始化的过程。之前学习的一直都是零零星星的知识点,不太完整。今天阅读了几篇写的很不错的文章,结合之前看书对JVM的基本知识的了解,终于对Java类加载机制,类加载器有了深刻的理解,总结一下,便于后面查看。
以一个经典的例子来学习JVM类加载机制再好不过了,例子是一个单例的创建,单例类如下。
public class Singleton { private static Singleton singleton = new Singleton(); public static int counter1; public static int counter2= 0; private Singleton() { counter1++; counter2++; } public static Singleton getSingleton() { return singleton; } }
测试代码:
public class TestSingleton { public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); System.out.println(singleton.counter1); System.out.println(singleton.counter2); } }
如果比较了解JVM类加载机制的小伙伴就很容易知道这个输出结果,不了解的也没关系,这篇文章带你了解JVM类加载机制,这边先不上结果,先说一下类加载的过程。
验证阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1.文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
2.元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
4.符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。
1.类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
2.这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如 public static int value = 1; 在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。
直接引用:通过对符号引用进行解析,找到引用的实际内存地址。
对于初始化阶段,虚拟机规范规定了有以下情况必须立即对类进行“初始化”:
1.使用 new 实例化对象、访问一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。
2.对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)
4.虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
第一种没有父类:
1.类的静态属性和静态代码块(按照编码顺序) 2.类的非静态属性和非静态代码块(按照编码顺序) 3.类的构造方法
第二种有父类:
1.父类的静态属性和静态代码块 2.子类的静态属性和静态代码块 3.父类的非静态属性和非静态代码块
4.父类的构造方法 5.子类的非静态属性和非静态代码块6.子类构造方法
1.启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
2.扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
3.应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的好处是避免重复加载 + 避免核心类篡改,避免重复加载很好理解,通过双亲委派机制询问上层加载器是否加载此类,如果加载过了就不用再重复加载了
避免核心类篡改,比如java.lang.String类,如果你自己定义了java.lang.String类是不会被JVM加载的,保护Java的内部核心的API不被篡改。
下图是自定义类加载器的加载一个类的过程。
刚开始的问题的答案为:
1 0
分析:
1.Singleton singleton = Singleton.getSingleton();这行代码我们可以知道Singleton类的静态方法被访问了,所以要对Singleton类进行加载、验证、准备、解析、初始化。 2.在准备阶段,静态属性分别赋予默认的值。 private static Singleton singleton = null public static int counter1 = 0 public static int counter2 = 0 3.在初始化阶段,将初始化类变量的值,singleton = new Singleton(); 这会执行 Singleton的构造方法,此时counter1 = 1; counter2 = 1; 4.counter1没有初始化的值,所以此时counter1=1;counter2设为0,所以此时counter2 = 0;解答完毕。
参考资料:
https://www.jianshu.com/p/b6547abd0706
https://www.jianshu.com/p/8c8d6cba1f8e
https://www.cnblogs.com/czwbig/p/11127222.html
https://blog.csdn.net/u010312474/article/details/91046318
https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc