JVM 学习 - 通俗易懂

JVM 的介绍

JVM 本质上就是一个进程,你在电脑运行了 JVM ,跟运行了一个微信,一个qq音乐,一个浏览器差不多。差别是我们不用学习浏览器内核,但我们要学习JVM内部结构和原理,这有助于开发java程序,如果你学会垃圾回收器工作原理,是不是能开发出更好的Java程序?

JVM 运行有一个进程id ,没有端口,端口一般指的是网络端口,当启动一个spring boot 项目时,我们通常会指定一个 servlet.port: 8080 , 这是 tomcat 的端口,跟 JVM 没关系。(servlet 仅仅是 java 中的接口/规范,具体实现是由 tomcat、 Jetty 等web服务器实现)

JVM 的作用是让你的 windows/linux 电脑有了一个 java 命令,如:java --versionjava HelloWorld ,用过这个 java HelloWorld命令的都知道,java 命令执行的是 .class 文件,编译命令是 javac HelloWorld.java 。 javac 不是JVM的一部分,它是 JDK 的编译器,一个独立进程,专门用来把 xxx.java 文件编译成字节码文件(.class)

我们下载的 JDK 包含的主要工具:

  • javac → Java 编译器,独立进程
  • java → Java 启动器,启动 JVM
  • javadoc → 生成文档
  • jar → 打包工具
  • jconsole, jvisualvm → 监控/调试工具
  • jstat 监控 JVM 各种运行时信息, 查看 GC 活动、堆内存使用、方法区信息等。
  • jmap 输出堆的详细信息,包括对象分布、堆配置、GC 活动,支持堆 dump

跟 JDK 类似的包还有一个 JRE, 这个都不陌生哈,简单理解 JRE 是 JDK 的部分,JRE 有 JVM, 但是没有编译器,生成文档这些工具,因为 JRE 一般用来执行 jar 包,我们在springboot开发环境中打好的jar包,jar包内已经是 .class 文件,不需要编译器,一个 JRE 即可运行,所以 linux 拉取的是 JRE。 tomcat 是内嵌在 spring boot 中,maven 打包时打成可执行jar (胖 jar) 就会把 tomcat 打进jar 包。

这样就好理解 java 是跨平台的语言,只需要一个 JVM 和 .class 文件就能运行java语言,那么在windows、linux、 mac 都能安装JVM, 源码编译成字节码文件,形式为 jar 包,JVM 就可以执行它。所以只要操作系统能跑JVM,那么就能执行 Java

讲完了对 JVM 和 java 的介绍,我们来看看 JVM 是怎么执行 .class 文件的,学习 JVM 内部原理。

JVM 内部结构

先看一张图,这张图很清晰的说明了把 JAVA 文件转成字节码文件,经历类加载,进入 JVM 内存,以及执行引擎和本地方法库的结构都能看的很清楚。

补一个中文的图:

注意:

  • 线程私有:程序计数器、虚拟机栈、本地方法区
  • 线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)

第一次看这图谁不一脸懵,除了 java -> class 流程外,其它一概不懂。

暂且理解成电脑里面启动了JVM进程,然后内存就分配一块空间给JVM 进程,在 JVM 内部把运行时数据区划分成大大小小多块逻辑内存,不知道干啥的,就是划分成这样一块一块的内存区域。然后一个一个学,这一块一块是干啥的。

或者可以这么粗略理解:

  1. 类加载器 → 把字节码加载到 方法区
  2. 运行时数据区(堆、栈、方法区等) → 存放执行所需的数据。
  3. 执行引擎 → 根据 PC 寄存器指示的字节码指令,
    • 从栈帧/堆取数据,
    • 解释/编译成机器码,
    • 交给 CPU 执行,
    • 更新 PC 寄存器。

先学一个类加载入入门。

JVM 类加载机制

首先先看类加载吧,JVM 是如何加载字节码文件?

我把类加载过程和JVM逻辑内存结构串起来讲。

执行五个步骤:

  • 加载: 根据类的全限定名(如 com.fency.HelloWorld)把 .class 文件读入内存,并在方法区(或叫做元空间 Metaspace)生成一个代表这个类的 Class 对象。
  • 验证:那 .class 已经加载进来方法区了,执行引擎开始检查 .class 文件是否合法、符合 JVM 规范,避免恶意或错误字节码破坏 JVM。包括文件格式验证、元数据验证、字节码验证、符号引用验证。
  • 准备:为类的 静态变量(static 字段,存放在方法区) 分配内存,并设置 默认初始值(零值)。例如:public static int a = 10; 在这一步只是把 a 分配内存,赋值为默认的 0,不会赋值 10
  • 解析: 把类中的 符号引用(字面量,以字符串形式表示的类、方法、字段名)转换为 直接引用(指向内存中具体对象的引用/地址)。 简单理解就是给字面量一个地址,比如 com.fency.HelloWorld 类,本质上是一个字面量(名字/标识),解析后就指向某个内存地址,就可以通过这个字面量操作内存了。方法区内部有一个常量池,符号引用存储在这里。
  • 初始化:执行类构造器 <clinit>() 方法,对静态变量赋初值、执行静态代码块。例如:上面 a = 10 就是在这一步赋值完成的。初始化可能会创建对象实例,对象实例存储在中。 执行类构造器 <clinit> 方法时使用虚拟机栈 + 程序计数器 。

