提到类加载,从字面上理解是把class类加载到内存中区,实际上JVM将类的加载是分为3个重要的阶段,加载,连接(验证,准备,解析),初始化。具体可用下图来表示:
加载:
将类的二进制数据加载到内存当中。将.class的二进制数据读入到内存中,将其放在运行时数据区的方法区(虚拟机运行环境中),然后在内存中创建一个java.lang.Class对象(规范并未说明Class位于哪,HotSpot虚拟机将其放置在方法去中,class对象是一枚镜子,反应了整个类的结构,也是反射的根源)用来封装类在方法区内的数据结构。
连接:
(1)验证:
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
(2)准备
准备阶段是正式为类静态变量分配内存并为其设置默认值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
(3)解析
将类中的符号引用特换为直接引用。这里不做详细阐述,有兴趣的可以去了解下。
初始化:
初始化是将类中的静态变量赋予正确的初始化值。静态变量声明语句,静态代码块都被看作是类的初始化语句。java虚拟机会按照初始化语句在类文件中的顺序一次执行他们。
关于实例变量:
类实例化后为实例变量分配内存,赋予默认值,最后初始化为默认值。java虚拟机为每一个类的实例创建一个初始化方法< init>,同时为每个构造方法也分配一个< init>。
一段摘自
首先我们需要知道java对类的使用方式分为2种:主动使用和被动使用。
每个类或接口只有被java程序主动使用时才会被初始化。
主动使用主要分为以下七种:(后续通过代码对其中的6种进行验证)
下面我们就通过代码来验证这几个主动使用是否导致类的初始化。
代码片如下
.public class Test9 {
public static void main(String[] args){
Parent9 parent = new Parent9();
}
}
class Parent9{
static {
System.out.println("Parent9初始化!");
}
}
这个比较简单,不做详细阐述,运行结果如下:
结论:创建类的实例会导致类的初始化
2. 访问类或接口(直接定义的)静态变量,或对静态变量赋值。(助记符分别为getstatic,putstatic)。
我们同样通过下面一段代码来验证,可以先思考下自己认为的结果,然后在运行结果验证,可以加深自己的印象。
代码片
.
public class Test2 {
public static void main(String[] args)
{
System.out.println(Child2.str);
}
}
class Parent2 {
public static String str = "hello world";
static {
System.out.println("Parent2 初始化");
}
}
class Child2 extends Parent2{
static {
System.out.println("Child2 初始化");
}
}
运行结果:
可能和某些友友想的不太一样。
注意这段主题内容我标红的地方,对于一个静态字段而言,只有直接定义了该类的字段的类才会被初始化。虽然Child2继承了Parent2,也有了此变量,但是并非Child2直接定义,所以并不会导致Child2的初始化。
对于接口的验证由于无法使用静态块。我们可以使用非静态块来大致验证下:
代码片
.
public class Test6 {
public static void main(String[] args){
System.out.println(Parent6.thread);
}
}
interface Parent6{
Thread thread = new Thread(){
{
System.out.println("interface Parent6 block!");
}
};
}
结果如下:
可以看出访问一个接口的静态变量会导致接口的初始化。这边我们使用的是运行时常量。具体关于常量的分析,下文也会讲到。
访问静态方法这里就不阐述了 ,和静态变量的访问大致类似。有兴趣的可以,自己验证下。
在上面对于静态变量或者静态方法访问的例子中,也可以通过查看助记符的方式来看类是否初始化。我们反编译一下刚刚的Test2文件看下相应的助记符,如下图所示:
着重看运行的这一行,访问静态变量时助记符为getstatic,这也是验证的一种方式。看到这些助记符说明类被初始化了。还有putstatic静态变量赋值,invokestatic访问静态方法等。
代码片如下
. public class Test7 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classzz = ClassLoader.getSystemClassLoader();
classzz.loadClass("com.newgain.classloader.Parent7");
System.out.println("==================");
Class classzzz = Class.forName("com.newgain.classloader.Parent7");
}
}
class Parent7{
static {
System.out.println("Parent7初始化");
}
}
可以看出只有反射Class.forName导致类的初始化。
代码片如下
.public class Test2 {
public static void main(String[] args)
{
System.out.println(Child2.str1);
}
}
class Parent2 {
public static String str = "hello world";
static {
System.out.println("Parent2 初始化");
}
}
class Child2 extends Parent2{
public static String str1 = "i love my country!";
static {
System.out.println("Child2 初始化");
}
}
结果如下:
我们访问Child2,它的父类Parent2被初始化了!这是对于类而言。对于接口而言并非如此,但是在举接口的例子时,这边要先把常量与类的初始化的关系先阐述一下。
说到常量,根据编译器的不同行为其实也会分为编译时常量和运行时常量。先看下面这段代码:
代码片如下
.
public class Test3 {
public static void main(String[] args){
System.out.println(Parent3.str);
}
}
class Parent3 {
public static final String str = "happy birthday!";
static {
System.out.println("Parent3 的初始化");
}
}
可以先思考下结果。运行结果如下:
Parent3并没有被初始化!amazing!我们增加-XX:+TraceClassLoading虚拟机参数来看下运行这段程序都加载了哪些类,我们看主要的部分,如下图:
查看了下虚拟机连Parent3这个类都没有加载,那更不要谈初始化了,类Parenta3肯定没初始化。所以得到如下结论:
编译器具有常量优化机制。常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中, 本质上调用类并没有直接引用到定义常量的类,以此并不会触发Parent3的初始化。(是将常量存入到Test3的常量池中,之后Test3和Parent3没有任何关系了,甚至可以将Prent1的class文件删除。我们通过类的加载情况也证明了这一点。)
接下来在把上面的代码做下修改,把 public static final String str = “happy birthday!”;改成 public static final String str =UUID.randomUUID().toString();我们得到的结果如下:
Parent3被初始化了。这就是编译常量和运行常量在初始化的区别。 当一个常量的值并非编译期决定的,那么其值就不会在调用期间放入调用类的常量池中,这时程序运行会主动使用该常量所在的类,当然会导致该类的初始化。
下面讲下本人遇到的一个比较细的问题:还是上面这段程序,我们把 public static final String str = “happy birthday!”; 这句话改下。改为 public static final Integer i = 127;可能有能有些朋友认为结果就是输出127,并不会导致Parent3类的初始化。并非如此,运行结果请看下图:
事实上Parent3被初始化了,其实包装类的写法在编译时要被编译器改成public static finalInteger i = Integer.valueOf(127);只是简化了写法。实际上属于运行时常量。会导致类的初始化。其他包装类常量也是如此。反编译Test3也可以在这一行看到getstatic助记符,证明类被初始化了。
代码片如下
.public class Test5 {
public static void main(String[] args){
System.out.println(Childa.str);
}
}
interface Parent5 {
Thread thread = new Thread(){
{
System.out.println("Prent5初始化!");
}
};
}
interface Child5 extends Parent5 {
String a = UUID.randomUUID().toString();
Thread thread = new Thread(){
{
System.out.println("Child5初始化!");
}
};
}
结果如下:
Prent5没有被初始化,与类不一样。所以访问一个接口的子接口并不会导致其父接口的初始化。只有访问父接口直接定义的(运行时)常量时,父接口才会被初始化。
前六个的主动使用会使类初始化的验证,就总结到这了。仅以此记录自己的学习过程。有位大神说过,有输入,就要有输出这样才能更好的吸收知识。fighting!