首页
首页 教程 派聪明
  • 首页
  • 教程
  • 派聪明
  • 登录
登录技术派畅享更多权益

用户名密码登录

其他登录:
icon_GitHubCreated with sketchtool.
绑定星球,畅享VIP服务

微信扫码/长按识别登录

输入验证码
有效期五分钟 👉 手动刷新

登录即同意 用户协议 和 隐私政策

绑定二哥编程星球,畅享 VIP 尊享服务!

戳我了解如何获取星球编号,新窗口打开

添加二哥微信 itwanger 审核更快

记得备注 星球编号
我会根据星球编号进行审核
1
大白话带你认识JVM
更新时间: 2024年01月16日
星球
2
JVM是如何运行Java代码的?
更新时间: 2024年01月16日
星球
3
Java的类加载机制
更新时间: 2024年01月16日
星球
4
Java的类文件结构
更新时间: 2024年01月16日
星球
5
从javap的角度轻松看懂字节码
更新时间: 2024年01月16日
星球
6
栈虚拟机与寄存器虚拟机
更新时间: 2024年01月16日
星球
7
字节码指令详解
更新时间: 2024年01月16日
星球
8
深入理解JVM的栈帧结构
更新时间: 2024年01月16日
星球
9
深入理解JVM的运行时数据区
更新时间: 2024年01月16日
星球
10
深入理解JVM的垃圾回收机制
更新时间: 2024年01月16日
星球
11
深入理解 JVM 的垃圾收集器
更新时间: 2024年01月16日
星球
12
Java 创建的对象到底放在哪?
更新时间: 2024年01月16日
星球
13
深入理解JIT(即时编译)
更新时间: 2024年01月16日
星球
14
JVM 性能监控之命令行篇
更新时间: 2024年01月16日
星球
15
JVM 性能监控之可视化篇
更新时间: 2024年01月16日
星球
16
阿里开源的 Java 诊断神器 Arthas
更新时间: 2024年01月16日
星球
17
内存溢出排查优化实战
更新时间: 2024年01月16日
星球
18
CPU 100% 排查优化实践
更新时间: 2024年01月16日
星球
19
JVM 核心知识点总结
更新时间: 2024年01月16日
星球
20
K个一组翻转链表
更新时间: 2024年02月07日
星球
关注公众号
原创
第十九节:JVM 核心知识点总结

第十九节:JVM 核心知识点总结

一、基本概念

1.1 OpenJDK

自 1996 年 JDK 1.0 发布以来,Sun 公司在大版本上发行了 JDK 1.1、JDK 1.2、JDK 1.3、JDK 1.4、JDK 5,JDK 6 ,这些版本的 JDK 都可以统称为 SunJDK 。

之后在 2006 年的 JavaOne 大会上,Sun 公司宣布将 Java 开源,在随后的一年多里,它陆续将 JDK 的各个部分在 GPL v2(GNU General Public License,version 2)协议下开源,并建立了 OpenJDK 组织来对这些代码进行独立的管理,这就是 OpenJDK 的来源,此时的 OpenJDK 拥有当时 sunJDK 7 的几乎全部代码。

1.2 OracleJDK

在 JDK 7 的开发期间,由于各种原因的影响,Sun 公司市值一路下跌,已无力推进 JDK 7 的开发,于是 JDK 7 的发布一直被推迟。

之后在 2009 年 Sun 公司被 Oracle 公司收购,为解决 JDK 7 长期跳票的问题,Oracle 将 JDK 7 中大部分未能完成的项目推迟到 JDK 8 ,并于 2011 年发布了JDK 7,在这之后由 Oracle 公司正常发行的 JDK 版本就由 SunJDK 改称为 Oracle JDK。

在 2017 年 JDK 9 发布后,Oracle 公司宣布:以后 JDK 将会在每年的 3 月和 9 月各发布一个大版本,即半年发行一个大版本,目的是为了避免众多功能被捆绑到一个 JDK 版本上而引发的无法交付的风险。

在 JDK 11 发布后,Oracle 同步调整了 JDK 的商业授权,宣布从 JDK 11 起,将以前的商业特性全部开源给 OpenJDK ,这样 OpenJDK 11 和 OracleJDK 11 的代码和功能,在本质上就完全相同了。

同时还宣布以后会发行两个版本的 JDK :

  • 一个是在 GPLv2 + CE 协议下由 Oracle 开源的 OpenJDK;
  • 一个是在 OTN 协议下正常发行的 OracleJDK。

两者共享大部分源码,在功能上几乎一致。唯一的区别是 Oracle OpenJDK 可以在开发、测试或者生产环境中使用,但只有半年的更新支持;而 OracleJDK 对个人免费,但在生产环境中商用收费,可以有三年时间的更新支持。

目前最新的长期支持的 JDK 是 JDK 21(LTS),详情可以参考朋友 why 技术的帖子。

1.3 HotSpot VM

它是 Sun/Oracle JDK 和 OpenJDK 中默认的虚拟机,也是目前使用最为广泛的虚拟机。