简化总结就是:

          读取.class ---> [方法区: 存类元数据]
                                 |
                                 v
                          验证 (字节码合法性)
                                 |
                                 v
                      准备 (分配静态变量内存, 默认值)
                                 |
                                 v
                   解析 (常量池符号引用 -> 直接引用)
                                 |
                                 v
     初始化 (执行 <clinit>, 访问方法区+堆, 线程栈, PC)

类加载学完就进入到 JVM 运行时数据区了,一个一个学习,先来一个 程序计数器,这东西和 cpu 好像有点关系,先看看。

程序计数器

全程叫做程序计数寄存器(Program Counter Register) , 有没有想到 cpu 的寄存器? 没错,名字就来源于 cpu ,它们有点关联,并非一模一样。

JVM 计数器是在 JVM 程序 运行时数据区的一小块内存(非常小,可以忽略不计),记录下一条要执行的执行的字节码指令地址

JVM 是多线程的,每个线程都有私有且独立的程序计数器,要能记住“执行到哪条字节码了”,当程序挂起线程时,计数器保存当前的工作状态, 切换回来时,从 PC 寄存器指向的字节码继续执行。

顺便回顾 cpu 寄存器存储指令相关的线程信息,cpu 只有把数据装载到寄存器才能运行。

cpu 有一套寄存器,分别是:

  • 程序计数器(PC Register)
    存储 下一条要执行指令的地址。JVM 很像cpu, 不同的是 CPU 一个核心仅有一个计数器,当切换线程工作时,需要整套寄存器都刷入内存( 线程控制块(TCB/PCB) ),再切换,这种保存/恢复机制让 cpu 切换线程开销很大。而JVM 的计数器是线程私有,挂起线程无非就是不执行,寄存器保持不变即可,或者可以理解成 JVM 本身就是内存的一块区域,计数器本身存储在内存中。
  • 指令寄存器(IR, Instruction Register)
    存放 当前正在执行的指令
  • 通用寄存器(General-purpose Register)
    存放运算时的 操作数计算结果,例如 EAX, EBX, RAX
  • 等等 ...

CPU 寄存器就是处理器的 工作台,CPU 执行指令时几乎所有信息都要放到寄存器里。 当cpu切换线程时,系统要做两件事:

  1. 保存 A 的上下文
    • 把线程 A 用到的寄存器内容(PC、通用寄存器、栈指针等)保存到内存(通常是内核栈或进程控制块 PCB)。
    • 这就像把 A 的工作台状态拍照存档。
  1. 恢复 B 的上下文
    • 从内存里取出线程 B 上次保存的寄存器状态,写回寄存器。
    • 相当于把 B 的工作台布置好,让 CPU 继续执行。

这就是为什么 线程切换开销大:寄存器内容要保存/恢复,可能还涉及缓存刷新。

顺便提一个问题,把 OS 的课串起来思考。

谁来决定 JVM 的线程切换? 或者说 JVM 内部有多个线程时,谁指挥它们运行/挂起?

答:操作系统的调度器!

JVM 调用了操作系统的底层 API , 操作系统接收到请求,调度器开始发力。

线程时间片都不陌生, 操作系统分配给线程一段任务执行时间(比如 1~10ms) ,如果是抢占式调度, 时间片用完,OS 强制保存线程上下文(寄存器、PC、栈指针等),换另一个线程运行。

这样电脑里的多个进程执行任务就不会等太久,不过你们女朋友不懂这个,以后就说自己的操作系统调度器的算法不合理,微信进程没有分配到时机片,导致消息回复晚了,这个解释高级的让她哑口无言!

综上所述,JVM 计数器的存在是因为 cpu 要不停的切换各个线程,切换会 JVM 的时候,JVM 有计数器,就知道从哪条指令继续执行。

JVM 的程序计数器是 线程私有的记录器,用来存放当前线程下一条要执行的字节码指令地址。 当操作系统调度导致线程挂起时,JVM 不需要额外保存/恢复计数器,因为它本来就是线程独占的。 这样保证了 JVM 多线程能够正确运行, 程序计数器的意义是保证任务执行的正确性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值