1、类的加载时机
类从被加载到java虚拟机内存开始,到类的卸载出内存位置,他的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中的验证、准备、解析3个阶段部分称为连接。
1、当程序运行中遇到new、getstatic、putstatic、invokestatic 四条指令时如果该类没有进行初始化、则需要先进行类的初始化。
2、使用java.lang.reflect包反射调用时候,如果类没有进行初始化,则需要先进行初始化。
3、当初始化一个类的时候发现父类没有初始化则需要先初始化父类 。
4、当虚拟机第一次启动的时候需要制定一个主类(一般是包含main方法的类),虚拟机会先初始化这个类。
总结:当一个类初始化时候需要父类全部初始化,但是一个接口的初始化并不需要父接口的初始化,只是在用到父接口的时候才会对父接口进行初始化。
类的加载过程
1、加载:在加载阶段
(1)通过一个类的全限定名获取一个类的二进制字节流。
(2)将这个字节流代表的静态存储结构转换为方法区运行的数据结构。
(3)在内存中生成这个类的Class对象。作为方法区这个类的访问入口。
其实以上的这三点是根据具体的虚拟机来确定的。
2、验证
验证是连接阶段的第一步,这一笔主要的为了验证class字节流符合java虚拟机的规范,其中不会危害到java虚拟机。
包括:文件格式验证、元数据验证、字节码的验证、符号引用的验证、
3、准备
准备阶段是为类变量分配内存空间,为类变量附初始值的阶段、这些类变量所使用的的内存将会在方法区中进行分配,
例如: private static int count = 123;
变量在准备阶段的初始值为 0 而不是123 ,在类的初始化阶段 count的值才为123
4、解析
解析阶段是虚拟机将常量池中的符号引用转换为直接引用的过程,
5、初始化
在准备阶段变量已经被虚拟机初始化过一次,而在初始化阶段则要根据程序制定的值去初始化变量。从另一个角度去看其实就是执行类构造器<clinit>()方法过程。
<clinit>()方法是由编译器自动收集的类中所有类变量赋值动作和静态语句块static{} 中语句合并产生的,编译器收集的顺序是就是代码在文件中的顺序。
静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量在前面的静态语句块可以赋值但是不能访问,
public class test {
static {
count = 2;// 可以赋值
System.out.println(count);// 但是不能访问// 编译器提示非法先前引用
}
private static int count = 1;
}
<clinit>()方法与类的构造函数不同,他不需要显示的调用父类的够着函数,虚拟机会保证在调用子类的<clinit>()方法之前调用父类的<clinit>()方法,因此虚拟机第一个执行的<clinit>()方法肯定是Object类的方法。
由于父类的<clinit>()方法先执行也就意味着父类的静态代码块要优先于子类的静态代码块。
虚拟就会保证在多线程环境中<clinit>()方法被正确的加锁,同步,如果多个线程同时初始化一个类,那么只会有一个线程执行<clinit>()方法。其他的线程会阻塞状态。在实际的操作中这种阻塞是被隐藏的。
类的加载器
这里重点说下双亲委派的概念:
从java虚拟机的角度来分析只存在两种不同的加载器,一种是启动类加载器,一种是左右 其他类的加载器,这种加载器是java实现的。
启动类加载器(BootStrap ClassLoader):这个类加载器负责加载存放在<java_home>\lib目录中的类。
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录下的jar
应用程序加载器(Application ClassLoader):由这个类加载的事ClassLoader类中getSystemClassLoader()方法的返回值,所以也称为系统类加载器。主要负责加载用户自己类库。
双亲委派的模型的工作过程是:如果一个类收到了类的加载请求首先不会自己去加载这个类,而是把这个请求委派给父加载器去完成,每一个层次的加载器都是这样的,因此所有的类加载请求都会委派给顶层的类加载器中,只有当父加载器反馈自己无法加载这个类的请求时,子类才会尝试加载。这种模型主要是为了让一个类只能由一个类加载器加载,实现类的唯一性。