java中Class类装载流程是怎样的,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。
java虚拟机加载class文件过程如下:
class文件只有在必须要使用的时候才会被装载,java虚拟机不会无条件地装载class类型。java虚拟机规定,一个类或接口在初次使用前,必须进行初始化。这里的“使用”是指主动使用,主动使用只有下列几种情况:
当创建一个类的实例时,比如使用new
关键字或反射、克隆、反序列化。
当调用类的静态方法时,使用了字节码invokestatic
指令。
当使用类或接口的静态字段(final
常量除外),比如使用getstatic
或者putstatic
指令。
当使用java.lang.reflect
包中的方法反射类的方法时。
当初始化子类时,要求先初始化父类。
作为启动虚拟机,含有main()
方法的那个类。
public class Parent { static { System.out.println("Parent init"); } public static int v = 100; } public class Child extends Parent { static { System.out.println("Child init"); } } public class Demo01 { public static void main(String[] args) { Child c = new Child(); } }
以上代码声明了3个类:Parent
、Child
和Demo01
,Child
为Parent
的子类。若Parent
被初始化,由代码中的static
语句块可知,将会打印“Parent Init
”,若Child
被初始化,则会打印“Child Init
”,执行Demo01
,运行结果:
Parent init Child init
由此可知,系统首先装载Parent
类,接着装载Child
类。
public class Parent { static { System.out.println("Parent init"); } public static int v = 100; } public class Child extends Parent { static { System.out.println("Child init"); } } public class Demo02 { public static void main(String[] args) { System.out.println(Child.v); } }
查看以上代码,Parent
中有静态变量v,并且在Demo02
中,使用其子类Child
调用父类中的变量。运行以上代码,输出结果如下:
Parent init 100
可以看到,虽然在Demo02
中直接访问了子类对象,但是Child
子类并未被初始化,只有Parent
父类被初始化。可见,在使用一个字段时,只有直接定义该字段的类才会被初始化。
注意:虽然Child类没有被初始化,但是此时Child类已经被系统加载,只是没有进入初始化阶段。
使用-XX:+TraceClassLoading
参数运行这段代码,就会得到以下日志(删除部分输出):
oaded java.net.Inet6Address from /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar] [Loaded java.net.Inet6Address$Inet6AddressHolder from /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar] [Loaded jvm.chapter10.Parent from file:/Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/] [Loaded jvm.chapter10.Child from file:/Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/] Parent init 100
加载类处于类装载的第一个阶段。在加载类时,java虚拟机必须完成以下工作:
通过类的全名获取类的二进制数据源
解析类的二进制数据流为方法区内的数据结构
创建java.lang.Class类的实例,表示该类型
对于类的二进制数据流,虚拟机可能通过多种途径产生或获得,如:class文件,或者jar、zip等归档数据包,也可以通过网络加载。
在获取类的二进制数据后,java虚拟机就会处理这些数据,并最终转为一个java.lang.Class
类提供的实例。
当类加载到系统后,就开始连续操作,验证是连续操作的第一步。它的目的是保存加载的字节码是合法、合理并且符合规范的。验证的步骤比较复杂,实际要验证的项目也繁多,大体上java虚拟机需要做的检查如下:
当一个类验证难过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。java虚拟机各类型变量默认值的初始值如下:
类型 | 默认初始值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | \u0000 |
boolean | false |
reference | null |
float | 0f |
double | 0d |
注意:java并不支持boolean类型,对于boolean类型,内部实现实际上是int类型,由于int类型的默认值是0,故对应的boolean类型的默认值就是false.
如果类存在常量字段(final修饰的字段),那么常量字段也会在准备阶段被赋值上正确的值,这个赋值属于java虚拟机的行为,属于变量的初始化。
准备阶段后是解析阶段。解析阶段的工作就是将类、接口、字段和方法的符号引用转为直接引用。
符号引用就是一些字面量的引用,和虚拟机的内部数据结构与内存布局无关。在Class类文件中,在大量的符号引用,但在运行时,只有符号引用是不够的,系统需要明确变量、方法等的位置。以方法为例,java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法时,只要知道这个方法在方法表中的偏移量就可以直接调用。通过解析操作,符号引用可以转变为目录方法在类的方法表中的位置,从而使得方法被成功调用。
综上所述,所谓解析,就是将符号引用转为直接引用,也就是得到类或者字段、方法在内存中的指针或者偏移量。
类的初始化是类装载的最后一个阶段,如果前面的步骤都没有问题,表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。初始化阶段的重要工作是执行类的初始化方法 <clinit>
。方法<clinit>
是由编译器自动生成的,它是由静态成员的赋值语句及static
语句块共同产生的。
值得一提的是,对于<clinit>
方法,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。也就是说,当多个线程试图初始化同一个类时,只有一个线程可以进入<clinit>
方法了(当需要使用这个类时,虚拟机就会直接返回它已经准备好的信息)。
正因为<clinit>
方法是带锁线程安全的,所以在多线程环境下进行类初始化时,可能会引起死锁,并且这种死锁是很难发现,因为看起来他们并没有可用的锁信息。
下面的代码展示了在类的初始化时,产生了死锁线程:
package jvm.chapter10; class StaticA { static { try { Thread.sleep(1000); } catch (Exception e) { } try { Class.forName("jvm.chapter10.StaticB"); } catch (ClassNotFoundException e) { } System.out.println("StaticA init OK!"); } } class StaticB { static { try { Thread.sleep(1000); } catch (Exception e) { } try { Class.forName("jvm.chapter10.StaticA"); } catch (Exception e) { } System.out.println("StaticB init OK"); } } public class Demo03 extends Thread { private char flag; public Demo03(char flag) { this.flag = flag; } @Override public void run() { try { Class.forName("jvm.chapter10.Static" + flag); } catch (Exception e) { } System.out.println(getName() + "over"); } public static void main(String[] args) throws Exception { Demo03 loadA = new Demo03('A'); Demo03 loadB = new Demo03('B'); loadA.start(); loadB.start(); loadA.join(); loadB.join(); } }
以上代码由3个类组成:StaticA
、StaticB
和 Demo03
.在Demo03
中创建了两个线程,线程A
试图初始化StaticA
,线程B
尝试初始化StaticB
,在StaticA
的初始化过程中,会尝试初始化StaticB
,同样在StaticB
的初始化过程中,也初始化 StaticA
,这就导致了死锁。
通过线程堆栈dump可以得到如下信息:
"Thread-1" #12 prio=5 os_prio=31 tid=0x00007f9dd4853000 nid=0x5703 in Object.wait() [0x0000700004797000] java.lang.Thread.State: RUNNABLE at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) at jvm.chapter10.StaticB.<clinit>(Demo03.java:33) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) at jvm.chapter10.Demo03.run(Demo03.java:52) Locked ownable synchronizers: - None "Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9dd4801800 nid=0xa803 in Object.wait() [0x0000700004694000] java.lang.Thread.State: RUNNABLE at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) at jvm.chapter10.StaticA.<clinit>(Demo03.java:17) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) at jvm.chapter10.Demo03.run(Demo03.java:52) Locked ownable synchronizers: - None
在这种情况下,系统并没有给出足够的信息来判定死锁,但是死锁确实存在,我们需要格外小心由类的初始化引起的死锁问题。
看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注亿速云行业资讯频道,感谢您对亿速云的支持。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。