先测试一番,全对的就走人

//题目一
class Parent1{
public static String parent1 = "hello parent1";
static { System.out.println("Parent1 静态代码块"); }
}
class Children1 extends Parent1{
public static String children1 = "hello children1";
static {System.out.println("Children1 静态代码块");}
}
//----------------------------------------------------------------
//题目二
class GrandParent2{
static { System.out.println("GrandParent2静态代码块"); }
}
class Parent2 extends GrandParent2{
public static String parent2="hello parent2";
static{ System.out.println("Parent2 静态代码块");}
}
class Children2 extends Parent2{
public static String children2 ="hello children2";
static{ System.out.println("Children2 静态代码块");}
}
//----------------------------------------------------------------
//题目三
class GrandParent3{
static { System.out.println("GrandParent3静态代码块"); }
}
class Parent3 extends GrandParent3{
public final static String parent3="hello parent3";
static{ System.out.println("Parent3 静态代码块");}
}
class Children3 extends Parent3{
public static String children3 ="hello children3";
static{ System.out.println("Children3 静态代码块");}
}
//测试
public class ClassLoaderTest {
public static void main(String[] args) {
//测试一的输出
System.out.println(Children1.children1);
System.out.println("-------------------------------");
//测试二的输出
System.out.println(Children2.parent2);
System.out.println("--------------------------------");
//测试三的输出
System.out.println(Children3.parent3);
}
//你认为输出什么呢
}
答案如下

Parent1 静态代码块

Children1 静态代码块

hello children1


GrandParent2静态代码块

Parent2 静态代码块

hello parent2


hello parent3

如果看清到这里,你的回答和结果一致,那么你真的懂了,可以转载给他人了,如果出乎你的意料,请认真看完。

什么是类加载(或者初始化)

Java源代码经过编译之后转换成class文件,在系统运行期间当需要某个类的时候,如果内存中还没该class文件,那么JVM需要对这个类的class文件进行加载,连接,初始化,JVM通常会连续完成这三步,这个过程叫做类的加载或者初始化, 类从磁盘加载到内存必须经历这三个阶段的。

重点是:类的加载都是在程序运行期间完成的,这提供了无限可能,意味着你可以在某个阶段对类的字节码进行修改,JVM也确实提供了这样的功能。

类的加载并不是对象的创建,类的加载是在为对象创建前做一些信息准备。

类的生命周期

我们明白了什么是类的加载,那么从类的加载到最后类的卸载成为类在JVM中的声明周期,这个生命周期总共包含了七个阶段:我画一张图,如下,我们逐个分析一下类的生命周期的每一步。

这是类的生命周期的,但它不总是按照这个固定的流程进行的,我们先知道这个就行,后面再说。

加载

类的加载指的是把class文件从磁盘读入内存中,将其放入元数据区域并且创建一个Class对象,放入堆中,Class对象是类加载的最终产品,Class对象并不是new出来的对象。

元数据区域存储的信息

  1. 这个类型的完整有效名
  2. 这个类型的直接父类完整有效名
  3. 这个类型的修饰符(public final abstract等)
  4. 这个类型的直接接口的列表

Class对象中包含的如下信息,这也是我们能够通过Class对象获取类的很多信息的原因

  1. 类的方法代码,方法名,字段等
  2. 类的返回值
  3. 类的访问权限

加载class文件有很多种方式,可以从磁盘上读取,可以从网络上读取,可以从zip等归档文件中读取,可以从数据库中读取

验证

验证的目的是验证class文件的正确性,是否能够被当前JVM虚拟机执行,主要包含了一些部分验证,验证非常重要,但不是必须的(正常情况下都是正确的)

文件格式验证:比如JDK8加载的是JDK6下编译的class文件,这肯定不行。

元数据验证:确保字节码描述信息符合Java语言规范的要求,你理解为校验外壳,比如类中是否实现了接口的所有方法。

