类加载过程
1.加载(loading)
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
关于方法区:永久代和元空间:
在jdk7之前,java把类的元数据放在永久代中,其中永久代使用的是JVM的内存
在jdk8之后,用元空间(代替的永久代,元空间使用本地内存。不会再出现Java.lang.outofMemoryError:PermGen Space的错误
进行转换的原因:
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
拓展:
移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。
JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中
2.链接(linking)
-
验证(verify)阶段:
- 看class文件是否符合当前虚拟机要求,保证被加载类正确且合法
- 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证
-
准备(Prepare)阶段:
- 为类变量(即为被static修饰的成员)分配内存并且设置该类变量的默认初始值,即零值。如int static a = 10,此时a = 0;
- 被final修饰的static,在编译时已经分配了,在准备阶段会显示初始化。如int final static a = 10, 在准备阶段a = 10;
- 不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
-
解析阶段(Resolve)阶段:
-
发生在初始化之后
-
将常量池内的符号引用转换为直接引用的过程
-
符号引用就是一组符号来描述所引用的对象(一个类的的加载,伴随着许多类的加载过程,在字节码文件中,不能全部显示的表现出来,这样会使得字节码文件过于冗杂,只需要通过一组符号来表示)。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
-
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info等。
例子:一个简单类的常量池
public class Point { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; int s = x + y; } public int getX() {return x;} public void setX(int x) {this.x = x;} public int getY() {return y;} public void setY(int y) {this.y = y;} }
-
3.初始化(Initialization)
- 初始化阶段就是执行类构造器方法()的过程 (该方法不需要定义,javac编译器胡自动收集类中的所有类变量的赋值动作和静态代码块中的语句进行合并而来)
- 类构造器方法中指令按语句在源文件中出现的顺序执行。
- ()和类的构造器不同(关联:构造器是虚拟机栈视角下的())
- 若该类有父类,先加载父类
- 虚拟机必须保证一个类的()方法在多线程下呗同步加锁
关于上述五点的解释:
初始化阶段就是执行类构造器方法()的过程
public class TestClinit {
public TestClinit() {
System.out.println("这是构造方法!");
}
public void demo() {
System.out.println("这是实例方法!");
}
static {
System.out.println("这是静态代码块!");
}
public static void main(String[] args) {
new TestClinit().demo();
}
}
输出结果:
这是静态代码块!
这是构造方法!
这是实例方法!
再看反编译后的方法:
还是上述的代码,我们去掉静态代码块。
可以看出,在类初始化阶段,先对类变量进行初始化,即执行类构造器方法()。
类构造器方法中指令按语句在源文件中出现的顺序执行。
public class TestClinit {
private static int a = 10;
static {
a = 20;
b = 10;
}
private static long b = 20;
public static void main(String[] args) {
System.out.println(TestClinit.a);
System.out.println(TestClinit.b);
}
}
输出结果
20
20
我们可以看到最后给a赋值数为20,则a输出20,最后给b赋值数为20,则b输出20.
我们可以看一下反编译后的clinit方法的代码
若该类有父类,先加载父类
public class TestExtend {
static class Father {
public static String name;
static {
name = "Father";
}
}
static class Son extends Father {
public static String son = name;
}
public static void main(String[] args) {
System.out.println(Son.son);
}
}
输出结果
Father
查看反编译结果
虚拟机必须保证一个类的()方法在多线程下呗同步加锁
public class TestThread {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread deadThread = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r, "22");
Thread t2 = new Thread(r, "33");
t1.start();
t2.start();
}
}
class DeadThread {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while (true) {
}
}
}
}
输出结果
22开始
33开始
22初始化当前类
我们发现当DeadThread的clinit没有执行完毕时,33这个线程时无法进来的。这里就是JVM帮我们进行了加锁。
一个有趣的现象:当我们去掉while (true) {}后,DeadThread的静态代码块也只输出了一次。这里说明静态代码块只执行一次。
4.拓展:Java中构造方法的执行顺序
- 先执行这个类的方法
- 调用父类的构造方法(父类还有父类的话,从最开始的基类开始调用)
- 按声明顺序将成员引用变量初始化
- 调用自身的构造方法