类加载

类的生命周期

java类执行过程
类的生命周期图

类加载过程包括:加载-验证-准备-解析-初始化。
这个过程顺序并不是固定的,最多仅仅代表它们开始的顺序,实际上这五个过程是交叉进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

注意:解析阶段有可能在初始化阶段之后再开始。

生命周期过程详解

  1. 加载:一、将class文件加载在内存中;二、将静态数据结构(数据存在于class文件的结构)转化成方法区中运行时的数据结构(数据存在于JVM时的数据结构);三、在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口。
  2. 验证:确保加载的类符合JVM规范与安全。
  3. 准备:为static变量在方法区中分配空间,设置变量的初始值。例如static int a=3,在此阶段会a被初始化为0,其他数据类型参考成员变量声明。
  4. 解析:虚拟机将常量池的符号引用转变成直接引用。例如"aaa"为常量池的一个值,直接把"aaa"替换成存在于内存中的地址。
    • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形 式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用 的目标并不一定已经加载到内存中。
    • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。
  5. 初始化:初始化阶段是执行类构造器<clinit>()方法。在类构造器方法中,它将由编译器自动收集类中的所有类变量的赋值动作(准备阶段的a正是被赋值a)和静态变量与静态语句块static{}合并,初始化时机后续再聊。
  6. 使用:正常使用。
  7. 卸载:GC把无用对象从内存中卸载。

类初始化时机

只有当对类主动使用的时候才会导致类的初始化。

主动引用(发生类初始化过程)
  1. 创建类的实例,也就是new的方式
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(如Class.forName(“com.shengsiyuan.Test”))
  5. 初始化某个类的子类,则其父类也会被初始化
  6. Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
被动引用(不会发生类的初始化)
  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  2. 定义对象数组,不会触发该类的初始化。
  3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  4. 通过类名ClassName.class获取Class对象,不会触发类的初始化。
  5. Class.forName("类名全路径")加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

类加载器

主要的类加载器有

  1. 启动类加载器(Bootstrap ClassLoader):由C++实现,没有父类,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
  2. 扩展类加载器(Extension ClassLoader):由Java语言实现,父类加载器为null,负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
  3. 应用程序类加载器(Application ClassLoader):由Java语言实现,父类加载器为ExtClassLoader,负责加载用户路径(classpath)上的类库。
  4. 自定义加载器:可以通过继承java.lang.ClassLoader或URLClassLoader实现自定义的类加载器,父类加载器为AppClassLoader。
双亲委派模式
image.png

JVM使用的是双亲委派模式的类加载机制,这种机制的好处有:

  1. 避免类的重复加载
  2. 保证核心基础jar包加载的安全问题。

加载过程大致可以分为两个过程:

  1. 查找过程,首先从收到请求的加载器开始,从下到上查找是否加载过该类,从下到上的顺序:自定义类加载器->应用类加载器->拓展类加载器->启动类加载器;
  2. 加载过程,如果找不到加载过该类,则从上到下,从启动类加载器开始尝试从自己负责的路径下加载该类,如果加载失败则由下级加载器加载,从上到下的顺序:启动类加载器 -> 拓展类加载器 ->应用类加载器 -> 自定义类加载器。
加载器类图
image.png

从类图可以看出

  • 拓展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。
  • sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的。
  • ExtClassLoader并没有重写loadClass()方法,而AppClassLoader重载了loadCass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。
自定义加载器

方式

  1. 继承URLClassLoader,不需要重写 findClass()等方法,继承双亲委派模式。
  2. 继承ClassLoader,只重写findClass方法,这种方式继承双亲委派模式。
  3. 继承ClassLoader,重写loadClass方法,不使用双亲委派模式,自定义类加载模式。

自定义加载器场景:

  1. 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。
  2. 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。
  3. 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。

注意:由于双亲委托机制,classpath目录下的类默认由Application类加载器加载,所以,自定义加载器如果没有重写loadClass方法去加载classpath下的类是不会成功的,当然,可以通过直接调findClass()绕过双亲委托来加载。

双亲委派模型的破坏者1——线程上下文加载器
image.png

在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

加载方式
  1. 显式加载:指的是在代码中通过调用ClassLoader加载class对象,比如代码中通过Class.forName()、this.getClass.getClassLoader.LoadClass(),自定义类加载器中的findClass()方法等。
  2. 隐式加载:不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,除了显式加载的,其它都是隐式加载。
ClassLoader关键方法
  • loadClass(String),该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,所以,自定义加载器可以覆盖loadClass方法覆盖双亲委派模式。
  • findClass(String) ,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
  • defineClass(byte[] b, int off, int len) ,是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象。
  • resolveClass(Class≺?≻ c) ,类解析阶段的操作方法,给Classloader链接一个类。

注意:
如果直接调用defineClass()或findClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

重复类加载

Java中来自不同Jar包中的相同的类名(包名,类名)在加载时类加载器将按照Class Path中的顺序加载,相同的类名仅仅会加载一次,顺序排在前面的类会被加载,加载成功后,后面再遇到相同类会忽略。
因此,最终所使用的类取决于ClassLoader对类的的选择,即Maven往Class Path打包的顺序。

类卸载

类加载后JVM内存图

JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  1. 该类所有的实例都已经被GC。
  2. 该类的java.lang.Class对象没有在任何地方被引用。
  3. 加载该类的ClassLoader实例已经被GC。

所以

  • 由Java虚拟机自带的类加载器(启动类加载器、扩展类加载器、应用程序类加载器)所加载的类,在虚拟机的生命周期中,始终不会被卸载。因为Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
  • 由用户自定义的类加载器加载的类是可以被卸载的,如下图使用自定义加载器加载的类和对象,只要左侧三个引用变量置为null,类加载器生命周期结束、加载的Class对象生命周期结束、Sample对象生命周期结束,方法区的类信息也会被卸载。

是否同个类

在JVM中表示两个class对象是否为同一个类对象存在两个必要条件

  1. 类的完整类名必须一致,包括包名。
  2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

获取Class对象的三种方式

Class类是反射机制的起源,我们得到Class类对象有3种方法:
第一种:通过类名获得
  Class<?> class = ClassName.class;
第二种:通过类名全路径获得:
  Class<?> class = Class.forName("类名全路径");
第三种:通过实例对象获得:
  Class<?> class = object.getClass();

Class.forName()和ClassLoader.loadClass()区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

注:
Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

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

推荐阅读更多精彩内容