字节码验证:确定程序语义执行是合法的,校验内在,校验方法体,防止字节码执行过程中危害JVM虚拟机。

符合引用验证:其对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,比如:符号引用中的类、字段、方法的访问性是否可被当前类访问,通过全限定名,是否能找到对应的类。

准备(重点)

验证完成之后,JVM就开始为类变量(静态变量) 分配内存,设置初始化值, 记住两点

  1. 不会为成员变量分配内存的。
  2. 初始化值是指JVM默认的指,不是程序中指定的值。

看如下代码,你就明白了:

//类变量,初始化值是 null, 不是123
public static String s1 = "123"
//成员变量
public String s2 = "456"

但有一个特殊,如果一个类变量是final修饰的常量,那么在准备阶段就会被赋值为程序中指定的值,如下代码,初始值是123

//初始值是123,不是null
public static final String s1 = "123"

为什么会这样呢?两行代码的区别在于final,final在Java中代表着不可变,不能赋值了之后重新赋值,所以一开始就必须赋值为用户想要的默认值,而不是Java语言的默认值。而不是final修时的变量有可能在之后发生变化,所以就先赋值为Java语言的默认值。

解析

解析阶段主要是将常量池中的符号引用转换为直接引用,解析动作主要包含类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用。

符号引用包括什么呢?

  1. 类和方法的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符,

直接引用是是什么呢?一个指向目标的指针地址或者句柄。

举个例子如下:

// 123 是一个符号引用,123所对应的内存中的地址是一个直接引用。
public static final String s1 = "123"

常量池是什么呢?,常量池包含好多种,字符串常量池,class常量池,运行时常量池,这里指的是class常量池。我们写的每一个Java类被编译后,就会形成一份class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用,每个class文件都有一个class常量池。

比如解析阶段,找不到某个字段就抛出NoSuchFieldError,同理NoSuchMethodError

初始化(重点)

初始化阶段用户定义的Java代码才会真正开始执行,一般来说当首次主动使用某个类的时候就会对该类初始化,初始化某个类时也会初始化这个类的父类,这里的首次主动使用,大家要理解清楚了,第二次使用时不会初始化的。类的初始化其实就是执行类构造器的过程,这个不是我们代码定义的构造方法。

下面列举了JVM初始化类的时机:

  1. 创建对象时(比如:new Person())
  2. 访问类变量时
  3. 调用类的静态方法时
  4. 反射加载某个类是(Class.forName("....."))
  5. Java虚拟机启动时被标明为启动类的类(单测时),Main方法的类。

初始化时类变量会被赋予真正的值,也就是开发人员在代码中定义的值,也会执行静态代码块。

JVM初始化类的步骤:

  1. 若该类还没有被加载和连接,则程序先加载并连接该类
  2. 若该类的父类还没有初始化,则先初始化该类的夫类
  3. 若该类中有静态代码块,则系统依次执行这些代码块

上面提到了首次主动使用时初始化类,那么就有被动使用,被动使用是什么意思呢?比如说通过子类引用父类的静态字段,那么子类会初始化吗?答案是不会的,所以下面测试的子类的静态代码块是不会执行的。

class Parent4{
public final static String parent4="hello parent4";
} class Children4 extends Parent4{
static{ System.out.println("Children4 静态代码块");}
}
public class ClassLoaderTest {
public static void main(String[] args) {
//测试四的输出
System.out.println(Children4.parent4);
}
}

再说一个点解析时有提到常量池的概念,在经过初始化后,类就被加载到内存中去了,这个时候jvm就会将class常量池中的内容存放到运行时常量池中,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的

上面还有一个关键字一般来说,那么不一般呢?类加载器并不需要等到某个类被首次主动使用时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它.

使用

使用就比较简单了,JVM初始化完成后,就开始按照顺寻执行用户代码了。

卸载

类卸载有个前提,就是class的引用是空的,要么程序中手动置为空,要么进程退出时JVM销毁class对象,然后JVM退出。只要class引用不存在,那么这个类就可以回收了。

