JVM -- 跨语言的平台,位置与结构,指令集架构,生命周期,发展历程,内存结构,类加载子系统

本文深入探讨了Java虚拟机(JVM)的工作原理,包括其内存结构、类加载子系统、垃圾收集机制以及JVM的发展历程。重点阐述了类加载的加载、链接和初始化阶段,双亲委派模型确保了核心类库的安全加载。同时,文章介绍了HotSpot VM的即时编译策略以及多语言混合编程的可能性。此外,还提到了自定义类加载器的用途和实现,强调了类加载器在保障系统安全和防止源码泄露方面的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,这其实是一种本末倒置的“病态”。
如果我们把核心类库的 APT 比做数学公式的话,那么、Java 虚拟机的知识就好比公式的推导过程。

架构师每天都在思考什么?

  • 应该如何让我的系统更快?
  • 如何避免系统出现瓶颈?

垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率,但.是,垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高扩展性应用和诊断运行时问题的基础,也是Java :工程师进阶的必备能力。

Java跨平台的语言

JVM跨语言的平台

Java,虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,它只关心“字节码”文件。也就是说Java虚拟机拥有语言无关性,并不会单纯地与Java语言“终身绑定”,只要其他编程语言的编译结果满足并包含Java虚拟机的内部指令集、符号表以及其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行。.

字节码

  1. 我们平时说的java字节码,指的是用java语言编译成的字节码。准确的说任何能在jvm平台上执行的字节码格式都是一样的。所以应该统称为:jvm字节码
  2. 不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的JVM上运行。
  3. Java虚拟机与Java 语言并没有必然的联系,它只与特定的二进制文件格式一Class 文件格式所关联,Class文件中包含了Java 虚拟机指令集(或者称为字节码、Bytecodes)和符号表,还有一些其他辅助信息。

多语言混合编程

Java平台.上的多语言混合编程正成为主流,通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。

对这些运行于Java虚拟机之.上、Java之外的语言,来自系统级的、底层的支持正在迅速增强,以JSR-292 为核心的一系列项目和功能改进(如DaVinci Machine项 目、Nashorn引擎、InvokeDynamic指 令、java. lang. invoke包等),推动Java虚拟机从“Java语言的虚拟机”向“多语言虚拟机”的方向发展

虚拟机分为系统虚拟机和软件虚拟机。

JVM 

●作用.
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里
●特点
  ➢一次编译,到处运行
  ➢自动内存管理
  ➢自动垃圾回收功能

JVM位置与结构

JVM是运行在操作系统之上的,它与硬件没有直接的交互。

jvm整体结构(概图)这张图要求会画

HotSpot VM是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和c/C++程序一较高下的地步。

  • 类装载子系统:将字节码文件加载到内存中,生成一个大的Class对象。
  • 方法区和堆颜色一致:在内存中多个线程共享堆和方法区。其它三个浅灰色的表示每个线程独有一份。
  • 执行引擎:操作系统只能识别机器指令,但是字节码指令不等同于机器指令。执行引擎就相当于其间翻译的角色。

JVM架构分为两种:基于栈的指令集架构和基于寄存器的指令集架构。

  • 基于栈的指令集架构:跨平台性、指令集小、指令多;执行性能比寄存器差,适合在资源受限平台上(机顶盒……)
  • 基于寄存器指令集架构:和硬件耦合,指令集大,指令少,适合在非资源受限平台上

时至今日,尽管嵌入式平台已经不是Java程序的主流运行平台了(准确来说应该是HotSpotVM的宿主环境已经不局限于嵌入式平台了),那么为什么不将架构更换为基于寄存器的架构呢?
答:基于栈的指令集架构在设计和实现上要简单一些,其次基于栈的指令集架构在非资源受限平台上也是完全可以用的。

JVM生命周期

虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。


虚拟机的执行
·一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。·程序开始执行时他才运行,程序结束时他就停止。执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。

 

