【01】全面理解JVM虚拟机

news2024/11/18 14:37:07

一、前言

  • 学习JVM是进行JVM调优的基础。写的代码部署到线上它会如何运行?要配多少内存?线上环境出问题了,服务崩溃了,应该怎么快速定位?这些问题都与JVM有着一定的关系。好的程序员都应该尽自己的能力把JVM每个底层逻辑整理成自己的知识体系。
  • 各种程序语言只能要写出满足JVM规范的class文件,都能够在JVM中运行;
  • java文件在JVM中执行的过程:
    JVM执行过程

二、Class文件规范

1、Class文件的结构

  • 实际上,Java官方只是定义了JVM的一种执行规范,也就是Class文件的组织规范。理论上,只要你能够写出一个符合这种规范的class文件,就能在JVM中执行,这也是JVM支持多语言的基础。
  • class文件本质上是一个二进制文件,可以使用文本工具打开,例如:UItraEdit工具。
  • 所有的class文件都必须以十六进制的CAFEBABE开头,这也是JVM规范的一部分。
  • JDK自己提供了一个javap指令可以直接来看一些class文件,例如:javap -v TestView.class
  • IDEA中可以添加一个ByteCodeView插件来直观的查看一个ClassFile的内容。

2、字节码指令的工作过程

  • 在 JVM 虚拟机中,会为每个线程构建一个线程私有的内存区域。其中包含的最重要的数据就是程序计数器和虚拟机栈。其中程序计数器主要是记录各个指令的执行进度,用于在 CPU 进行切换时可以还原计算结果。虚拟机栈中则包含了这个线程运行所需要的重要数据。
  • ​ 虚拟机栈是一个先进后出的栈结构,其中会为线程中每一个方法构建一个栈帧。而栈帧先进后出的特性也就对应了我们程序中每个方法的执行顺序。每个栈帧中包含四个部分,局部变量表,操作数栈,动态链接库、返回地址。
  • 操作数栈是一个先进后出的栈结构,主要负责存储计算过程中的中间变量。操作数栈中的每一个元素都可以是包括long型和double在内的任意 Java 数据类型。
  • 局部变量表可以认为是一个数组结构,主要负责存储计算结果。存放方法参数和方法内部定义的局部变量。以 Slot 为最小单位。
  • 动态链接库主要存储一些指向运行时常量池的方法引用。每个栈帧中都会包含一个指向运行时常量池中该栈帧所属方法的应用,持有这个引用是为了支持方法动态调用过程中的动态链接。
  • 返回地址存放调用当前方法的指令地址。一个方法有两种退出方式,一种是正常退出,一种是抛异常退出。如果方法正常退出,这个返回地址就记录下一条指令的地址。如果是抛出异常退出,返回地址就会通过异常表来确定。
  • 附加信息主要存放一些 HotSpot 虚拟机实现时需要填入的一些补充信息。这部分信息不在 JVM 规范要求之内,由各种虚拟机实现自行决定。

三、类加载机制

1、JDK8类加载体系

  • 有了class文件之后,则需要通过类加载模块才能将这些Class文件加载到JVM内存中。
  • 每个类加载器对加载过的类都会保存一个缓存;
  • 双亲委派机制,即:向上委托查找,向下委托加载;
  • 沙箱保护机制;

2、双亲委派机制

  • JDK8中的类加载器都继承于一个统一的抽象类ClassLoader,类的加载核心也在这个父类这。其中,加载类的核心方法如下:
