前言 最近在看一本书,发现代码里用到了Thread.currentThread().getContextClassLoader(),为什么类加载器还与线程有关系呢,为什么不直接使用ClassLoader.getSystemClassLoader()呢?带着这些疑问又把JVM类加载机制从头到尾学习了一遍。

篇一 类加载时机

我们编写的代码存储在java文件中,java源代码通过编译生成Java虚拟机可识别的字节码,存储在Class文件中。运行java程序时需要将Class文件中的信息加载到Java虚拟机中,而这个过程就是类加载的过程。

如上图所示,假设写一个类A存储为A.java,通过javac A.java编译生成A.class,A.class中存储了各种描述A类的信息。然后运行程序,执行java A.class,这时java虚拟机会先将A.class中信息转换成Java虚拟机所需的存储格式,然后存放在方法区中。之后,Java虚拟机会再创建一个java.lang.Class对象实例,这个实例将作为访问方法区中A类信息的入口。使用A中方法时,要先创建一个实例new A(),Java虚拟机基于类的描述信息在Java堆中创建一个A的实例。

那何时会触发类的加载呢?Java虚拟机规范中并未明确指出,但对类的初始化时机做了明确说明。这两者有什么关系呢,我们先了解一下类的加载流程:

根据这个流程,初始化触发时类加载的第一个阶段---加载阶段肯定已经完成了,那我们可以这样推论,类初始化的触发时机定会触发整个类加载过程。

上面示例中提到的新建一个对象实例会先加载对象,java虚拟机规范中提到在以下五种情况将触发类的初始化[1]

1) 遇到new,getstatic,putstatic,invokestatic这四条字节码指令时;

