Java虚拟机类加载机制

Java类加载的过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

对于第一阶段的加载,Java虚拟机规范中没有强制约束。但是初始化只有五种情况:

  1. 用new关键字是花花对象,调用和设置类的静态字段,调用一个类的静态方法。
  2. 使用反射对类进行调用,如果类没有初始化会先将其初始化。
  3. 但初始化一个类时,发现其父类没有初始化,会先初始化其父类
  4. 但虚拟机启动时,用户需要指定一个要执行的主类。
  5. 当使用jdk1.7的胴体语言支持时。

这五种场景中的行为称为主动引用,所有被动引用都不会触发初始化。

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义类引用类,不会触发此类的初始化
  3. 常量不会触发类的初始化(final修饰的变量在编译阶段就被存入调用类的常量池中)。

接口的加载和过程和类的加载过程有一些不同,接口只有在真正使用父接口的时候才会初始化父接口。

加载阶段

  1. 通过类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.class的对象,作为方法区这个类的各种数据的访问入口。

数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。数组类的元素类型最终是由类加载器创建的。

  1. 如果数组的组件类型是引用类型,就使用上述的加载过程去创建这个类型
  2. 如果不是引用类型虚拟机就会把数组标记为与引导类加载器管理
  3. 如果组件类型不是引用类型,则数组的默认可见性为public。

加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。Class对象虽然是一个对象,但是存放在方法区里。

验证阶段

验证是连接阶段的第一步,主要保证Class文件的字节流中包含的信息符合虚拟机的要求。验证阶段有四个阶段的校验动作:

  1. 文件格式校验:
    • 是否以魔数开头
    • 主次版本号是否在虚拟机处理范围内
    • 常量池中是否有不被支持的常量类型
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
    • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
  2. 元数据校验:
    • 验证当前类是否有父类
    • 是否继承了final类
    • 如果不是抽象类是否实现了所有方法
    • 类中的字段、方法是否与父类产生矛盾
  3. 字节码校验:
    • 保证任意时刻操作数栈的数据类型与指令代码都能配合工作
    • 保证跳转指令不会跳转到防反弹以外的字节码指令上
    • 保证方法体重的类型转换是有效的
  4. 符合引用校验:
    • 符合引用中通过字符串描述的全限定名是否能找到对应的类
    • 在指定类中是否存在符合方法的字段描述符以及简单名称锁描述的方法和字段
    • 符合引用中的类、字段、方法的访问性是否可被当前类访问

准备阶段

准备阶段是正式为类变量分配内存并设置初始化值的阶段。这些变量所使用的内存都在方法区中分配。这时候分配的内存仅包括类变量(static修饰),实例变量将在对象实例化的时候一起分配到堆中。通常情况下初始值为数据类型的零值,赋值操作在初始化阶段才会执行。

public static int value = 1; //初始值为0
public static boolean value = true; //初始值为false
public static char value = 'a'; //初始值为'\u0000'

如果字段属性存在常量属性则准备阶段直接赋值。

public static final int value = 1; //直接赋值为1

解析阶段

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定加载到内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不相同。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符合引用。

初始化阶段

类初始化阶段是执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块(statis{})中的语句合并产生的。
  • <clinit>()方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<client>()方法执行之前,父类的<client>()方法已经执行完毕。
  • 由于父类的<clinit>()方法先执行,父类的静态语句块要优先于子类的变量赋值操作。
  • <clinit>()方法对于类和接口来说并不是必须的,如果类没有静态语句块,也没有变量的赋值操作,那么编译器就不会生成<clinit>()方法。
  • 如果接口有变量初始化赋值操作,接口与类一样都会生成<clinit>()方法。接口不需要先执行父类的<clinit>()方法,接口的实现类也不需要先执行接口的<clinit>()方法。
  • 在多线程环境中,虚拟机也会保证一个类的<clinit>()方法被正确的加锁、同步。

类加载过器

类加载器是Java运行环境的一部分,负责动态加载Java类到Java虚拟机的内存空间。类通常是按需加载的,即第一次使用该类时才加载。JVM有三个默认的类加载器

  • 引导(BootStrap)类加载器,负责将放在<JAVA_HOME>\lib目录中或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到内存中。
  • 扩展(Extensions)类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 系统(Application)类加载器,负责加载用户类路径上所指定类库。可以直接使用(ClassLoader.getSystemClassLoader()获取)

类加载器直接的关系是双亲委派模型。要求除了顶层加载器外,其余的类加载器都应当有直接的父类加载器。类的父子关系是以组合关系类实现的。

获取类加载器:

    //系统类加载器
    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(appClassLoader);
    //扩展类加载器
    ClassLoader extClassLoader = appClassLoader.getParent();
    System.out.println(extClassLoader);
    //启动类加载器
    ClassLoader bsClassLoader = extClassLoader.getParent();
    System.out.println(bsClassLoader);

输出:

sun.misc.Launcher$AppClassLoader@2626b418
sun.misc.Launcher$ExtClassLoader@4617c264
null

系统类加载器可以直接获取,扩展类加载器的也可以使用,不过启动类加载器是由C++实现的,逻辑上是不存在的,所以为null。

其他

Java魔数:SUN公司规定每个Class文件都必须以一个word(4个字节)开始,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。用十六进制打开Class文件就可以看到Class文件的魔数是0XCAFEBABE。

主次版本号:魔数的后续的内容就是一个word的长度来表示生产的class文件的版本号,版本号分为主版本号和次版本号,高版本JDK可以向下兼容以前的Class文件,但不能运行以后版本的Class文件。

编译器版本 十六进制版本号 十进制版本号
JDK1.9 0X35 53
JDK1.8 0X34 52
JDK1.7 0X33 51
JDK1.6 0X32 50
JDK1.5 0X31 49
JDK1.4 0X30 48
JDK1.3 0X2F 47
JDK1.2 0X2E 46
JDK1.1 0X2D 45

常量池的tag项说明

常量类型
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_INterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

参考链接

  1. 深入理解Java虚拟机(6)-Class类文件结构
  2. Java类加载器
  3. Java自定义类加载器与双亲委派模型
  4. Mac 十六进制编译器
  5. 《深入理解Java虚拟机》
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容