简述 Java 类及其实例使用历程

前言

Java 语言是一门面向对象的语言(Java 类型一切皆对象,基本类型除外),我们使用 Java 语言进行程序编写,都是以类的形式进行组织。各种各样的职能类与工具类配以恰当的业务逻辑构成了一个个功能丰富多彩的应用。

可以认为,使用 Java 语言编写的程序最主要的就是各种类的加载使用。因此,了解 Java 类的加载使用过程,会让我们可以更加高效编写出正确,高效的代码。

因此,本篇博文主要对 Java 类加载及其对象创建的整个过程做一个相对完整的讲解。

Java 类加载及其对象创建过程

熟悉 Java 的人都知道,Java 语言是一门基于 JVM 的平台无关的编程语言。使用 Java 编写的语言最终会经过编译器(即 javac)的编译后,生成 Class 字节码文件。最终由 JVM 将该 Class 文件加载进内存中,创建出一个对应的 Class 对象;然后由该 Class 对象,就可以实现实例的创建。

因此,类的加载与对象创建具体涉及到如下三个过程:

  1. Java 源码编译生成 Class 文件
  2. JVM 加载 Class 文件到内存,生成 Class 对象
  3. 创建类实例

下面依次对上述 3 个过程进行分析

Java 源码编译生成 Class 文件

先简单介绍下 Class 文件:Class 文件是一组以 8 bit(即 1 字节)为基础单位的二进制流,其内的各个数据项都具备一定的描述信息(具体信息参考 Java 虚拟机字节码规范),排列紧凑且无多余内容。

Class 文件存储了 Java 语言定义的类的全部信息,包括类名,类属性和类方法····其具备强大的信息描述能力,而 Class 文件内部结构其实只有两种数据类型:无符号数

  • 无符号数:属于字节码的基本数据类型。以 u1u2u4u8 分别代表 1个字节,2个字节,4个字节和8个字节的无符号数。无符号数可以用来描述数字,索引引用,数量值或者按照 UTF-8 编码构成的字符串值。

  • :有多个无符号数或者其他表作为数据项构成的复合数据类型。一个类拥有多种结构,比如继承信息,字段,方法等等,这些结构在 Class 文件中都以表的形式进行存储(不同的结构对应不同的表,每个表都有自己定义的格式)。所有表都习惯地以 "_info" 结尾。整个 Class 文件本质上就是一张表。其格式如下所示:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count-1
u2 access_flag 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

从上述 Class 文件格式表中可以看到,Class 文件其实就是依次存储了以下内容(对 cp_infofield_infomethod_infoattribute_info 这些表的具体格式就不展开讲解了):

  • 魔数(magic):用于验证是否是 Class 文件,其固定值为:0xCAFEBABE

  • 次版本号(minor_version):Class 文件次版本号

  • 主版本号(major_version):Class 文件主版本号

  • 常量池(constant_pool):常量池主要存放两大类常量:字面量(Literal)符号引用(Symbolic References)
    字面量 比较接近于 Java 语言层面的常量概念,如字符串,声明为 final 的常量值等等。
    符号引用 主要包含三类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

  • 访问标志(access_flag):用于识别类或接口层次的访问信息,包括:该 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。

  • 类索引(this_class):用于确定类的全限定名

  • 父类索引(super_class):用于确定父类的全限定名

  • 接口索引集合(interfaces):用于描述这个类实现的接口集合

  • 字段表(fields):用于描述接口或者类中声明的变量。字符包括类级变量(即 static 变量)和实例级变量

  • 方法表(methods):用于描述接口或者类中声明的方法。方法中的 Java 代码(方法块)经过编译器翻译成字节码后,存放在方法属性表集合中的一个名为 Code 的属性里面

  • 属性表(attributes):在 Class 文件中,字段表,方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息

Java 虚拟机不予任何语言相关联,包括 Java,它只与 Class 文件有联系。当 Java 源码被编译成 Class 文件后,Java 源码定义的类信息就被完整地存储到 Class 文件中了。

到此,Java 源码经由编译器就会编译成 Class 字节码文件了。

此时,就可以进入到下一步:JVM 加载 Class 文件到内存

JVM 加载 Class 文件到内存,生成 Class 对象

当我们 new 一个对象或者调用了类的静态字段/静态方法时,就会触发类的加载。

在类加载之前,JVM 进程肯定是要先启动,后续才能进行类的加载过程。

