类装载过程

类装载过程详解

==在下文中请谨慎区分“装载”与“加载”==

系统装载class类型可以分为加载、连接、初始化。而连接又可以分为验证、准备、解释三个阶段。如下图所示:

image

前言

虚拟机不会无故装载Class,只有在必须使用某类的时候才会加载该Class.必须使用Class情况有下面几种:

  • new关键字创建对象实例的时候, 或者通过反射、克隆、反序列化的时候。
  • 调用类的静态方法即使用invokestatic字节码的时候。
  • 使用类或者借口的静态字段(final关键字标注的字段除外) 比如字节码 putstatic、getstatic。
  • 使用java.lang.refelct包中的方法反射该Class的时候。
  • 使用子类的前提是初始化父类。
  • 启动虚拟机的时候含有main()方法的类。

    加载类

  1. 通过类的全名获取类的二进制数据流。
  2. 解析类的二进制数据流为方法区内的数据结构。
  3. 创建java.lang.Class实例

Java虚拟机获取二进制数据流的方式有很多。最一般的就是通过读取.class文件,也可以从jar、zip归档文件中读取数据流。还可以从网络上获取二进制数据流甚至可以在程序运行时生成一段二进制数据流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GetStringMethodes {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz =Class.forName("java.lang.String");
Method[] methods = clazz.getDeclaredMethods();
for (Method m:methods) {
String mod = Modifier.toString(m.getModifiers());
System.out.print(mod+" "+m.getName()+"(");
Class<?>[] ps= m.getParameterTypes();
if(ps.length==0) System.out.println(")");
for(int i=0;i<ps.length;i++){
char end=i==ps.length-1? ')':',';
if (i==ps.length-1){
System.out.println(ps[i].getSimpleName()+end);
}else{
System.out.print(ps[i].getSimpleName()+end);
}
}
}
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public toString()
public hashCode()
public volatile compareTo(Object)
public compareTo(String)
public indexOf(String,int)
public trim()
public toCharArray()
public static transient format(Locale,String,Object[])
public static transient format(String,Object[])
public static copyValueOf(char[],int,int)
public static copyValueOf(char[])
public native intern()
...省略其他输出
Process finished with exit code 0

连接

==连接过程分为三步:
验证、准备、解释。==

验证

image

这里解释一下第四步:
Class文件会在其常量池中记录他即将引用的其他类或者方法。因此在验证阶段,虚拟机会检查这些即将被引用的类或者方法是否存在。并且验证是否有权限访问。如果找不到一个类则会抛出NoClassDefFoundError,如果是一个方法没有被找到那么就会抛出NoSuchMethodeError.

准备

当一个Class通过验证的时候就会进入准备阶段。在这个阶段虚拟机会为该类分配内存空进,并且初始化一些数值。

image

如果存在常量,那么常量也会被赋值。该赋值操作属于虚拟机的行为,实际上在准备阶段不会有任何Java代码被执行。

解析类

经过准备阶段后,就进入了解析阶段。解析阶段会把类、方法、接口、字段等间接引用转为直接引用。

初始化

如果之前的步骤都顺利完成那么就会进入最后一个阶段——初始化。
此时,类才会开始执行Java字节码。

初始化的重要步骤是执行 是由类的static字段或者static代码块生成的。我们可以简单理解为:该阶段就是要执行static代码段以及初始化静态字段。

虚拟机在初始化一个子类之前总是会尝试先初始化父类。所以父类的会在子类的之前执行,即父类static代码段优先级高于子类static代码段。

在某些情况之下,static会产生死锁问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StaticA {
static {
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
try{
Class.forName("StaticB");
}catch (ClassNotFoundException e){
e.printStackTrace();
}
System.out.println("StaticA has finish init");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StaticB {
static {
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
try{
Class.forName("StaticA");
}catch (ClassNotFoundException e){
e.printStackTrace();
}
System.out.println("StaticB has finish init");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StaticDeadLockMain extends Thread{
private char flag;
public StaticDeadLockMain(char flag){
this.flag = flag;
this.setName("Thread"+flag);
}
@Override
public void run() {
try{
Class.forName("Static"+flag);
}catch (ClassNotFoundException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
StaticDeadLockMain loadA= new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB= new StaticDeadLockMain('B');
loadB.start();
}
}

image

==static代码段产生死锁的问题确实存在。==