虚拟机的退出
有如下的几种情况:·

  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止·由于操作系统出现错误而导致Java虚拟机进程终止
  • 某线程调用Runtime类或system类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作。
  • ·除此之外,JNI ( Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。

JVM发展历程

SUN Classic VM

早在1996年,世界上第一款商用Java虚拟机。这款虚拟机内部只提供解释器。如果使用JIT编译器,就需要进行外挂。但是一旦使用了JIT编译器,JIT就会接管虚拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作。

Exact  VM

具备现代高性能虚拟机的维形,1.热点探测   2. 编译器与解释器混合工作模式


HotSpot VM (重点)

目前Hotspot占有绝对的市场地位,称霸武林。

  1. 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是HotSpot
  2. sun/oracle JDK和l openJDK的默认虚拟机。
  3. 因此木课程中默认介绍的虚拟机都是HotSpot,相关机制也主要是指Hotspot的Gc机制。(比如其他两个商用虚拟机都没有方法区的概念)
  4. 从服务器、桌面到移动端、嵌入式都有应用。

名称中的HotSpot指的就是它的热点代码探测技术

  1. 通过计数器找到最具编译价值代码(热点代码),触发即时编译或栈上替换
  2. 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡


JRockit  VM

专注于服务器端应用
它可以不太关注程序启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行。


大量的行业基准测试显示,JRockit JVM是世界上最快的JVM
使用JFockit产品,客户已经体验到了显著的性能提高(一些超过了70% )和硬件成本的减少(达50%)。


优势:全面的Java运行时解决方案组合

  • JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒级的JVM响应时间,适合财务、军事指挥、电信网络的需要
  • MissionControl服务套件,它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。

2008年,BEA(JRockit的创造者)被Oracle收购。
oracle表达了整合两大优秀虚拟机的工作,大致在JDK 8中完成。整合的方式是在HotSpot的基础上,移植JRockit的优秀特性。
高斯林:目前就职于谷歌,研究人工智能和水下机器人

IBM J9 VM
市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM广泛用于IBM的各种Java产品。
也号称是世界上最快的Java虚拟机(仅仅只是在IBM自家的产品而言)。2017年左右,IBM发布了开源J9 VM,命名为openJ9,交给Eclipse基金会管理,也称为Eclipse openJ9
 

Graal VM

2018年4月,oracle Labs公开了Graal VM,号称"Run Programs Faster Anywhere",勃勃野心。

  • Graal VM在HotSpot VM基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用。语言包括: Java、scala、Groovy、Kotlin; C、C++、Javascript、Ruby、Python、R等
  • 支持不同语言中混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件
  • 工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中间表示。Graal VM提供Truffle工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化,获得比原生编译器更优秀的执行效率。
  • 如果说Hotspot有一天真的被取代,Graal VM希望最大。但是Java的软件生态没有丝毫变化(上层程序员感受不到差异)。
     

Java代码执行流程

java编译器这里不做重点,但如果想要开发一门新的语言,那么只要让这门语言满足编译器能够生成字节码文件即可。

重点在翻译字节码和JIT编译器组成的执行引擎。

类加载子系统

内存结构

前面简图的详细图  (默认hotSpot虚拟机)


课程纲目


类加载子系统的作用

类加载子系统分为三个主要阶段:加载——链接——初始化

  • 类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定。
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

加载阶段

加载:
  1.通过一个类的全限定名获取定义此类的二进制字节流 (从物理磁盘上获取或者网络压缩包等等)
  2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3.在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口

链接阶段

解析:因为一个类在解析的时候会加载非常多的类,那么多类不可能直接放在字节码文件当中,这个时候就需要使用符号引用来引用相关的结构

初始化阶段

  • 初始化阶段就是执行类构造器方法<clinit>()的过程。
  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • <clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>( ) )
  • 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()己经执行完毕。
  • 虚拟机必须保证一个类的<clinit> ()方法在多线程下被同步加锁。(一个类往内存中只需要加载一次即可,假设有两个线程想加载它,那么就会出现多个线程对同一份资源进行操作的现象)
     
