什么是类加载
虚拟机把类的描述数据,从Class文件加载到内存,并对数据进行校验,转换,初始化,形成可被虚拟机直接使用的Java类型,这就是类的加载。
简短理解一句话:把数据从Class里加载到内存。Class是什么?Class就是配置文件。
使用类加载的机制,可以使Java更具有灵活性,比如jsp、OSGi技术都应用此。
类加载时机
类也是有生命周期的。顺序说明包括七个部分:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。
被动引用的五种情况:
1、这个过程需要被理解,但是绝对不是一个一个去背。理解和背是不同的。当我们使用一个对象使,好像只关注其初始化(new)和使用。所以第一层理解,当jvm遇到字节码为new、getstatic、putstatic和invokestatic时(new对象、或者加载静态字段1),会出发jvm对类的加载、验证、准备操作,这部分不用程序员关注。
2、反射对类调用时,触发类初始化
3、初始化一个类时,先触发其父类的初始化
4、main()里面包括的方法所在类要初始化
5、和动态语言有关
类加载过程
加载
加载阶段完成:
1、通过类的全限定名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法去这个类数据的访问入口。
由于虚拟机规范没有定义二进制字节流的获取方式,所以诞生了如下技术:
1、从压缩包中获取字节流:JAR,WAR,EAR
2、运行时获取:动态代理。有jdk动态代理,cglib这种字节码插入技术。
3、有其他文件产生:JSP
4、网络获取:Applet
比较特殊的是数组。数组不通过类加载器创建,由JVM直接创建。但数组包括类型的,也是要走类加载器的。数组可见性和其包括的类型一致,默认为public。
方法区中的数据结构由JVM定义,但JVM规范未规定具体的数据结构。
验证
Java语言是类型安全的,仅仅通过Java代码很难做到访问数组外界的数据,或任意类型转换。因为编译器就会拒绝编译。但在字节码层面就越过了编译器,理论上可以做任何内存操作。所以为了保护虚拟机安全,必须对字节码进行验证。Java SE7版本里对验证用了很长篇幅的描述。
包括文件格式、元数据、字节码验证、符号引用验证。
字节码验证最为复杂,为了降低消耗时间,JVM设计了一个StackMapTable属性,用来存放方法体本地变量表和操作栈应有状态。理论上StackMapTable也有可能被篡改。所以编译器有一个优化参数:-XX:UseSplitVerifier来选择开启或关闭。
准备
准备阶段是正式为类变量分配内存的阶段。类变量就是static修饰的。初始值为数据类型的零值。比方说public static int v=123,则在经过初始化阶段后,v的值为0,而不是123,将123赋值的操作是在初始化阶段。如果被final修饰,则直接赋值123。
解析
将常量池内的符号引用替换为直接引用的过程。直接引用是直接指向目标的指针、相对偏移量或句柄。包括:
1、类或接口解析
2、字段解析
会按照继承关系从下往上递归搜索,如果没找到,报java.lang.NoSuchFieldError,如果找到了,但对字段没有访问权,报java.lang.IllegalAccssError
3、类方法解析
4、接口方法解析
接口中所有方法默认是public的
初始化
< clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,排列顺序和源文件相同。静态语句块只能访问定义在它之前的变量,后面的只能赋值,不能访问。先执行父类的< clinit>,所以父类的静态方法会比子类先执行。
类加载器
比较两个类是否相等,只有在这两个类是被同一类加载器加载的条件下才有意义。类加载器包括:
启动类加载器、扩展类加载器、应用程序加载器、自定义加载器。
其之间的层次关系称为双亲委派模型。这部分在我之前的博客里讲过。Java类加载机制,这里类加载机制不是通过继承关系(Inheritance)实现,而是复用父类加载器。