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

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 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


未完待续 ...  


目录
相关文章
|
6天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
54 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
13天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
11天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
23天前
|
存储 Java 开发者
浅析JVM方法解析、创建和链接
上一篇文章《你知道Java类是如何被加载的吗?》分析了HotSpot是如何加载Java类的,本文再来分析下Hotspot又是如何解析、创建和链接类方法的。
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
1月前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
12天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
12天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析

推荐镜像

更多