protected Class<?> loadClass(String name,boolean resolve) throw ClassNotFoundException{
	 synchronized (getClassLoadingLock(name)) {
            // 每个类加载器对他加载过的类都有一个缓存,先去缓存中查看有没有加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {//没有加载过,就走双亲委派,找父类加载器进行加载。
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                   // 父类加载器没有加载过,就自行解析class文件加载。
                    c = findClass(name);
                  
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
           //这一段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。
           // 运行时加载类,默认是无法进行链接步骤的。
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
}

3、沙箱保护机制

  • 双亲委派机制最大的作用就是保护JDK内部的核心类不会被应用覆盖。而为了保护JDK内部的核心类,JAVA在双亲委派的基础上还加了一层保险。具体内容就是下面这个方法中:
private ProtectionDomain preDefineClass(String name,ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);
        // 不允许加载核心类
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }
        if (name != null) checkCerts(name, pd.getCodeSource());
        return pd;
    }

4、类和对象的关系

  • 首先:类 Class 在 JVM 中的作用其实就是一个创建对象的模板。也就是说他的作用更多的体现在创建对象的过程当中。而在程序具体执行的过程中,主要是围绕对象在进行,这时候类的作用就不大了。因此,在 JVM 中,类并不直接保存在最宝贵最核心的堆内存当中,而是挪到了堆内存以外的一部分内存中。这部分内存,在 JDK8 以前被成为永久带PermSpace,而在 JDK8 之后被改为了元空间 MetaSpace。
  • ​ 堆空间可以理解为JVM的客厅,所有重要的事情都在客厅处理。元空间可以理解为JVM的库房,东西扔进去基本上就很少管了。 这个元空间逻辑上可以认为是堆空间的一部分,但是他跟堆空间有不同的配置参数,不同的管理方式。因此也可以看成是单独的一块内存。这一块内存就相当于家里的工具间或者地下室,都是放一些用得比较少的东西。最主要就是类的一些相关信息,比如类的元数据、版本信息、注解信息、依赖关系等等。
  • 元空间可以通过-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize参数设置大小。但是大部分情况下,你是不需要管理元空间大小的,JVM 会动态进行分配。
  • ​ 另外,这个元空间也是会进行 GC 垃圾回收的。如果一个类不再使用了,JVM 就会将这个类信息从元空间中删除。但是,显然,对类的回收效率是很低的。只有一些自定义类加载器自行加载的一些类有被回收的可能,大部分情况下,类是不会被回收的。所以堆元空间的垃圾回收基本上是很少有效果的。大部分情况下,我们是不需要管元空间的。除非你的JVM 内存确实非常紧张,这时可以设定 -XX:MaxMetaspaceSize参数,严格控制元空间大小。​ 然后:在我们创建的每一个对象中,JVM也会保存对应的类信息。

四、执行引擎

在Class文件中,已经明确的定义清楚了程序的完整执行逻辑,而执行引擎就是讲这些字节指令转为机器指令去执行。

1、解释执行与编译执行

JVM 中有两种执行的方式:

  • 解释执行就相当于是同声传译。JVM 接收一条指令,就将这条指令翻译成机器指令执行。
  • 编译执行就相当于是提前翻译。编译执行也就是传说中的 JIT 。
    ​ - 大部分情况下,使用编译执行的方式显然比解释执行更快,减少了翻译机器指令的性能消耗。而我们常用的 HotSpot 虚拟机,最为核心的实现机制就是这个 HotSpot 热点。他会搜集用户代码中执行最频繁的热点代码,形成CodeCache,放到元空间中,后续再执行就不用编译,直接执行就可以了。
  • ​ 但是编译执行起始也有一个问题,那就是程序预热会比较慢。所以,现在JDK 默认采用的就是一种混合执行的方式。他会自己检测采用那种方式执行更快。