最初由 Longview Technologies 公司设计发明,该公司在 1997 年被 Sun 公司收购,随后 Sun 公司在 2006 年开源 SunJDK 时也将 HotSpot 虚拟机一并进行了开源。

Oracle 收购 Sun 以后,建立了 HotRockit 项目,并将其收购的另外一家公司(BEA)的 JRockit 虚拟机中的优秀特性集成到 HotSpot 中。

HotSpot 在这个过程里移除掉永久代,并吸收了 JRockit 的 Java Mission Control 监控工具等功能。

到 JDK 8 发行时,采用的就是集两者之长的 HotSpot VM。

我们可以在自己的电脑上使用 java -version 来获得 JDK 的信息:

二、Java 内存区域

Java 内存区域我们之前讲过,这里再盘一盘。

2.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。

字节码解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要该计数器来完成。

每个线程都拥有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储。

2.2 虚拟机栈

虚拟机栈(Java Virtual Machine Stack)也是线程私有,它描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

方法从调用到结束就对应着一个栈帧从入栈到出栈的过程。在《Java 虚拟机规范》中,对该内存区域规定了两类异常:

  • 如果线程请求的栈深度大于虚拟机所允许的栈深度,将抛出 StackOverflowError 异常;
  • 如果 Java 虚拟机栈的容量允许动态扩展,当栈扩展时如果无法申请到足够的内存会抛出 OutOfMemoryError 异常。

2.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈类似,其区别在于:Java 虚拟机栈是为虚拟机执行 Java 方法(也就是字节码)服务的,而本地方法栈则是为 JVM 使用到的本地(Native)方法服务。

2.4 堆

堆(Java Heap)是虚拟机所管理的最大一块内存空间,它被所有线程所共享,用于存放对象实例。

Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为是连续的。Java 堆可以被实现成固定大小的,也可以是可扩展的。

当前大多数主流的虚拟机都是按照可扩展来实现的,即可以通过最大值参数 -Xmx 和最小值参数 -Xms 进行设定。

如果 Java 堆中没有足够的内存来完成对象实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

2.5 方法区

方法区(Method Area)也是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、JIT 编译后的代码缓存等数据。

方法区也被称为 “非堆”,目的是与 Java 堆进行区分。《Java 虚拟机规范》规定,如果方法区无法满足新的内存分配需求时,将会抛出 OutOfMemoryError 异常。

JDK 8 以后的方法区实现已经不再是永久代(Permanent Generation)了,而是使用元空间(Metaspace)来实现。

运行时常量池(Runtime Constant Pool)也是方法区的一部分,用于存放常量池表(Constant Pool Table),常量池表中存放了编译期生成的各种符号字面量和符号引用。

JDK 8 以后的运行时常量池在元空间中。

三、对象

3.1 对象的创建

当我们在代码中使用 new 关键字创建一个对象时,其在 JVM 中需要经过以下步骤:

1. 类加载过程

当虚拟机遇到一条字节码指令 new 时,首先将去检查这个指令的参数是否能在常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程。

2. 分配内存

在类加载检查通过后,虚拟机需要给新生对象分配内存空间。根据 Java 堆是否规整,可以有以下两种分配方案:

①、指针碰撞:假设 Java 堆中内存是绝对规整的,所有使用的内存放在一边,所有未被使用的内存放在另外一边,中间以指针作为分界点指示器。

此时内存分配只是将指针向空闲方向偏移出对象大小的空间即可,这种方式被称为指针碰撞。

②、空闲列表:如果 Java 堆不是规整的,此时虚拟机需要维护一个列表,记录哪些内存块是可用的,哪些是不可用的。在进行内存分配时,只需要从该列表中选取出一块足够的内存空间划分给对象实例即可。

注:Java 堆是否规整取决于其采用的垃圾收集器是否带有空间压缩整理能力,前面讲过了。

除了分配方式外,由于对象创建在虚拟机中是一个非常频繁的行为,此时需要保证在并发环境下的线程安全:如果一个线程给对象 A 分配了内存空间,但指针还没来得及修改,此时就可能出现另外一个线程使用原来的指针来给对象 B 分配内存空间的情况。

想要解决这个问题有两个方案:

①、方式一:采用同步锁定,或采用 CAS 配上失败重试的方式来保证更新操作的原子性。

②、方式二:为每个线程在 Java 堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。

线程在进行内存分配时优先使用本地缓冲,当本地缓冲使用完成后,再向 Java 堆申请分配,此时 Java 堆采用同步锁定的方式来保证分配行为的线程安全。

3. 对象头设置

将对象有关的元数据信息、对象的哈希码、分代年龄等信息存储到对象头中。

可以和 JIT 那节的内容关联起来。

4. 对象初始化

调用对象的构造方法,即 Class 文件中的 <init>() 来初始化对象,为相关字段赋值。

3.2 对象的内存布局

在 HotSpot 中,对象在堆内存中的存储布局可以划分为以下三个部分:

1. 对象头 (Header)

对象头包括两部分信息:

  • Mark Word:对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,官方统称为 Mark Word,我们曾在 synchronized 的四种锁状态讲过。
  • 类型指针:对象指向它类型元数据的指针,Java 虚拟机通过这个指针来确定该对象是哪个类的实例。需要说明的是,并非所有的虚拟机都必须要在对象数据上保留类型指针,这取决于对象的访问定位方式。