JVM 启动时,会把其管理执行 Java 程序的内存划分为若干个不同的数据区域,称为 虚拟机运行时数据区域JVM 内存结构。具体的区域如下:

  • 程序计数器:一块较小的内存,指向当前线程运行的字节码指令地址。

  • 虚拟机栈:描述线程执行 Java 方法的内存模型:每个方法在执行时,会创建一个 栈帧 压入到线程虚拟机栈中,栈帧 用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法的调用和执行完成都对应着一个栈帧的入栈和出栈过程。Java 虚拟机的执行引擎是基于栈结构的,此处的栈指的就是该虚拟机栈(更确切说应当是:操作数栈)。

  • 本地方法栈本地方法栈虚拟机栈 的作用相似,区别在于 虚拟机栈 用于执行 Java 方法,而 本地方法栈 用于执行 Native 方法。当执行 Native 方法时,程序计数器 的值为空(Undefined)。

  • Java 堆:用于存放几乎所有的对象实例和数组实例。该区域是 JVM 所管理的内存中最大的一块,也是垃圾收集器主要进行的区域。

  • 方法区:用于存储已被虚拟机加载的类信息,常量(final),静态变量(static)和即时编译器编译后的代码等数据。该区域的垃圾回收主要针对的是常量池中的废弃常量和无用的类(该区域的垃圾回收效率较低,因此甚至可以不对该区域进行垃圾回收处理)。在 HotSpot 虚拟机中,该区域也称为 永久代

JVM 内存结构图如下所示:

运行时数据区

当 JVM 内存区域分配完成后,就可以进行类的加载。

类的加载过程主要涉及 3 个阶段操作:加载阶段连接阶段初始化阶段
其中,连接阶段 又可以分为 3 个过程:验证准备解析
如下图所示:

类的生命周期

下面依次对上述类加载过程进行简单讲解:

  • 加载:在加载阶段,虚拟机主要完成以下 3 件事情:
    1)通过类的全限定名找到该类的字节码文件流。这部分功能有 类加载器 进行加载,对于 Java 开发人员来说,Java 虚拟机类加载器可分为 4 种类型:启动类加载器(Bootstrap ClassLoader)扩展类加载器(Extension ClassLoader)应用程序类加载器(Application ClassLoader)自定义类加载器(User ClassLoader)。其中,应用程序类加载器 也被称为 系统类加载器,如果程序没有自定义类加载器,那么一般情况下使用的就是 应用程序类加载器。要判断一个类(对象)是否为同一个类,必须满足两个条件:由同一个类加载器加载 与 同一个类文件(即字节码相同)。因此,为了防止 Java 程序出现类混乱,Java 虚拟机采用的默认类加载机制为:双亲委派模型,简而言之,双亲委派模型 就是说将类加载请求先交由父类加载器进行加载,父类加载器无法加载时,才由子类加载器进行加载。这样就保证了 Java 程序中类对象的一致性。
    2) 将该类字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
    3)在内存中(没有具体规定一定在堆中分配,对于 HotSpot 虚拟机而言,Class 对象在方法区中分配)生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

  • 验证:验证是连接阶段的第一步,该过程主要是为了确保 Class 文件的字节流正确且安全。其主要会完成下面 4 个阶段的检验动作:
    1)文件格式验证:主要验证字节流是否符合 Class 文件格式规范,并且能被当前版本的虚拟机处理。该阶段验证通过后,就会将字节流转化为方法区中的对应类的存储结构(该操作其实就是 加载 阶段要完成的第 2 件事,因此,加载 阶段 和 连接 阶段启动有先后顺序,但存在交叉执行),后续 3 个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
    2)元数据验证:主要验证 Class 文件字节流是否符合 Java 语言规范。
    3)字节码验证:主要是对类的方法体进行校验分析,确保方法在运行时不会做出危害虚拟机安全的事件。
    4)符号引用验证:该阶段其实是发生在链接的第三阶段--解析 阶段发生的,主要就是对方法区常量池符号引用的校验。

  • 准备:准备阶段是为类变量(即 static 变量)分配内存并进行默认初始化(赋予默认零值)过程。

  • 解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程(即将符号引用替换为内存中已存在的对象)。

  • 初始化:类初始化阶段是类加载的最后一步,该阶段主要做的就是按出现顺序依次执行类字段的定义初始化和构造初始化(静态代码块)。

到此,方法区中就已经存在一个完备的 Class 对象了。

现在,我们就可以 new 出一个实例对象了。

创建类实例

当我们在代码用 new 一个对象时,这个操作反映到 Class 文件上其实就是一个 new 指令,该指令后面会跟随一个类的全限定名。JVM 通过在方法区运行时常量池中,通过该类的全限定名就可以找到对应的类信息,然后就可以在堆中分配一块内存,用于创建该类实例变量,然后依次执行实例的默认初始化,定义初始化和构造初始化,这样,类实例对象就成功创建完成了。

类实例对象创建完后,就可以使用了。当使用结束时,JVM 就会在垃圾回收器启动时,对其进行存活判断,看是否要回收该对象。

因此,接下来就是垃圾回收过程。

垃圾回收

一个对象要想让垃圾收集器进行回收,则首先要判断该对象是否是一个 “无用对象”,即没有其他实例引用该对象。