2、编译执行时的代码优化

  • 热点代码会触发 JIT 实时编译,而JIT 编译运用了一些经典的编译优化技术来实现代码的优化,可以智能地编译出运行时的最优性能代码。
  • HotSpot虚拟机中内置了两个(或三个)即时编译器,其中有两个编译器存在已久,分别被称为“客户端编译器”(Client Compiler)和“服务端编译器”(Server Compiler),或者简称为C1编译器和C2编译器(部分资料和JDK源码中C2也叫Opto编译器),第三个是在JDK 10时才出现的、长期目标是代替C2的Graal编译器。Graal编译器采用 Java 语言编写,因此生态的活力更强。并由此衍生出了 GraalVM 这样的支持实时编译的产品。也就是绕过 Class 文件,直接将 Java 代码编译成可在操作系统本地执行的应用程序。这也就是 AOT 技术Ahead Of Time。
  • ​ C1 会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度。启动快,占用内存小,执行效率没有server快。默认情况下不进行动态编译,适用于桌面应用程序。
  • C2 进行耗时较长的优化,以及激进优化,但优化的代码执行效率更高。启动慢,占用内存多,执行效率高,适用于服务器端应用。 默认情况下就是使用的 C2 编译器。并且,绝大部分情况下也不建议特意去使用 C1。

3、静态执行与动态执行

  • 静态执行指在Class文件编译的过程中就已经确定了执行方法。
  • 动态执行指需要在运行期间才能够确定调用哪个方法。例如:多个重载方法,需要根据传参来确定调用哪个方法。

五、GC垃圾回收器介绍

执行引擎会将class文件扔到JVM的内存中运行。在运行过程中,需要不断的在内存中创建并销毁对象。在传统C/C++语言中,销毁这些对象需要手动进行内存回收,防止内存泄漏,而在Java中则使用GC垃圾回收机制来自动管理内存的回收。

1、垃圾回收器

  • 这里推荐一个工具:阿里开源的 Arthas ,官网地址 。 这个工具功能非常强大,是对 Java进程进行性能调优的一个非常重要的工具,对于了解 JVM 底层帮助也非常大。
  • 一个 Java 进程会将他管理的内存分为heap堆区和nonheap非堆区两个部分。其中非堆区的几个核心部分像code_cache(热点指令缓存),metaspace(元空间),compressed_class_space(压缩类空间)我们之前都接触到了。这一部分就相当于 Java 进程中的地下室,属于不太活跃的部分。而中间heap堆区就相当于客厅了,属于Java 中最为核心的部分。而这其中,又大体分为了eden_space,survivor_space和old_gen三个大的部分,这就是 JVM 内存的主体。
    整体内存布局
  • 其中堆区是 JVM 核心的存放对象的内存区域。他的大小可以由参数 -Xms(初始堆内存大小),-Xmx(最大堆内存)参数指令。从这两个参数可以看到,堆内存是可以扩展的。如果初始内存不够,JVM 会扩大堆内存。但是如果内存扩展到了最大堆内存时还不够。这时就无法继续扩展了,而是会抛出 OOM 异常。这两个参数在生产环境中最好设置成一样,减少内存扩展时的性能消耗。​ 而GC垃圾回收器,就是要对这些内存空间进行及时回收,从而让这些内存可以重复利用。

2、分代收集模型

  • 不同GC对内存的管理和回收方式是不同的。而JDK8默认的垃圾回收器是Parallel Scavenge
  • JAVA做过统计,80%的对象都是“朝生夕死”。这些对象,被集中放在了一块比较小的内存空间当中,快速创建,快速回收,这块内存区域就是年轻代。在年轻代会非常频繁的进行垃圾回收,称为YoungGC。而年轻代又会被进一步划分为一个eden_space和两个survivor。这三个区域的大小比例默认是 8:1:1。
  • ​ 另外少部分需要长期使用的对象,被放到另一块竞争没有那么激烈的对象,则被放到另外一块比较大的内存空间当中,长期保持,这块内存就是老年代。在老年代,垃圾回收的频率则会相对比较低,只有空间不够时才进行,称为OldGC。
  • ​ 年轻代与老年代默认的大小比例是 1:2。
  • ​ 常见的分代收集模型中,对象会优先在eden区创建,经过一次YoungGC后,如果没有被回收,就会被移动到一个survivor区。接下来,下一次YoungGC时,又会被移动到另一块Survivor区。每移动一次,记录一个分代年龄。直到分代年龄太大了(默认是16),就会被移动到老年代。到老年代后,对象就不再记录分代年龄了,在老年代安安静静的用到退休。
  • 这就是JDK最有代表性的分代年龄收集机制。通过分代收集机制,JVM可以对不同的对象采取不同的回收策略,从而提高垃圾回收的效率。