2. 实例数据 (Instance Data)

即我们在代码中定义的各种类型的字段,无论是从父类继承而来,还是子类中定义的都需要记录。

3. 对齐填充 (Padding)

主要起占位符的作用。HotSpot 要求对象起始地址必须是 8 字节的整倍数,即间接要求了任何对象的大小都必须是 8 字节的整倍数。对象头部分在设计上就是 8 字节的整倍数,如果对象的实例数据不是 8 字节的整倍数,则由对齐填充进行补全。

3.3 对象的访问定位

对象创建后,Java 程序就可以通过栈上的 reference(也就是引用)来操作堆上的具体对象。

《Java 虚拟机规范》规定 reference 是一个指向对象的引用,但并未规定其具体实现方式。主流的方式方式有以下两种:

  • 句柄访问:Java 堆将划分出一块内存来作为句柄池, reference 中存储的是对象的句柄地址,而句柄则包含了对象实例数据和类型数据的地址信息。
  • 指针访问:reference 中存储的直接就是对象地址,而对象的类型数据则由上文介绍的对象头中的类型指针来指定。

通过句柄访问对象:

通过直接指针访问对象:

句柄访问的优点在于对象移动时(垃圾收集时移动对象是非常普遍的行为)只需要改变句柄中实例数据的指针,而 reference 本身并不需要修改;

指针访问则反之,由于其 reference 中存储的直接就是对象地址,所以当对象移动时, reference 需要被修改。但针对只需要访问对象本身的场景,指针访问则可以减少一次定位开销。由于对象访问是一项非常频繁的操作,所以这类减少的效果会非常显著,基于这个原因,HotSpot 主要使用的是指针访问的方式。

四、垃圾收集机制

在 JVM 内存模型中,程序计数器、虚拟机栈、本地方法栈这 3 个区域都是线程私有的,会随着线程的结束而销毁,因此在这 3 个区域当中,无需过多考虑垃圾回收问题。垃圾回收问题主要发生在 Java 堆上。

在 Java 堆上,垃圾回收的主要内容是死亡的对象(不可能再被任何途径使用的对象)。

判断对象是否死亡有以下两种方法:

4.1 引用计数法

在对象中添加一个引用计数器,对象每次被引用时,该计数器加一;当引用失效时,计数器的值减一;只要计数器的值为零,则代表对应的对象不可能再被使用。该方法的缺点在于无法避免相互引用的问题:

objA.instance = objB
objB.instance = objA    
objA = null;
objB = null;
System.gc();

如上所示,此时两个对象已经不能再被访问,但其互相持有对对方的引用,如果采用引用计数法,则两个对象都无法被回收。

4.2 可达性分析

但上面的代码在大多数虚拟机中都能被正确的回收,因为大多数主流的虚拟机都是采用的可达性分析方法来判断对象是否死亡。

可达性分析是通过一系列被称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径被称为引用链(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,这代表 GC Roots 到该对象不可达, 此时证明该对象不可能再被使用。

在 Java 语言中,固定可作为 GC Roots 的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
  • 在方法区(元空间)中类静态变量引用的对象,譬如 Java 类中引用类型的静态变量;
  • 在方法区(元空间)中常量引用的对象,譬如字符串常量池(String Table)里的引用;
  • 在本地方法栈中的 JNI(Native 方法)引用的对象;
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(如 NullPointException,OutOfMemoryError 等)及系统类加载器;
  • 所有被同步锁(synchronized 关键字)持有的对象;

除了这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域的不同,还可能会有其他对象 “临时性” 地加入,共同构成完整的 GC Roots 集合。

4.3 对象引用

可达性分析是基于引用链进行判断的,在 JDK 1.2 之后,Java 将引用关系分为以下四类:

强引用 (Strongly Reference)

最传统的引用,如 Object obj = new Object() 。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用

已加入二哥编程星球,即刻绑定星球编号解锁🔐

该文档仅「二哥编程星球」的VIP用户可见

二哥的编程星球内容包括:

1. 付费文档: 技术派、MYDB 等项目配套的 120+篇教程查看权限

2. 面试指南: 校招、社招的 40 万+字面试求职攻略

3. 智能助手: 无限期使用派聪明 AI 助手,已对接讯飞星火和 ChatGPT双通道,不用花 1 分钱

4. 专属问答: 向二哥 1v1 发起提问,内容不限于 offer 选择、学习路线、职业规划等

5. 简历修改: 提供简历修改服务,附赠星球 100+优质简历模板可供参考

6. 学习环境: 打造一个沉浸式的学习环境,有一种高考冲刺、大学考研的氛围


二哥的星球

》步骤①:微信扫描上方二维码,点击「加入知识星球」按钮

》步骤②:访问星球置顶帖球友必看: https://t.zsxq.com/11rEo9Pdu,获取项目配套文档的语雀访问地址和密码

已加入星球,绑定星球编号
删除提醒

确定删除《第十九节:JVM 核心知识点总结》吗

1人已点赞

回复