判断对象是否 “无用” 的方法一般有两种:引用计数法可达性分析

  • 引用计数法:通过给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器就减1;任何时候,当计数器为0时,表明该对象不可能在被使用,则可进行回收。
    引用计数 的优点是实现简单,判定效率也很高。但存在一个问题:很难解决对象之间循环引用问题,如下图所示:
有效引用

假设 A 是一个持久对象(不会被回收),其引用了 B;而 B 引用了 C,C引用了 D,D 由引用了 B。因此,各自对象的引用计数器如上图红色数字所示。假设此时 A 断开了 B 的引用,如下图所示:

无效引用

此时,B,C,D 本来应当算是无效对象,但由于他们循环引用,导致各自的计数器不为0,因此无法回收。

  • 可达性分析:该算法通过一系列的 GC Roots 的对象作为起点,从这些节点开始向下搜索,处于 GC Roots 引用链上的对象即为存活对象,不可回收;无法达到 GC Roots 的对象即为不可达对象,可被回收。
    上图中以 A 作为 GC Roots,则第二幅图因为 B,C,D 均没有引用链可以到达 A,因此,B,C,D 为不可达对象,可被回收。

Java 使用的判定算法为:可达性分析法

上面只是对单个对象的状态进行分析,而我们知道,当 GC 时,处理的对象是一大块内存的所有对象,不同的内存中,对象的状态(声明周期)不同,因此,这里就涉及到了垃圾收集算法。

常用的垃圾收集算法有如下 4 种:

  • 标记-清除算法:见名知意,该算法包含两个阶段:标记 和 清除。标记过程主要就是可达性分析过程,首先找到内存中所有不可达对象位置,然后进行清除。
    标记-清除算法的好处是简单直接,缺点是效率不高(标记和清除两个阶段的效率都不高),并且会产生大量不连续的内存碎片。内存碎片过多可能会导致无法分配大对象而导致的频繁的触发垃圾收集动作。

  • 复制算法:该算法将内存分配为大小相等两块,每次只使用其中一块进行分配对象。当该块内存用完时,将该块内存上面还存活的对象复制到另一块内存上,然后清除该块内存,如此往复操作。
    复制算法 的优点是不存在内存碎片问题,缺点是内存利用率不高,每次只能使用一般的内存空间。
    实际使用中, 无需将内存对半分割,可以自定义更恰当的比例进行分割。比如,对于 HotSpot 虚拟机,其将年轻代划分为 8:1:1 的一个较大的 Eden 空间和两个较小的 Survivor 空间。每次分配对象时,使用 Eden 空间和其中一块 Survivor 空间,当回收时,将 Eden 空间和其中那块 Survivor 空间的存活对象复制到另一块 Survivor 空间中,最后清理掉 Eden 空间和那块 Survivor 空间。当复制对象时,Survivor 空间如果不够用,则多余的对象会通过分配担保机制直接进入老年代。
    复制算法 的缺点是当对象存活率较高时,就要进行较多的复制操作,效率会变低。

  • 标记-整理算法:该算法标记过程与 标记-清除 算法标记过程一致,但标记后,不进行清除,而是将存活对象都向一端移动,然后直接清理掉端边界以外的内存。
    标记-整理算法 的有点是不会产生内存碎片。

  • 分代收集算法:当前商业虚拟机的垃圾收集都采用 分代收集。该算法只是根据对象存活周期的不同将内存划分为几块,每块采用不同的垃圾收集算法。
    比如,对于 Java 堆来说,一般将其分为 新生代老年代新生代 对象的特点是朝生夕灭,每次垃圾收集时都有大批对象死去,因此适合采用 复制算法,每次 GC 时,只需付出很少的复制操作即可完成垃圾回收;老年代 因为对象的存活率高,且没有额外空间对它进行分配担保,因此必须使用 标记-清理标记-整理 算法来进行回收。

到此,对于 Java 类从源码到 JVM 进程的加载及其实例创建使用过程涉及到的一些相关内容,就已分析完毕。

参考

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

推荐阅读更多精彩内容

  • 《深入理解Java虚拟机》笔记_第一遍 先取看完这本书(JVM)后必须掌握的部分。 第一部分 走近 Java 从传...
    xiaogmail阅读 5,087评论 1 34
  • 第二部分 自动内存管理机制 第二章 java内存异常与内存溢出异常 运行数据区域 程序计数器:当前线程所执行的字节...
    小明oh阅读 1,159评论 0 2
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,096评论 1 32
  • 感恩,冬天过去了,春天来了,阳光普照,让阳台的花开得更加艳丽。 感恩,今天休息,可以多睡一会儿,感恩,我的被给我温...
    愛月亮的魚兒爱阅读 156评论 0 1
  • 早上的阳光直射在水面上,让人不敢直视,怕是会灼伤了眼睛,放眼望去,流动的水面上波光粼粼,就像星星撒在了河面上,金光...
    蝴蝶王妃阅读 1,398评论 0 6