3、JVM中有哪些垃圾回收器

java从诞生到现在最新的JDK21版本,总共产生了以下10个垃圾回收器:
垃圾收集器

  • 左侧的是分代算法。也就是将内存划分为年轻代和老年代进行管理。而有虚线的部分表示可以协同进行工作。JDK8默认就是使用的Parallel Scavenge和Parallel Old的组合。也就是在arthas的dashboard中看到的ps。
  • 右侧的是不分代算法。也就是不再将内存严格划分位年轻代和老年代。JDK9 开始默认使用 G1。而 ZGC是目前最先进的垃圾回收器。shennandoah则是OpenJDK 中引入的新一代垃圾回收器,与 ZGC 是竞品关系。Epsilon是一个测试用的垃圾回收器,根本不干活。

六、GC日志分析实例

GC可以说是决定了java程序运行效率的关键,所以我们需要学会定制GC参数以及分析GC日志

1、如何定制GC运行参数

  • 在现阶段,各种GC垃圾回收器都只适合一个特定的场景,因此,我们也需要根据业务场景,定制合理的GC运行参数。
  • 另外,JAVA程序在运行过程中要处理的问题是层出不穷的。项目运行期间会面临各种各样稀奇古怪的问题。比如 CPU 超高,FullGC 过于频繁,时不时的 OOM 异常等等。这些问题大部分情况下都只能凭经验进行深入分析,才能做出针对性的解决。
  • ​ 如何定制JVM运行参数呢?首先我们要知道有哪些参数可以供我们选择。
    • ​ 关于 JVM 的参数,JVM 提供了三类参数:
      • ​ 一类是标准参数,以-开头,所有 HotSpot 都支持。例如java -version。这类参数可以使用java -help 或者java -? 全部打印出来;
      • ​ 二类是非标准参数,以-X 开头,是特定 HotSpot版本支持的指令。例如java -Xms200M -Xmx200M。这类指令可以用java -X 全部打印出来。
      • ​ 第三类是不稳定参数,这也是 JVM调优的噩梦。以-XX 开头,这些参数是跟特定HotSpot版本对应的,很有可能换个版本就没有了。JDK8 中的以下几个指令可以帮助开发者了解 JDK8 中的这一类不稳定参数:
        java -XX:+PrintFlagsFinal:所有最终生效的不稳定指令。 java -XX:+PrintFlagsInitial:默认的不稳定指令 java -XX:+PrintCommandLineFlags:当前命令的不稳定指令 --这里可以看到是用的哪种GC。 JDK1.8默认用的ParallelGC

2、打印GC日志

  • 对 JVM 虚拟机来说,绝大多数的问题往往都跟堆内存的 GC 回收有关。因此下面几个跟 GC 相关的日志打印参数是必须了解的。这通常也是进行 JVM 调优的基础。
 -XX:+PrintGC: 打印GC信息 类似于-verbose:gc

​ -XX:+PrintGCDetails: 打印GC详细信息,这里主要是用来观察FGC的频率以及内存清理效率。

​ -XX:+PrintGCTimeStamps 配合 -XX:+PrintGC使用。在 GC 中打印时间戳。

​ -XX:PrintHeapAtGC: 打印GC前后的堆栈信息

​ -Xloggc:filename : GC日志打印文件。
  • 不同 JDK 版本会有不同的参数。 比如 JDK9 中,就不用分这么多参数了,可以统一使用-X-log:gc* 通配符打印所有的 GC 日志。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1692486.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记录github小程序短视频系统的搭建过程

