JAVA虚拟机(JVM)-- 万字解析

简介: JAVA虚拟机(JVM)-- 万字解析

什么是JVM

       一个程序肯定要跟硬件打交道,但硬件肯定没办法直接和程序对接,那这是如何实现的?程序员首先编写java文件(文件后缀是 .java),然后这个java文件被编译成二进制字节码文件(文件后缀是 .class)才能跟JVM打交道,然后JVM再去跟操作系统说我要让某个硬件去干某件事,最后操作系统再去调动硬件


       所以这个过程中,JVM所要完成的工作就是执行二进制字节码文件的指令,根据这个指令去调用操作系统


JVM导致java的跨平台性

       知道上面这个之后,我们只需要再知道一个点,就能理解什么是java跨平台性,java为什么能跨平台了。操作系统有很多个(windows、linux、mac...)不同的操作系统对于同一个动作(比如将数据写入硬盘)的指令有可能是不同的,而一个java文件只能编译出一个二进制字节码文件,不过对于不同的操作系统,java提供了相对应的JVM版本,而一个二进制字节码文件就可以跟不同的JVM版本进行对接,保证了一个二进制字节码文件能够在不同的操作系统中都能兼容


JVM运行时数据区

       JVM要执行二进制字节码文件,包含着一个很重要的步骤,就是为程序(这里的程序其实就是这个二进制字节码文件),开辟出一块内存空间供程序使用,那这块内存空间到底要存些什么?要怎么开辟呢?


这块空间的名字叫运行时数据区


它有如下几个模块

下面简单(追求的就是一个快速)说明每个模块的作用


方法区

       主要用于存储类信息、静态变量、静态方法等


       存储几乎所有的对象实例


方法区和堆是所有线程共享的,接下来这三个模块都是线程各自占有的


虚拟机栈

       每个方法被执行的时候,虚拟机栈都会创建一个栈帧,这个栈帧会存储这个方法的必要信息,当方法执行结束之后,这个栈帧就会出栈


程序计数器

       这个自己要先去简单了解并发,程序计数器简单讲就是要让处理器知道上次执行到了该线程的哪个地方


本地方法栈

      本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务(什么是本地方法我也不懂,我这句话都是摘的)。

对象实例在JVM中的存储

       对象实例是一个程序最重要的部分之一,没有哪个正儿八经的程序是没有对象实例的,上面说到,堆区中存储的就是对象实例,那具体是这么存储呢?


堆为对象分配空间的两种方式

       假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称 为“空闲列表”。所以选择哪种分配方式由Java堆是否规整决定


对象在内存中要存储那些信息

对象在堆内存中的存储信息可以划分为三个部分:对象头、实例数据和对齐填充


1、对象头部分包括两类信息。


第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分方称它为“Mark Word”


对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。


2、实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。


3、对象的第三部分是对齐填充,它没有特别的含义,仅仅起着占位符的作用。这是因为虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。


如何在堆中找到对象

       创建对象自然是为了后续使用该对象,Java程序会通过栈上的reference数据来操作堆上的具 体对象。而reference数据访问对象的方式是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:


如果使用句柄访问的话, Java 堆中将可能会划分出一块内存来作为句柄池, reference 中存储的就

是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息

如果使用直接指针访问的话, Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关

信息, reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销

JVM中的堆区分代

堆区分为新生代和老年代,形象化的理解:新生代主要存储刚刚创建出来的对象实例,如果新生代中的对象能经过多次回收(具体看下面讲的JVM垃圾回收机制)的洗礼,就让为它会比较经常被使用,从而进入老年代,顺便提一嘴,还有一个叫永久代的,其实就是方法区


JVM的垃圾回收机制(简称GC)

       JVM的垃圾回收机制非常强大,是JVM的一个很重要的功能,而且这也是跟对象实例息息相关的,上面我们讲到了对象实例是怎么储存的和使用的,那么如果对象实例不用了要怎么清除呢?


如何判断对象已经没用了

当JVM认为一个对像已经没用了,就会把这个对象判定为是垃圾,就会去回收它的空间,有两个方法判断一个对像是否已经没用了


1、引用计数法:记录指向该对象的引用数,当该数值为零时就将该对象判定为垃圾


这个方法实现简单,判定效率也高,不过它有个致命的问题,它无法解决相互对象之间相互循环引用的问题,看下面这个例子

此时对象1和对象2除了对方指向自己的引用外,没有其他的引用了,这个时候,无论是对象1还是对象2,我们认为都已经没用了,因为程序是找不到它俩的,但是引用计数法无法将它们判定为垃圾,因为它们的被引用数不是为零

public class Test {
  public Object object = null;
  public static void main(String[] args) {
    Test a = new Test();//对象1
    Test b = new Test();//对象2
    a.object = b;
    b.object = a;
    a = null;
    b = null;
  }
}