你自己可以试验一下,写一个classload类加载器,写一个Test测试类,实际测试一下,我的测试代码如下:

public class ClassTest {
public static void main(String[] args){
ClassLoaderMy classLoader = new ClassLoaderMy();
classLoader.setRoot("D:\\github\\java_common\\target\\classes\\");
Class clazz = classLoader.findClass("jvm.Test类中有一个静态代码块。");
Object obj = clazz.newInstance();
System.out.println("1:"+clazz.hashCode());
obj=null;
System.out.println("2:"+clazz.hashCode());
classLoader = null;
System.out.println("3:"+clazz.hashCode());
clazz = null; System.out.println("此时 obj classloader clazz 都为空了"); classLoader = new ClassLoaderMy();
classLoader.setRoot("D:\\github\\java_common\\target\\classes\\");
clazz = classLoader.findClass("jvm.Test");
System.out.println("4:"+clazz.hashCode());
obj = clazz.newInstance();
}
//打印结果如下,看之前你猜一猜。Test类中有一个静态代码块。
}

初始化了

1:1775282465

2:1775282465

3:1775282465

此时 obj classloader clazz 都为空了

4:1267032364

初始化了

最终结果你会发现,前三个hashcode的值是一样的,第四个的值发生了变化,说明class文件被卸载了后重新加载生成了新的class对象,否则,同一个对象的hashcode是不会发生变化的,而且Test类的静态代码块执行了两遍,完整代码地址如下:

https://github.com/sunpengwei1992/java_common/tree/master/src/jvm

我画了一张图,方便大家更好的理解,如下,当左边的三个变量都指向为null时,最右边的元数据区域的代表Class对象的Test二进制数据就会被卸载,当下次使用时就会被重新加载,初始化等。

但是,注意了 由JVM自带的类加载器加载的类,在JVM生命周期中,始终不会被卸载,

JVM自带的类加载器包括根类加载器,扩展类加载器,系统类加载器,这些回头单聊。

解密测试题目

接下来我们聊一聊一开始的测试题,其实看到这里,想必大家都明白了吧,还是说一说。

第一个不用讲了,都会。

第二题:子类Children2,父类Parent2, 祖父类GrandParent2,我们通过Chidlren2打印父类Parent2的静态变量,类加载时,发现有父类存在,逐层往上加载,那么Parent2和GrandParent2都会被加载,所以Parent2和GrandParent2的静态代码块都会被执行,而Children2就不会被加载了,因为不符合首次主动使用的条件。

第三题:同样的道理,只是Parent3和GrandParent3的静态代码块为什么没执行呢,因为Parent3的静态变量是final类型的,在准备阶段就已经完成了,不需要再逐层往上加载了.

提一下接口的加载

当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会加载该接口,如下代码,执行main方法,Parent5接口是不会被加载的,parent5变量也是不会被初始化的。

interface Parent5{
public final static String parent5 = "hello parent5";
}
interface Children5 extends Parent5{
public final static String children5 = "hello children5";
}
public static void main(String[] args) {
System.out.println(Children5.children5);
}

表格整理一下流程