GitHub - lkmc2/AwesomeVideoWxApp: 《倾心短视频》微信小程序 这个项目按readme中的来可以部署成功&#xff0c;但是会发现图片、视频全是空的&#xff0c;如下图&#xff1a; 修改源代码&#xff0c;更换图片上传与保存地址 大概涉及到这些代码块&#xff0c;进行更改即可。…

HarmonyOS interface router scale pageTransition SlideEffect.Left ArkTS ArkUI

&#x1f3ac;️create Component export default struct TitleBar {build(){Row(){Text(transition).fontSize(30fp).fontColor(Color.White)}.width(100%).height(8%).backgroundColor(#4169E1).padding({left:10})}}&#x1f39e;️interface export interface IList{ti…

【zotero6】ZotCard笔记模板分享

zotcard插件下载链接&#xff1a;传送门 因为zotero出了新的zotero7&#xff0c;现在下载插件会出现zotero6和zotero7不兼容的情况&#xff0c;通过这个链接可以区分适配不同版本的插件。 下载后点击工具的附加组件 然后选择通过文件添加 就可以添加插件了 再通过 工具->…

第十三期Big Demo Day亮点项目:CCarbon重塑碳交易生态,助力全球绿色发展

第十三期Big Demo Day活动即将于2024年5月28日在香港数码港的CyberArena隆重举行。我们荣幸地宣布&#xff0c;利用区块链技术优化全球碳交易CCarbon项目将亮相&#xff0c;参与精彩的项目路演。本次活动由ZeeprLabs、BiKing Exchange、Gather冠名赞助&#xff0c;Central Rese…

commvault学习(8):备份与恢复sql server

1.安装sql server2008r2 安装sql server 2.在客户端添加cv代理mssql server 如果此前的cv代理中没有sql server&#xff0c;那么可以手动再补充 点击setup 添加MSSQL Server 将程序添加到windows防火墙排除表 勾选自动探寻实例 3.备份sql server 3.1配置数据库内容 右击默…

HLS入门

一. HLS是什么&#xff1f;与VHDL/Verilog编程技术有什么关系? 高层次综合 (HLS) 抽象级别更高&#xff1a;HLS允许设计者在更高的抽象级别上工作&#xff0c;使用高级编程语言来描述硬件的功能。这种方法减少了设计者需要处理的底层细节&#xff0c;使得设计过程更加高效。…

下一代Docker会让部署更丝滑吗

下一代Docker会让部署更丝滑吗 如何通俗易懂的理解DockerDocker有什么缺点Docker与AI结合&#xff0c;会让部署更加丝滑吗 随着互联网技术的不断发展&#xff0c;单机系统已经无法满足日益正常的用户量以及正常处理用户请求&#xff0c;这个时候就需要进行多机部署&#xff0c;…

JDBC(Java DataBase Connectivity)Java数据库连接

JDBC(Java DataBase Connectivity) Java 语言连接数据库 再本模块中,java提供里一组用于连接数据库的类和接口Java 语言开发者,本身没有提供如何具体连接数据库的功能只是定义了一组java程序连接数据库的访问接口 连接到数据库向数据库发送增,修改,删除这一类的sql发送查询sq…

学习Thymeleaf时遇到的问题

使用idea创建web项目&#xff0c;启动服务器后无法访问页面 原因是tomcat 新版本引用包名改变 由javax变为jakarta 解决办法1 把项目的poe.xml文件由 改为 解决办法2 新建项目时选择新版本&#xff0c;但是新版本不支持thymeleaf

uniapp android使用uni.chooseLocation,app云打包后,定位地址列表一直在加载中

复现BUG 1、自己生成一个证书 参考生成证书流程 2、使用刚生成证书的SHA1 &#xff0c;重新创建一个高德key 高德开放平台地址 3、打包&#xff08;打包的包名要与高德申请key所填的包名一致&#xff09;