正是因为这个缺点,主流的java虚拟机都不会使用该判定方法


2、可达性分析:


选定一些满足特定条件的对象作为根对象(GC Roots),那些与跟对象存在直接或间接引用关系的就是有用的对象,而与根对象没有任何关联的对象,就是垃圾对象(如下图)


这是当今主流的判定机制

GC的分类

Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。


Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。


Full GC是清理整个堆空间,包括年轻代和老年代(Minor GC和Major GC一起执行就是Full GC)


GC和分代的关系

那么现在我们就知道了为什么要分代了:


对象实例一般会首先分配到新生代当中,当新生代当中的空间不够用的时候,就会触发Minor GC,这个时候就会有一些没用的对象实例被清除掉,而有些就会留下来,那些能够挺过一定次数Minor GC的对象,最后就会进入到老年代当中,如果老年代中的空间也不够用了,那么就会进行Major GC


回收算法

我们上面说到GC会对垃圾进行回收,那具体要这么回收呢?这个就是回收算法,目前有三种回收算法,分别是:标记-清除、标记-复制、标记-整理


标记-清除

看下面的示意图,这个代表堆中的某块空间(可以是年轻代或老年代),每个紫色方块就是一个对象,上面我们说,JVM的对象是否存活的判定方法是可达性分析,所有那些没被GC Root引用的就要给标记成垃圾对象,标记完后再统一进行回收,这会造成内存空间碎片化的问题,另外还有执行效率不稳定的问题

标记-复制

将堆区分为两块区域,先只在其中一块区域创建对象,垃圾回收的时候,先标记出那些不要被回收的对象,然后将其复制到另外一块区域中,然后清空原本那块区域,新生代使用的就是标记-复制算法,新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。这里存在一个问题,当存活的对象的总大小大于那块Survivor空间,那就会造成溢出,而那些溢出的对象,会直接进入老年代,这叫分配担保

标记-整理

标记-复制算法存在需要额外空间进行分配担保的问题,新生代有老年代做分配担保,那老年代没人做分配担保就没办法使用标记-整理算法,要使用标记-整理算法,同样是先进行标记,不过不马上进行回收,而是让所有的存活对象都向内存空间一端移动,然后直接清理掉边界以外的堆空间


标记-整理存在一个弊端,在整理的过程中,必须全程暂停用户应用程序,这个被形象地称为“Stop The World”,实际上只要对象的存储地址发生了改变,就会“Stop The World”,所以标记-复制算法也会“Stop The World”

垃圾收集器

上面说的回收算法是理论层面的,接下来讲这些理论的实现者--垃圾收集器


垃圾回收器大概可以分为三类:


串行

吞吐量优先

响应时间优先

串行


使用单线程回收,因此就适用于堆内存较小,CPU数量少的(因为多了也没用)个人电脑


吞吐量优先


使用多线程回收,适用于堆内存较大,CPU较多的场景(如果是CPU个数较少,比如单核CPU,会导致回收线程之间相互争抢CPU的时间片,导致线程上下文切换的时间浪费,效率反而会比串行的垃圾回收器低),适合工作在服务器上


吞吐量优先的目标是:在单位时间内,让Stop The  World(以下简称STW)的时间最短


响应时间优先


同样也是多线程回收,同样适用于堆内存较大,CPU较多的场景,同样适合工作在服务器上


响应时间优先的目标是:让单次的STW的时间最短


一个例子区分 吞吐量优先 和 响应时间优先:


在一个单位时间内,吞吐量优先追求 0.5 + 0.5 = 1,吞吐量不在乎 0.5 很大,只在乎 1 最小,而响应时间优先追求 0.3 + 0.3 + 0.3 +0.3 +0.3 =1.5 ,响应时间优先不在乎1.5很大,只在乎 0.3 最小


三种垃圾收集器的使用方法和工作流程


串行

开启串行垃圾回收器的VM参数是:-XX:+UseSerialGC = Serial + SerialOld


Serial 垃圾回收器工作在新生代,采用 标记-复制 算法


SerialOld 垃圾回收器工作在老年代,采用 标记-整理 算法


工作流程如下:

为什么需要STW?


       因为在垃圾回收的工程中,有些对象的地址是会发生改变的,如果垃圾回收的过程中用户线程还在工作,那用户线程就有可能找不到对象, 从而产生错误,因此在垃圾回收器进行回收的过程中,其他用户线程都要阻塞


吞吐量优先

开启吞吐量优先的垃圾回收器的参数是:-XX:+UseParallelGC ~ -XX:+UseParallelOldGC


ParallelGC 垃圾回收器工作在新生代中,采用 标记-复制 算法


