关于java的类加载(血泪史)
以下思路都是网上东一点西一点看来的,肯定有错,所以希望大家不要看了【误ww。
java程序的编译和运行过程
java程序从源文件到程序运行经过两大步骤:
1)编译:源文件---<编译器>---字节码(ByteCode);
2)运行:字节码---<java虚拟机(JVM)>---运行程序。
因为java程序既要编译,又要经过JVM的解释运行,所以Java被称为半解释语言
("semi-interpreted" language)。
*图片来自网络
下面我们来分别看看每一点里都发生了什么。
一、编译
java编译过程中,需要经过:
1) 分析和输入到符号表;
2) 注解处理(.class文件中,所有的注解都被清空);
3) 语义分析和生成.class文件,
最后生成了.class文件,也就是所谓的编译后得到的字节码文件。
生成的.class文件包含以下几部分:
1)结构信息:包括class文件格式版本号及各部分的数量与大小的信息;
2)元数据:对应于Java源码中声明与常量的信息,包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池;
3)方法信息:对应Java源码中语句和表达式对应的信息,包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息。
二、过渡
在讲类加载之前,我们先来了解一下懒加载。
Java中的类加载是懒加载。通俗地讲,就是当程序需要使用某个类时才去加载它,不然就让它保持着字节码的形式,不去动它。而且只加载一次。做文件读写是很消耗时间的,类加载中加载和初始化的代价太高。所以JVM的策略是,只要我用不到这个类,那么我就永远不加载这个类。
三、类加载
所谓的类加载,是指把.class文件加载到内存,然后进行校验,解析和初始化,最终形成java类型,并为之创建一个 java.lang.Class对象。类加载分为以下几个步骤:
1) 加载;
2) 链接;
3) 初始化。
下面来具体了解一下。
1、 加载
在加载过程中,虚拟机需要完成3件事情:
①通过指定的类全限定名,获取此类的二进制字节流;
②然后将此二进制字节流的静态存储结构转化为方法区的运行时的数据结构;
③在内存中生成一个代表这个类的Class对象,作为方法区这个类的数据访问入口。
通俗的说,就是JVM把类的字节码信息加载到内存。
2、 链接
链接就是把类的二进制数据合并到JRE(java运行环境)中。链接包括3个部分:
①验证:
验证是虚拟机非常重要的一步,其目的是为了确保class文件的字节流符合java虚拟机自身的要求,不会导致虚拟机崩溃。java语言本身是比较安全的语言,它没有数组越界等情况的发生。But,class语言并不是一定由java语言产生的。甚至于,可以直接使用16进制工具编写class文件。而这些文件就不能保证class文件的规范性。
②准备:
准备阶段就是为类变量(非成员变量)正式分配内存并设置初始值。这个初始值与初始化不是同一个概念。比如:
public static int value = 12;
这个阶段value的值为0 而不是12。value赋值为12的阶段是在初始化的过程中出现的。
java所有的基本类型都赋值为零值。(简单来说就是0 or null,0.0f,false等)
这里可以明确,类的属性是会默认初始值的。而局部变量没有初始值。所以是未定义的。
③解析:
解析是java语言面向对象的基础。
解析的过程是将常量池里面的字符引用替换为直接引用的过程。
3、 初始化
主要对类变量(而非对象变量)的初始化。
声明类变量的初始值 = 静态初始化块 他们是相同的,等效的。都会被当成类的初始化语句,JVM会按照这些语句在程序中的顺序依次执行他们。
JVM初始化一个类包含如下几个步骤:
①假设类还没有被加载和连接,那么先加载和连接该类;
②假设该类的父类还没被初始化,那么先初始化父类。JVM总是最先初始化java.lang.Object类;
③假设类中有初始化语句,则一次执行这些初始化语句。
当程序主动使用任何一个类时,系统会保证该类以及所有父类(直接父类和间接父类)都会被初始化。
•类初始化的时机:
1) 创建类的实例。new,反射,反序列化;
2) 类的静态方法被调用;
3) 访问某类的类变量,即类的静态域被访问,并且它不是常量;
4) 赋值类变量,即类的静态域被赋值;
5) 反射创建某类或接口的Class对象,如Class.forName("Hello");
注意:loadClass调用ClassLoader.loadClass(name,false)方法,没有link,自然没有initialize
6) 初始化某类的子类;
7) 直接使用java.exe来运行某个主类。即cmd java 程序会先初始化该类;
8) 在顶层类中执行assert语句。
•类初始化的规则:
1) 类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化;
2) 超类早于子类和衍生类的初始化;
3) 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化,即使静态域被子类或子接口或者它的实现类所引用;
4) 接口初始化不会导致父接口的初始化;
5) 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前,即静态变量或代码块初始化早于非静态块和域;
6) 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类。
7) 没使用的类根本不会被初始化,因为他没有被使用。
•类中各成员的初始化顺序:
1) 如果类中存在继承关系(像 Son 继承 Father),则首先会初始化导出类(Son)的基类(Father),然后再是导出类;
2) 在基类首先会初始化静态的东西,静态块>静态变量,而且只初始化一次(因为静态的东西都是跟着类的加载而加载的);
3) 随后就是初始化导出类的静态东西,跟基类的静态初始化一样(同上);
4) 初始化基类无参构造器(调用有参就初始化有参构造器);
5) 初始化导出类无参构造器(注意:导出类的成员变量和代码块都是是比构造函数的初始化要早,看输出结果可知)。
下面几张图更形象的解释了这一顺序:
参考资料:
http://www.cnblogs.com/deman/p/5469792.html#_label2
https://blog.csdn.net/yelllowcong/article/details/77620403
https://www.cnblogs.com/jimxz/p/3974939.html
https://blog.csdn.net/geek_sun/article/details/83052637#commentBox
https://www.cnblogs.com/jasonstorm/p/5663864.html
https://blog.csdn.net/super_YC/article/details/71439786
https://blog.csdn.net/xyajia/article/details/80922329