为了验证我们先设置个基础类B,提供了一个静态字段,一个静态块。静态方法和静态块会在编译期汇集到<clinit>类初始化方法中,固可以用这个方法的运行结果引证类的初始化。

  1. public class B { 

  2. public static int f; 

  3. static { 

  4. System.out.println("init B"); 



  5. public static void m(){ 

  6. System.out.println("invoke m"); 





  • new:新建对象;

  1. public class C{ 

  2. public static void main(String args[]) { 

  3. new B(); 





运行结果:

  1. init B 

说明类已经初始化了

  • pustatic:设置类的静态字段;

  1. public class C{ 

  2. public static void main(String args[]) { 

  3. B.f=5; 





运行结果:

  1. init B 

  • getstatic:获取类的静态字段;

  1. public class C{ 

  2. public static void main(String args[]) { 

  3. System.out.println(B.f); 





运行结果

  1. init B 



  • invokestatic:调用类的静态方法;

  1. public class C{ 

  2. public static void main(String args[]) { 

  3. B.m(); 





运行结果:

  1. init B 

  2. invoke m 

2) 使用java.lang.reflect包或Class的方法对类进行反射调用时;

  1. public class C{ 

  2. public static void main(String args[]) { 

  3. try { 

  4. Class.forName("B"); 

  5. } catch (ClassNotFoundException e) { 







运行结果:

  1. init B 

3) 当初始化一个类,其父类还没有初始化时;

这里我们新建一个类A继承B,通过初始化A看看B有没有初始化。

  1. public class A extends B { 

  2. static { 

  3. System.out.println("init A"); 






  4. public class C{ 

  5. public static void main(String args[]) { 

  6. new A(); 





运行结果:

  1. init B 

  2. init A 

发现B也初始化了,并且是B先初始化完A才初始化,也就是初始化一个类时会先看其父类是否已经初始化,依次类推一直到java.lang.Object。其实除了类需要初始化,接口也需要初始化,用于初始化接口变量的赋值。与类的初始化不同,接口初始化时并不会递归初始化所有父接口,而是用到哪个接口就初始化哪个接口,如调用接口中的常量。由于接口中不允许有静态块,那<clinit>就只用于初始化其常量。
新建两个接口E、G,类F实现这两个接口:

  1. public interface E { 

  2. B b = new B();//依据接口的特性 默认变量static final修饰 



  3. public interface G { 

  4. D d = new D(); 



  5. public class D { 

  6. static { 

  7. System.out.println("init D"); 





  8. public class B { 

  9. static { 

  10. System.out.println("init B"); 





  11. public class F implements E,G { 

  12. //只为了测试这个方法无需任何实现 



  13. public class C { 

  14. public static void main(String args[]) { 

  15. System.out.println(F.b); 





这里运行结果只有init B而没有init D说明没有触发接口G的初始化。

4) 当Java虚拟机启动时,用户指定的主类(包含main方法的类)要先进行初始化;

程序运行一定会有一个入口,也就是Main类,java虚拟机启动时会现将其初始化。下面我们在B类里加一个空的main方法,运行看一下效果:

  1. public class B { 

  2. public static int f; 

  3. static { 

  4. System.out.println("init B"); 



  5. public static void m(){ 

  6. System.out.println("invoke m"); 



  7. public static void main(String args[]){ 





编译后运行java B控制台输出init B,说明B也初始化完成了。

5) 在初次调用java.lang.invoke.MethodHandle实例时,通过java虚拟机解析出类型是REF_getStatic,REF_puStatic,REF_invokeStatic的方法句柄时;

注意,上面所说的场景都有一个前提就是对应的类没有初始化过,如果这个类已经初始化了,直接使用就可以了。

以上这些场景都属于对一个类的主动引用,除了这些场景外其他引用类的方式都不会触发初始化。我们从上面的场景用找几个特例来看一下是否能使其初始化:

1)针对第一条,通过子类调用父类的静态变量或静态方法

这里严重上面第三条的类A,类B

  1. public class C{ 

  2. public static void main(String args[]) { 

  3. A.f=5; 





通过类A赋值类B的静态字段f,运行结果只有init B,而没有初始化类A,因为这里用到的是类变量,只是借用了A对B的继承关系,无需对A进行初始化。

2)调用类中的常量

常量在编译阶段会直接在调用类中将常量值存入其常量池中,与被调用类其实也没有关系了,固对常量的调用并不会引起被调用类的初始化,如下:

  1. public class D { 

  2. public static final String f = "f"; 

  3. static { 

  4. System.out.println("init D"); 






  5. public class C{ 

  6. public static void main(String args[]){ 

  7. System.out.println(D.f); 





运行结果:



证实了上面的说法

3) 通过数组引用类

对于数组java虚拟机会特殊处理,在执行时Java虚拟机会动态生成一个数组对象,这时初始化的只是这个数组对象。当使用数组的元素时才会真正触发元素类型的初始化。
直接在main方法中新建数组:

  1. public class H { 

  2. public static void main(String args[]){ 

  3. B[] bs = new B[1]; 





运行结果什么也没有输出,说明B没有初始化。通过输出bs发现这是一个[LB对象,是由newarray指令动态生成的。

  1. public class H { 

  2. public static void main(String args[]){ 

  3. B[] bs = new B[1]; 

  4. System.out.println("-------"); 

  5. System.out.println(bs[0].f); 





运行结果:

  1. ------- 

  2. init B 



这说明直到bs[0].f时才真正触发B的初始化。

写了这么多才发现仅仅谈到类加载的时机,离着解决篇头的问题还差一大截。没办法,要想彻底了解清楚类加载必须慢下心一步一步来。

若发现文章中任何问题,欢迎指正,互相学习。


  1. 这里说的初始化指的是类初始化,还有实例初始化是在创建实例时进行的。

漫谈JVM之类加载机制(篇一)的更多相关文章

  1. 大白话谈JVM的类加载机制

    前言 我们很多小伙伴平时都是做JAVA开发的,那么作为一名合格的工程师,你是否有仔细的思考过JVM的运行原理呢. 如果懂得了JVM的运行原理和内存模型,像是一些JVM调优.垃圾回收机制等等的问题我们才 ...

  2. JVM内存结构 JVM的类加载机制

    JVM内存结构: 1.java虚拟机栈:存放的是对象的引用(指针)和局部变量 2.程序计数器:每个线程都有一个程序计数器,跟踪代码运行到哪个位置了 3.堆:对象.数组 4.方法区:字节流(字节码文件) ...

  3. JVM之类加载机制

    JVM之类加载机制 JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 类加载五部分 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这 ...

  4. JVM的类加载机制全面解析

    什么是类加载机制 JVM把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这就是JVM的类加载机制. 如果你对Class文件的结 ...

  5. 一文教你读懂JVM的类加载机制

    Java运行程序又被称为WORA(Write Once Run Anywhere,在任何地方运行只需写入一次),意味着我们程序员小哥哥可以在任何一个系统上开发Java程序,但是却可以在所有系统上畅通运 ...

  6. (转) JVM——Java类加载机制总结

    背景:对java类的加载机制,一直都是模糊的理解,这篇文章看下来清晰易懂. 转载:http://blog.csdn.net/seu_calvin/article/details/52301541 1. ...

  7. 图解JVM的类加载机制(详细版)

    注:本文为作者整理和原创,如有转载,请注明出处. 上一篇博文,把JAVA中的Class文件格式用图形的方式画了一下,逻辑感觉清晰多了,同时,也为以后查阅的方便. Class文件只是一种静态格式的二进制 ...

  8. JVM的类加载机制

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 类加载的过程: 包括加载.链接(含验证.准备 ...

  9. 【JVM】类加载机制

    原文:[深入Java虚拟机]之四:类加载机制 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 类加 ...

随机推荐

  1. Pandas面板(Panel)

    面板(Panel)是3D容器的数据.面板数据一词来源于计量经济学,部分源于名称:Pandas - pan(el)-da(ta)-s. 3轴(axis)这个名称旨在给出描述涉及面板数据的操作的一些语义. ...

  2. .Net Core使用jexus配置https

    今天搞了一下怎么从http换成https,写一篇博客记录该过程.关于jexus的安装和使用请看我之前的一篇博客<Jexus部署Asp.Net Core项目>,唯一的不同是,将jexus升级 ...

  3. GO学习笔记:面向对象--method

    现在假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算他的面积,那么按照我们一般的思路应该会用下面的方式来实现: package main import "fmt&quo ...

  4. web自动化中的page object模式

    一. 原理 将页面的元素定位和元素行为封装成一个page类,实现页面对象和测试用例分离 类的属性:元素定位 类的行为:元素的操作 测试用例:调用所需页面对象中的行为,组成测试用例 二. 好处 1. 当 ...

  5. C#外部类、内部类(嵌套类)之间的成员访问特点

    最近程序中需要用到多线程工作下的单例模式.而其多种实现方法中,利用内部类实现懒汉模式是一种值得推荐的方式.顺便也就对内部类和外部类之间的关系做了一下研究,总结如下(理解不困难,不粘贴代码了,有需要的留 ...

  6. EL标签

    1.EL的作用 jsp的核心语法: jsp表达式 <%=%>和 jsp脚本<%  %>. 开发jsp的原则: 尽量在jsp页面中少写甚至不写java代码. 使用EL表达式替换掉 ...

  7. laravel5表单验证

    学习laravel框架有一段时间了,觉得它自带的表单验证特别好用,和大家分享分享 对于一些验证规则手册上都有,相信大家看了就会,我简单的说下怎么使用自定义正则验证: 验证手机号:'tel' => ...

  8. ARM 内核SP,LR,PC寄存器

    深入理解ARM的这三个寄存器,对编程以及操作系统的移植都有很大的裨益. 1.堆栈指针r13(SP):每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式.非异常模 ...

  9. [置顶] 【Android实战】----从Retrofit源码分析到Java网络编程以及HTTP权威指南想到的

    一.简介 接上一篇[Android实战]----基于Retrofit实现多图片/文件.图文上传中曾说非常想搞明白为什么Retrofit那么屌.最近也看了一些其源码分析的文章以及亲自查看了源码,发现其对 ...

  10. Precision/Recall、ROC/AUC、AP/MAP等概念区分

    1. Precision和Recall Precision,准确率/查准率.Recall,召回率/查全率.这两个指标分别以两个角度衡量分类系统的准确率. 例如,有一个池塘,里面共有1000条鱼,含10 ...