JVM详解-类加载机制、类加载器、双亲委派模型

目录

类加载过程

加载

验证

准备

解析

初始化

卸载

生命周期

类与类加载器

双亲委派模型

破坏双亲委派模型


类加载过程

        虚拟机把描述类的数据从 Class 文件加载到内存,最终生成可以被虚拟机直接使用的java 类型,这就是虚拟机的类加载机制。与那些在编译时需要进行连接工作的语言不同,java 类型的加载、连接和初始化都是在程序运行期间完成的。(可以实现反射,OSGi 等技术)

        类的生命周期:加载、连接(验证、准备、解析)、初始化、使用、卸载。

        加载、连接(验证、准备、解析)、初始化是类的加载过程。

        虚拟机规范中没有强制约束什么时候开始类加载,但以下几种情况必须开始初始化(而加载、验证、准备必须在初始化之前开始):

         第一:生成该类对象的时候(new 关键字)、读取或设置一个类的静态字段(被final修饰、已在编译期放入常量池的静态字段除外)、调用一个类的静态方法。会初始化该类及该类的所有父类;

        第二:初始化一个类时,如果其父类还没有被初始化,则先初始化父类;

        第三:class.forName("类名"),使用 java.lang.reflect 包的反射;

        第四:虚拟机启动时,main 方法的类;

加载

在加载阶段,虚拟机主要完成三件事:

        1.通过一个类的全限定名来获取定义此类的二进制字节流。

        2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。

        3.在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区域数据的访问入口。

        数组类本身不通过类加载器创建,由 java 虚拟机直接创建。但数组类的元素(指数组去掉维度后的类型)最终是由类加载器创建。

        加载阶段完成后,虚拟机外部的二进制字节流(如.class 文件、jar、war 包)就按照虚拟机所需要的格式存储在方法区之中,然后在内存中实例化一个 java.lang.Class 类的对象(对于 HotSpot,Class 对象比较特殊,它虽然是对象,但是存放在方法区里)

验证

        验证的目的是为了确保 Class 文件中的字节流包含的信息符合当前虚拟机的要求、规范,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段这些内存都将在方法区中分配。

 1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。         

2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在 Java 代码中被显式地赋予的值。

        假设一个类变量的定义为:public static int value = 3;

        那么变量 value 在准备阶段过后的初始值为 0,而不是 3,因为这时候尚未开始执行任何 Java 方法,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。

        对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。

        对于同时被 static 和 final 修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被 final 修饰的常量则既可以在声明时显式地为其赋值(只被final修饰是常量,存放在方法区),也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。

        对于引用数据类型 reference 来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即 null。

        如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

3、如果类字段的字段属性表中存在 ConstantValue 属性(即同时被final 和static修饰,且为基本类型或 String),那么在准备阶段变量 value 就会被初始化为ConstValue属性所指定的值。(因为 static 的要被赋默认值 0,而 final 又不能变,所以提前赋值

        假设上面的类变量 value 被定义为: public static final int value = 3;

        编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据ConstantValue 的设置将 value 赋值为 3。         方法区是存放虚拟机加载类的相关信息,如类、静态变量和常量

        类加载的时候将所有方法的字节码放到方法区的

解析

        加载、验证、准备、初始化、卸载这几个阶段的顺序是确定的,而解析阶段不一定,可以延迟执行,在某些情况下可以在初始化阶段之后再开始,这是为了支持java 语言的运行时绑定。

        解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。包括类或接口的解析、字段解析、类方法解析、接口方法解析。(比如 String s ="aaa",转化为s 的地址指向“aaa”的地址)

        常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java 层面的常量概念,如文本字符串、被声明为 final 的常量值、-128-127 的 Integer 包装类等。而符号引用总结起来则包括了下面三类常量:

        1、类和接口的全限定名(即带有包名的 Class 名,如:org.lxh.test.TestClass)

        2、字段的名称和描述符(private、static 等描述符)

        3、方法的名称和描述符(private、static 等描述符)

        虚拟机在加载 Class 文件时才会进行动态连接,也就是说,Class 文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接 被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

        前面说解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。

        这里说明下符号引用和直接引用的区别与关联:

        符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

        直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

初始化

        初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java 程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,如赋值。

卸载

        该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。

        加载该类的 ClassLoader 已经被回收。

        该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

        如果以上三个条件全部满足,jvm 就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java 类的整个生命周期就结束了。

        由 Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面介绍过,Java 虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java 虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的 Class 对象,因此这些 Class 对象始终是可触及的。

        由用户自定义的类加载器加载的类是可以被卸载的(没有引用时)。当再次有需要时,会检查类的 Class 对象是否存在,如果存在会直接使用,不再重新加载;如果不存在类会被重新加载

生命周期

        实例变量取决于类的实例。每创建一个实例,java 虚拟机就会为实例变量分配一次内存,实例变量位于堆区中,其生命周期取决于实例的生命周期。

        静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。

        总而言之:静态变量生命周期就是类的开始和销毁(与类相同)

                           实例变量生命周期就是对象的开始和销毁(与对象相同)

类与类加载器

类加载器用于实现类的加载动作

        类加载器按照层次,从顶层到底层,分为以下三种:

        (1)启动类加载器(Bootstrap ClassLoader)(c++实现,其他加载器都是java 实现)这个类加载器负责将存放在 JAVA_HOME/lib 下的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java 程序直接引用。

        (2)扩展类加载器(Extension ClassLoader) 这个加载器负责加载 JAVA_HOME/lib/ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器

        (3)应用程序类加载器(Application ClassLoader) 这个加载器是 ClassLoader 中 getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Classpath)上所指定的类库,可直接使用这个加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。

         对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。(相等指类的 Class 对象的 equals 方法,使用 instanceof 关键字)。

        JVM 如何确立每个类在 JVM 的唯一性?类的全限定名和类加载器。

双亲委派模型

        双亲委派模型工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成 ,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类才会尝试加载。

        如类 java.lang.Object,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反如果没有使用双亲委派模型,各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的Object 类,java 类体系中最基础的行为也就无法保证,应用程序也会变得一片混乱。

        如自己定义一个 object 类,不会首先加载这个类,只会加载最顶层的object 类。

        类加载器的父类不是继承关系(子类可以调用父类),而是组合关系

破坏双亲委派模型

        一般的场景中使用 Java 默认的类加载器即可,但有时为了达到某种目的又不得不实现自己的类加载器,例如为了达到类库的互相隔离,例如为了达到热部署重加载功能。

        重 新 定 义 一 个 继 承 ClassLoader 的 TestClassLoaderN 类,这个类与前面的TestClassLoader 类很相似,但它除了重写 findClass 方法外(一般的自定义类加载器只重写 findClass),还重写了 loadClass 方法,默认的 loadClass 方法是实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时才由自己加载。这里为了破坏双亲委派机制必须重写 loadClass 方法,即这里先尝试交由 System 类加载器加载,加载失败才会由自己加载。它并没有优先交给父类加载器,这就打破了双亲委派机制。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值