从零搭建python环境:深入解析虚拟环境与Python版本管理

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;为何需要虚拟环境&#xff1f; 二、虚拟环境的创建与命名 1. 虚拟环境…

Spark SQL【Java API】

前言 之前对 Spark SQL 的影响一直停留在 DSL 语法上面&#xff0c;感觉可以用 SQL 表达的&#xff0c;没有必要用 Java/Scala 去写&#xff0c;但是面试一段时间后&#xff0c;发现不少公司还是在用 SparkSQL 的&#xff0c;京东也在使用 Spark On Hive 而不是我以为的 Hive O…

FastSAM 部署 rknn

基于yolov8(ultralytics)工程导出的FastSAM的onnx模型&#xff0c;后处理和yolov8seg是一样的。      模型和完整测试代码。 1 FastSAM 导出 onnx 导出onnx的方式有两种&#xff0c;一种使用FastSAM工程&#xff0c;一种是使用yolov8(ultralytics)工程。本篇博客使用yolov…

Unity在Windows平台播放HEVC/H.265格式视频的底层原理

相关术语、概念 HEVC/H.265 HEVC&#xff08;High Efficiency Video Coding&#xff09;是一种视频压缩标准&#xff0c;也被称为H.265。它是一种高效的视频编码标准&#xff0c;可以提供比之前的标准&#xff08;如H.264&#xff09;更高的压缩率&#xff0c;同时保持较高的…

【LabVIEW FPGA入门】使用事件发生函数同步FPGA循环

1.使用事件发生函数 使用 Occurrences 函数来控制单独的同步活动。特别是&#xff0c;当您希望程序框图的一部分等待程序框图的另一部分完成任务而不强制 LabVIEW 进行轮询时&#xff0c;请使用这些函数。 您可以使用全局变量执行类似于occurrences函数的功能&#xff0c;通过一…

QT教程-一,初识QT

目录 一,QT是什么&#xff1f;能够使用它做什么&#xff1f; 二&#xff0c;Qt 能够使用的语言 三&#xff0c;Qt主要用于什么领域&#xff1f; 四&#xff0c;Qt开发的软件 一,QT是什么&#xff1f;能够使用它做什么&#xff1f; Qt是一个跨平台的 C 开发库&#xff0c;主…

二十六篇:嵌入式系统行业全景:应用、趋势与市场洞察

嵌入式系统行业全景&#xff1a;应用、趋势与市场洞察 1. 行业应用案例分析 1.1 汽车电子 引言 在飞速发展的科技领域&#xff0c;汽车电子正逐步成为现代汽车设计中的核心。嵌入式系统在这一进程中充当了枢纽的角色&#xff0c;不仅推进了车辆性能的极限&#xff0c;还不断…

WordPress国外超人气主题Vikinger汉化版

WordPress国外超人气主题Vikinger汉化版 前言效果图安装教程领取主题下期更新预报 前言 我们在上一个教程已经学过如何安装WordPress&#xff0c;所以现在不用多说。 效果图 安装教程 下载后先本地解压&#xff0c;找到vikinger.zip文件&#xff0c;上传安装并启用主题。 访…

【Docker】Linux 系统(CentOS 7)安装 Docker

文章目录 对 VMware 软件的建议官方说明文档Docker安装卸载旧版本docker设置仓库开始安装 docker 引擎最新版 Docker 安装指定版本 Docker 安装&#xff08;特殊需求使用&#xff09; 启动 Docker查看 Docker 版本查看 Docker 镜像设置 Docker 开机自启动 验证开机启动是否生效…

Boosting Cache Performance by Access Time Measurements——论文泛读

TOC 2023 Paper 论文阅读笔记整理 问题 大多数现代系统利用缓存来减少平均数据访问时间并优化其性能。当缓存未命中的访问时间不同时&#xff0c;最大化缓存命中率与最小化平均访问时间不同。例如&#xff1a;系统使用多种不同存储介质时&#xff0c;不同存储介质访问时间不同…