static{
        number = 20;
        System.out.println(number);   // 报错,非法前向引用,后面才声明,这里不能用,只能赋值。
    }

    private static int number = 10;

    public static void main(String arg[]) {

        System.out.println(MD5Demo.number);  // 10 申明语句覆盖了赋值语句。

    }

类加载器的分类

  • JVM支持两种类型的类加载器,分别为引导类加载器(BootstrapClassLoader)自定义类加载器(User-Defined classLoader)
  • 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
  • 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:

这里的四者之间的关系是包含关系。不是上层下层,也不是子父类的继承关系。
对于用户自定义的类来说默认使用系统类加载器加载,对于系统核心类库,都是使用引导类加载器加载的。如下例子。

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);   // sun.misc.Launcher$AppClassLoader@18b4aac2

        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);   // null
    }
}

启动类加载器(引导类加载器,Bootstrap classLoader)

  • 这个类加载使用C/C++语言实现的(所以我们获取不到),嵌套在JVM内部。(它就是JVM的一部分)
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib /rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承自java.lang.classLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器(它们本身也算是一个类,也需要加载),并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
     

扩展类加载器Extension classLoader

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于classLoader类
  • 父类加载器为启动类加载器
  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
     

用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

为什么要自定义类加载器?

  • 隔离加载类(某些项目需要使用中间件,中间件和应用模块是隔离的,这样的话就需要把类加载到不同的环境中)
  • 修改类加载的方式
  • 扩展加载源(可以用数据库或者机顶盒之类的源头加载数据)
  • 防止源码泄漏 (java代码比较容易被编译篡改,所以要对字节码文件加密,也就需要自定义解密类加载器)
     

用户自定义类加载器实现步骤:
1. 开发人员可以通过继承抽象类java.lang.classLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
2. 在JDK1.2之前,在自定义类加载器时,总会去继承classLoader类并重写loadclass ()方法,从而实现自定义的类加载类,但是在JDKl.2之后已不再建议用户去覆盖loadclass()方法,而是建议把自定义的类加载逻辑写在findclass ()方法中
3.在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

ClassLoader

classLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)

双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

场景重现  (如下,如果加载的是自定义的String,那项目将极容易遭受攻击,黑客传他自定义的java类库给你,你运行了就玩完了)

所以有了双亲委派机制

工作原理

  1. 如果一个美加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

在上例中:层层委托,到达BootStrap ClassLoader  而“出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类” 这种情况属于java开头的,BootStrap ClassLoader直接就可以加载了。new  出来的对象自然就是核心API里的了。

第二种情况中,BootStrap ClassLoader加载器不加载Com开头的包,扩展类加载器(Extension classLoader)的加载路径为:“java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库”,这也不在。继续交由子类处理,最后由系统类加载器加载。

双亲委派机制通俗理解就是,先看看安全的路径(上层加载器对应的路径)能不能加载,不能再往下,以确保系统安全

优势
避免类的重复加载
保护程序安全,防止核心API被随意篡改

沙箱安全机制


自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\string.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制

Sandboxie(又叫沙箱、沙盘)即是一个虚拟系统程序,允许你在沙盘环境中运行浏览器或其他程序,因此运行所产生的变化可以随后删除。它创造了一个类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。 在网络安全中,沙箱指在隔离环境中,用以测试不受信任的文件或应用程序等行为的工具。(插入不信任的U盘的时候可以在沙箱中运行)

 类的主动使用和被动使用等

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的classLoader(指classLoader实例对象)必须相同。

换句话说,在JVM中,即使这两个类对象(class对象)来源同一个class文件,被同一个虚拟机所加载,但只要加载它们的classLoader实例对象不同,那么这两个类对象也是不相等的。

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的?(动态空间?)

Java程序对类的使用方式分为:主动使用和被动使用。·

主动使用,又分为七种情况:

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(比如:Class.forName ( "com.atguigu . Test") )
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类
  7. JDK 7开始提供的动态语言支持: java.lang.invoke.MethodHandle实例的解析结果 REF_getstatic、REF_putstatic、REF_invokestatic句柄对应的类没有初始化,则初始化

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。(这才是主动与被动的区别,被动使用会加载但不会初始化)
 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值