ParallelOldGC 垃圾回收工作在老年代中,采用 标记-整理 算法


在 jdk 1.8 中默认使用的就是 ParallelGC,而这两个开关是一同开启的,也就是开启 ParallelGC会连同开启ParallelOldGC


Parallel 就是并行的意思,说明这些垃圾回收线程是并行执行的


工作流程如下:

通过参数 -XX:ParallelGCThreads=n  可以设置垃圾回收线程的数量


除此之外还有三个比较重要的参数:


-XX:+UseAdaptiveSizePolicy :采用自适应的大小调整策略,会动态的调整  Eden 和 Survivor 的比例,还会调整堆的大小、老年代的晋升阈值等

-XX:GCTimeRatio=ratio : 用于调整垃圾回收的时间在总工作时间中的占比,计算公式为 1/(1+ratio),如果达不到设置的时间占比,ParallelGC就会尝试调大堆的大小,因为堆的容量较大,GC的次数就会比较少,ratio的值默认为99,但是 1/100 的占比时间很难达到,因此我们正常将 ratio 设置为19

- XX:MaxGCPauseMillis=ms : 单次垃圾回收的最大时间,默认值200ms,于  -XX:GCTimeRatio 参数存在冲突,因为 -XX:GCTimeRatio 可能为了达到目标,而调大堆的大小,而且堆越大,单次垃圾回收的时间就越长

响应时间优先

开启响应时间优先的参数:-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld


ConcMarkSweepGC 垃圾回收器工作在老年代中,采用 标记-清除 算法


其中的 Conc 值 concurrent 并行的意思,因为该GC在进行垃圾回收的过程中, 某些时刻存在垃圾回收线程和用户线程并发的现象


ConcMarkSweepGC 可能会存在并发失败的问题,如果出现并发失败,那 ConcMarkSweepGC 就会退化成 SerialOldGC


ParNewGC 垃圾回收器工作在新生代中,采用 标记-复制 算法


工作流程如下:

从上面我们可以看到,只有 初始标记 和 重新标记 的阶段需要STW,而且初始标记的时间又非常短,因此能达到更好的响应时间


G1 垃圾回收器

G1(Garbage First)是 JDK 9 默认的垃圾回收器,同时注重吞吐量和低延迟,默认的暂停目标是 200 ms


适用于超大的堆内存,其工作原理是将堆划分为多个大小相等的 Region ,每个 Region 都可以单独作为 Eden、Survivor、老年代


G1 整体上使用 标记-整理 算法,两个 Region之间采用 复制 算法


如果在 JDK 1.8 中使用G1,需要设置VM参数:-XX:+UseG1GC


未完待续 ...  


目录
相关文章
|
7月前
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
241 0
|
2月前
|
安全 Oracle Java
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
198 0
JAVA高级开发必备·卓伊凡详细JDK、JRE、JVM与Java生态深度解析-形象比喻系统理解-优雅草卓伊凡
|
10月前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
1984 65
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
10月前
|
存储 Java 开发者
浅析JVM方法解析、创建和链接
上一篇文章《你知道Java类是如何被加载的吗?》分析了HotSpot是如何加载Java类的,本文再来分析下Hotspot又是如何解析、创建和链接类方法的。
488 132
|
7月前
|
存储 设计模式 Java
重学Java基础篇—ThreadLocal深度解析与最佳实践
ThreadLocal 是一种实现线程隔离的机制,为每个线程创建独立变量副本,适用于数据库连接管理、用户会话信息存储等场景。
224 5
|
7月前
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
270 5
|
7月前
|
机器学习/深度学习 人工智能 Java
Java机器学习实战:基于DJL框架的手写数字识别全解析
在人工智能蓬勃发展的今天,Python凭借丰富的生态库(如TensorFlow、PyTorch)成为AI开发的首选语言。但Java作为企业级应用的基石,其在生产环境部署、性能优化和工程化方面的优势不容忽视。DJL(Deep Java Library)的出现完美填补了Java在深度学习领域的空白,它提供了一套统一的API,允许开发者无缝对接主流深度学习框架,将AI模型高效部署到Java生态中。本文将通过手写数字识别的完整流程,深入解析DJL框架的核心机制与应用实践。
373 3
|
7月前
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
195 1
|
7月前
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
257 5
|
8月前
|
Java API 数据处理
深潜数据海洋:Java文件读写全面解析与实战指南
通过本文的详细解析与实战示例,您可以系统地掌握Java中各种文件读写操作,从基本的读写到高效的NIO操作,再到文件复制、移动和删除。希望这些内容能够帮助您在实际项目中处理文件数据,提高开发效率和代码质量。
178 4

热门文章

最新文章

推荐镜像

更多
  • DNS