深入Java类加载全流程,值得你收藏的更多相关文章

  1. 大牛带你学会java类加载机制,不要错过,值得收藏!

    很多人对java类加载机制都是非常抗拒的,因为这个太难理解了,但是我们作为一名优秀的java工程师,还是要把java类加载机制研究和学习明白的,因为这对于我们在以后的工作中有很大的帮助,因为它在jav ...

  2. Java/JDK安装教程手册(正规图文全流程)、运行、环境配置

    Java/JDK教程手册 本文提供全流程,中文翻译.Chinar坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) 一 Download Resouc ...

  3. Unity3D 发布APK安卓环境配置步骤、安装、教程(含Java/Android)(超全流程)

    Unity3D安卓环境配置运行 本文提供全流程,中文翻译.Chinar坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享. ...

  4. 深入JVM类加载器机制,值得你收藏

    先来一道题,试试水平 public static void main(String[] args) { ClassLoader c1 = ClassloaderStudy.class.getClass ...

  5. Java对接拼多多开放平台API(加密上云等全流程)

    前言 本文为[小小赫下士 blog]原创,搬运请保留本段,或请在醒目位置设置原文地址和原作者. 作者:小小赫下士 原文地址:Java对接拼多多开放平台API(加密上云等全流程) 本文章为企业ERP(I ...

  6. Java 类加载机制 ClassLoader Class.forName 内存管理 垃圾回收GC

    [转载] :http://my.oschina.net/rouchongzi/blog/171046 Java之类加载机制 类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指 ...

  7. 关于java类加载机制的一些理解

    关于java的类加载机制加载顺序,这个东西可以说是基础的东西,不过很遗憾这方面很多人也都不是很在意,比如我自己,最近上班闲下来了,就开始看一些博客文章了,今天恰好被一篇博文给吸引了,并且他的示例题一开 ...

  8. Netty Nio启动全流程

    Netty Nio启动全流程 1. 各组件之间的关系 说明:EventLoopGroup类似线程池,EventLoop为单线程,每个EventLoop关联一个Nio Selector,用于注册Chan ...

  9. Java类加载机制与Tomcat类加载器架构

    Java类加载机制 类加载器 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这 ...

随机推荐

  1. jQuery无new创建对象原理

    // jQuery 无new 创建对象套路 (function(g,undefined){ var foo = function(){ return new foo.fn.init(); }; foo ...

  2. 【sublime】Pretty Json插件的安装与配置使用

    一.安装 Package Control  代码安装 从菜单 View - Show Console 或者 ctrl + ~ 快捷键,调出 console.将以下 Python 代码粘贴进去并 ent ...

  3. Java第一次创建对象速度比之后慢的原因

    类的对象在第一次创建的时候,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载.如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入.一般某个类 ...

  4. 【温故知新】Java web 开发(二)Servlet 和 简单JSP

    系列一介绍了新建一个 web 项目的基本步骤,系列二就准备介绍下基本的 jsp 和  servlet 使用. (关于jsp的编译指令.动作指令.内置对象不在本文讨论范围之内) 1. 首先,在 pom. ...

  5. 从头学pytorch(十一):自定义层

    自定义layer https://www.cnblogs.com/sdu20112013/p/12132786.html一文里说了怎么写自定义的模型.本篇说怎么自定义层. 分两种: 不含模型参数的la ...

  6. OpenVINO 入门

    关于OpenVINO 入门,今天给大家分享一个好东西和好消息! 现如今,说人工智能(AI)正在重塑我们的各行各业绝不虚假,深度学习神经网络的研究可谓如火如荼, 但这一流程却相当复杂,但对于初学者来说也 ...

  7. Java程序运行原理

    概念介绍: ![file](https://img2018.cnblogs.com/blog/1454321/202001/1454321-20200104145655999-149562495.jp ...

  8. linux dubbo-admin-2.6.0 环境搭建

    1.去maven官网下载apache-maven-3.6.2-bin.tar.gz安装包 2.上传至linux服务器中 3.解压maven安装包 tar -zxvf apache-maven-3.6. ...

  9. AES Base64 C++加密工具

    最近写了一段C++ 的AES 加密的工具,在github 上找了很多工具,都不太好用,Star数字比较的高的一个,只能加密16个字节的,加密数字长数据,会出现乱码,不能使用 这里附上代码,文件以供大家 ...

  10. C#录制视频

    这是一个使用C#语言制作的录制框架,支持录制桌面,多屏,声音,摄像头,某个应用程序的界面 1.安装 使用此框架需要安装扩展包Kogel.Record,可以Nuget上搜索 或者使用Nuget命令 In ...