JVM-004-类加载机制

虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的Java类型。

在Java里面,类型的加载、连接和初始化过程都是在程序运行期间完成,不同于在编译时需要进行连接工作的语言。

java支持动态扩展就是依赖运行时动态加载和动态连接实现的

1. 什么时候进行类加载

类从被加载到虚拟机内存中到卸载出内存为止,整个生命周期包括:

image

除了解析之外其他步骤的顺序是确定的,解析在某些情况下可以在初始化阶段之后在开始,目的是为了支持JAVA语言的运行时绑定(动态绑定)

1.1 加载
并没有进行强制性约束,交给虚拟机根据具体实现在自有把握

1.2 初始化
严格规定了5种情况必须对类进行初始化,在这之前验证 准备 解析已经完成

  • 遇到 new getstatic putstatic invokestatic 4条字节码指令时,生成这四条指令的java代码场景有:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(final修饰并且已在编译器把结果放入常量池静态字段中的除外),调用类的静态方法的时候

  • 使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有初始化则先要触发初始化。

  • 初始化一个类的时候其父类还没有进行初始化,则先要触发对其父类的初始化。

  • 启动虚拟机后,包含main方法的类要先被初始化

  • 当使用jdk7动态语言支持时,invoke.methodhandle,使java也可以像C语言那样将方法作为参数传递

    有且仅有以上5种场景中的行为被称为对一个类的主动引用,除此之外所有引用类的方式都不会触发初始化,被称为被动引用。

1.2.1 一些被动引用的例子

  • 对于静态字段只有直接定义这个字段的类才会被初始化,通过子类来引用父类中定义的静态字段,只会触发父类的初始化。
  • 通过定义数组来引用类,并不会对类进行初始化。
  • 常量在编译阶段会存入调用类的常量池中,本质并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

关于接口与类不同的地方在:初始化一个类的时候要求其父类全部都已经初始化过了,但是一个接口在初始化的时候不要求其父接口全部完成初始化,只有在真正使用到父接口的时候例如引用了父接口中定义的常量才会被初始化。

2. 类加载的过程

2.1加载
加载主要完成三件事情

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

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,然后再内存中实例化一个java.lang.Class类的对象 这个对象存储在方法区里而不是存放普通对象的堆中。

加载还未完成,连接阶段可能已经开始了,这两个阶段的开始时间仍然保持着固定的先后顺序。

类 和 数组加载过程的区别?

数组也有类型,称为“数组类型”。如:

String[] str = new String[10]; 

这个数组的数组类型是Ljava.lang.String,而String只是这个数组中元素的类型。

当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类,再由类加载器创建数组中的元素类。

而普通类的加载由类加载器完成。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器。

2.2验证
主要目的是确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全 包括以下验证

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

2.3准备

  • 为已经在方法区中的类中的静态成员变量分配内存 类的静态成员变量也存储在方法区中。
  • 为静态成员变量设置初始值 初始值为0、false、null等。

需要注意的是 类变量(被static修饰的变量)而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。

并且这里的初始值通常情况下是数据类型的零值,赋值操作是在编译后,初始化阶段才会执行。

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

2.5初始化
初始化阶段就是执行类构造器clinit()的过程。
clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。

在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。

初始化过程的注意点:

  • clinit()方法中静态成员变量的赋值顺序是根据Java代码中成员变量的出现的顺序决定的。
  • 静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量。
  • 静态代码块能给出现在静态代码块之后的静态成员变量赋值。
  • 构造函数init()需要显示调用父类构造函数,而类的构造函数clinit()不需要调用父类的类构造函数,因为虚拟机会确保子类的clinit()方法执行前已经执行了父类的clinit()方法。
  • 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成clinit()方法。
  • 接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化。
  • 接口中不能使用静态代码块。
  • 接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方- 法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接- 口的clinit()方法。
  • 虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方法会被执行,其它的方法都被阻塞。并且,只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次。

类加载器

把类加载过程的第一个阶段--加载阶段中的 通过一个类的全限定名来获取描述此类的二进制字节流 这个动作放到jvm外部去实现,以便让应用程序自己决定如何去获取所需要的类。

一个类需要由加载它的类加载器和这个类本身来确定它在java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。

比较两个类是否相等,只有在这两个类是否由同一个类加载器加载的前提下才有意义。

双亲委派模型

对于JVM来说类加载器分为两种

  1. 启动类加载器,使用C++实现,是虚拟机自身的一部分
  2. 其他的类加载器,Jva语言实现,独立于虚拟机外部,继承与抽象类 java.lang.ClassLoader

对于开发人员来说分为三种

  1. 启动类加载器,负责将放在lib目录中或者-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的 类库 加载到虚拟机内存中。
  2. 扩展类加载器,负责将lib\ext目录中的,或者java.ext.dirs系统变量所指定的路径中的所有类库。
  3. 应用程序类加载器, 这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库
双亲委派模型

类加载器之间的这种层次关系被称为双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。类加载器之间的父子关系一般不会以继承的方式实现,而是都使用组合的关系来复用父加载器的代码。

双亲委派模型的工作过程

 protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClassOrNull(name);
        }
        } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }
  1. 加载器比较依赖它的父类,收到类加载的请求时首先会去把这个请求委派给父类加载器去完成,所以所有的加载器请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(在它的搜索范围内没有找到这个类)时,自加载器才会尝试自己加载。
  2. 使用双亲委派模型,java类随着类加载器一起具备了一种带有优先级的层次关系。比如Object类只会在顶层的启动类加载器中加载,这样系统中就不会出现多个不同的Object类,如果执意要写一个重名的java类,那么这个类永远都不会被加载。
  3. 对于基础类又要调用回用户代码怎么解决?
    线程上下文类加载器,通过这个加载器,福类加载器可以请求自类加载器去完成类加载活动。这种行为违背了双亲委派模型的一般性原则。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容