类加载的时机
- 遇到new getstatic setstatic invokestatic这4个字节码指令的时候。即当创建对象,使用类的静态方法,静态变量的时候会对类进行加载。
- 使用java.lang.reflect进行反射调用的时候,会启用类的加载
- 初始化一个类发现其父类还没有进行初始化的时候,首先初始化其父类
- 执行方法的时候,首先加载的是包含main()函数的主类。
特殊情况:1 如果在子类中调用的是父类的静态常量,那么只会加载父类,不会家在子类。
2: 创建数组的时候,并不启用类的加载。因为在虚拟机当中创建数组使用过的 newarray创建出来的是Object的数组。
3:调用常量的时候也不会加载类,因为常量在编译的使用会自动的进入常量池。
类加载的过程
加载
- 通过这个类的全限定名称得到一个类的二进制字节流
- 将这个字节流所代表的静态存储结构转化成为方法去中运行的数据结构
- 在内存中生成一个Class<>的对象,代表对这个类的引用。
首先是根据类的名称去找到这个二进制的文件,然后把这个文件加载到内存当中,最后在内存中创建一个class的对象代表着对它的引用。
验证
- 文件格式验证
- 元数据验证 元数据是指:这个类的格式的检验比如是否有父类,这个类是不是抽象类,这个类中的方法有没有被重写。
- 字节码验证 字节码验证是为了保证满足虚拟机对格式的要求。
- 符号引用验证 能不能找到对应的类,已经对应的方法是否的能够被访问到。
准备
准备阶段市政市委类变量分配内存并设置初始值的阶段,这些变量所使用的内存都在方法区中分配。
加载完成的内容都存储在方法区当中,当时类变量并没有任何的值所以会在准备阶段,为类变量分配内存空间并且赋值。
解析
解析阶段是把符号引用转换成为直接引用,即把符号与对应的内存的地址所对应起来。
- 类和接口的解析:区别在于子类要解析全部的父类的内容,而子接口却不需要解析全部的父接口的内容。只有在需要的时候再去解析。
- 字段的解析:查找对应的字段在类中的存放的位置。
- 类方法的解析:
- 接口方法的解析
初始化
clinit方法会对类中的静态代码块,已经静态内容进行赋值。赋值跟静态代码的输入顺序有关。原因是是因为静态变量被所有类所共享内存空间。
2 clinit会显示的调用父类的构造器,以保证在子类的clinit的执行顺序之前发生。
3 clinit并不是必须的
4 接口中没有静态代码块,当有静态变量但是不同是只有在需要的时候才会对父类接口的内容进行加载。
5 注意如果在静态代码块中加锁的话,会导致进程阻塞。
使用
卸载
class Singleton1{
private static Singleton1 s1 = new Singleton1();
public static int value1;
public static int value2 = 0 ;
private Singleton1(){
value1++;
value2++;
}
public static Singleton1 getSingleton(){
return s1;
}
}
class Singleton2{
public static int value1;
public static int value2 = 0;
public int value3;
private static Singleton2 s2 = new Singleton2();
private Singleton2(){
value1++;
value2++;
}
public static Singleton2 getSingleton(){
return s2;
}
}
public class Test1 {
public static void main(String[] args){
Singleton1 s1 = Singleton1.getSingleton();
Singleton2 s2 = Singleton2.getSingleton();
System.out.println(Singleton1.value1);//1
System.out.println(Singleton1.value2);//0
System.out.println(Singleton2.value1);//1
System.out.println(Singleton2.value2);//1
System.out.println(s2.value3);
}
}
类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在其在Java虚拟机的唯一性,每一个类加载器都有一个独立的命名空间。
双亲委派模型
1 BootsrapClassloader 用于加载<JAVA_HOME>/lib中的,并且被虚拟机识别的类库加载到虚拟机
2 ExtensionClassloader用于加载<JAVA_HOME>/lib/ext目录中的内容,可以直接使用扩展类加载器
3 ApplicationClassLoader 称为系统类加载器,它负责加载用户类路径上的类库。
工作模型:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求为派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求都会传送到顶层的启动类加载起当中,只有父加载器反馈无法加载这个类的时候,子加载器才会尝试去加载。
优点是:保证了对于一些核心的类每一个Java虚拟机所对应的内容是相同。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//父类加载器不为空便去父类加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//为空交由最顶层的BootstrapClasslloader来进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//如果没有找到的话便去 查找当前的CLassLoader看看是否能够进行加载。
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
类变量与实例变量的区别
- 类变量和实例变量
类变量即类成员变量中的静态变量,它们可以通过类名来直接访问。
实例变量是类成员变量中的非静态变量,只有在实例化对象之后通过对象来访问。
- 空间分配的时间不同
类变量是在类加载后的准备阶段在方法区分配内存的。
实例变量是在类实例化为对象的时候在堆中分配内存。
- 初始化
类变量在准备阶段会进行默认初始化,当某些条件满足时候会触发类的初始化。详见《深入理解java虚拟机》的类加载机制。
实例变量在空间分配内存后,虚拟机会将所分配到的内存空间都初始化为零值(不包括对象头)。这一步操作保证了对象的实例字段在java代码中可以不赋初值就可以直接访问,程序能访问到这些字段的数据类型所对应的零值。
对于局部变量,只能显示地进行初始化,否则不能访问该变量的值。
- final关键字
当final关键字作用于类变量时,必须有显示的初始化。在声名对象的语句中初始化,或者在静态代码块中进行初始化。
当final关键字作用于实例变量时,也必须进行显示的初始化。在声名对象的语句中初始化,或者在构造代码块中进行初始化,或者在所有的构造函数中进行初始化。
final修饰的变量只能初始化化一次,因此该变量已经在一种方式中进行过初始化,则不能在另一种方式再进行一次初始化。