谈谈我对JVM的理解(一)

前言

        想用通俗的话,讲述我对 JVM 的理解 。如果读者能够从我的讲述中获取到新知识,荣幸之至,如果没能对其描述清楚,也希望小伙伴们不要见怪,我们可以评论区一起讨论一起进步,也欢迎大佬们进行指正,感激不尽~

Java 的诞生

        这历史追溯得有些许久远,但是很有必要。总的来说,Java 诞生的时候 C/C++ 盛行,它的出现是为了解决 C++ 在特定领域中存在的问题,具体有什么问题?Java 又是怎么解决?

  1. 平台无关性:C++编写的程序需要依赖特定的操作系统和硬件平台。Java通过引入Java虚拟机(JVM),使得程序可以在任何支持Java虚拟机的平台上运行,实现了平台无关性,也就是可以跨平台运行

  2. 内存管理和垃圾回收:在C++中,开发者需要手动管理内存,包括手动分配和释放内存,容易引发内存泄漏和悬挂指针等问题。而Java引入了自动化的垃圾回收机制,负责动态分配和回收内存,极大地简化了内存管理工作。

  3. 强大的类库支持:Java提供了丰富的类库和API,如集合框架、图形界面、网络、数据库等,不需要我们从头开始编写底层功能~

  4. 安全性:C++中存在指针操作、越界访问、缓冲区溢出等安全性问题。Java通过提供安全性机制,如类加载、访问控制、异常处理等,提供了更高层次的安全性保障,减少了潜在的系统漏洞。

  5. 面向对象特性:相比于C++,Java进行了简化,更加注重面向对象编程思想的实践(C++不仅仅面向对象,所以在这方面就不是很纯粹~),便于程序的维护和扩展。 

        这并不是说 Java 语言比 C++ 优秀,只是说 Java 在某方面比 C++ 更为快捷方便。

“程序是怎么跑起来的”?

        前面提到 Java 是为了解决 C++ 的问题,那它们的运行有什么区别?为什么要谈这个内容?好吧,我发现很多资料在讲述JVM的时候默认为大家对 Java 程序的运行过程已经十分了解,所以直接就开始讲述JVM是虚拟计算机等等。。。但我还是想先进行一个梳理,顺便再聊聊 C++,就当作知识科普看看吧~

        (也可以看看我的另一篇小博文: C++、python、Java的运行过程对比

C++的运行过程

  1. 编写C++源代码,保存为.cpp文件。
  2. 使用C++编译器(gccclang等)将C++源代码编译成机器码,生成可执行文件。
  3. 运行生成的可执行文件。

Java的运行过程        

  1. 编写Java源代码,保存为.java文件。
  2. 使用Java编译器(javac)编译Java源代码,生成字节码文件(.class)。
  3. 使用Java虚拟机(Java Virtual Machine,JVM)加载字节码文件,并执行。

        可以看出,Java 和 C++不同的是,C++ 经过编译器,直接就变成了可执行文件!但是 Java 编译以后生成的是字节码文件!是不能直接执行的!这个字节码文件包含了 JVM 能够理解的指令和结构,所以需要 JVM 作为中间层解析并执行这些字节码指令!有的小伙伴又有疑问了,这不是很多余吗?直接编译不就好了?好家伙,别忘了,咱们前面才谈到 Java 诞生原因之一是为了解决 C++ 的跨平台问题,JVM 就是实现这一步的工具,当然,它的功能可不止这个,咱们慢慢说。

JVM是什么 

        正题终于来了,JVM 是什么?

        JVM (Java Virtual Machine):Java 虚拟机,其实可以理解为是一个解释器。它能够Java中用于描述一段程序逻辑的字节码指令转换为对应的机器码指令。同时,JVM还负责内存管理、垃圾回收等任务,提供一个安全和稳定的运行环境

        重点词:将字节码转换为机器码,负责内存管理、垃圾回收等,提供运行环境。家人们,这不就是 Java 想要解决的 C++ 问题?!

JVM的位置

        注意这里有个细节,JVM 和操作系统相连接,那么就需要一个接口去进行调用,这就和后面会谈到的 本地方法 native 有关系了! 

JVM的体系结构

        这个部分请务必图文结合着来看,可以自己动手再画画!(虽然我这里的图是网图,美观的好图应该一起分享)

        - 类加载器(Class Loader):负责将类文件加载到JVM中,并把它们转换成Java运行时数据结构,即类模板对象。

        - 运行时数据区(Runtime Data Area):JVM运行Java程序需要的内存区域。

        - 执行引擎(Execution Engine):负责执行编译器编译产生的字节码指令。

        - 垃圾收集器(Garbage Collector):负责回收堆区域内存中不再使用的对象。

        - 本地方法接口(Native Interface):是JVM调用其他语言(如C、C++)实现的方法的接口。

        从这个体系结构上来看,虚拟机栈、本地方法栈、程序计数器这三部分肯定不会产生垃圾,所谓的JVM调优其实是在堆里面调优,方法区属于特殊的堆,但是99%都在堆区进行调优~

        注:几乎看到的所有的 JVM 体系结构图都会有一个类文件(.class),但是它本身并不属于 JVM 体系结构的模块!我们从上一节已经知道类文件就是源码经过编译器产生的字节码文件!

        接下来再来详细说说具体的各个模块分别扮演着怎样的角色以及作用。

类加载器

        “搬运工”,将编译后的Java字节码文件加载到JVM的内存中,并转换成类模板对象。类加载器分为几个层次:启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器,加载器之间构成了类加载器的层次结构,保证了类的加载安全性,后面我们再来好好谈谈这个。

加载器执行过程 

        先来说说类加载器是如何工作的,总共四大步:加载、链接、初始化、卸载。

        - 加载:把 class 文件加载到内存;数据结构等信息放到方法区;生成这个类的对象

        - 链接:①验证:查看类是否有正确的数据结构,和其他类协调(安全检查)

                     ②准备:为这个类的静态变量分配内存、设置默认值

                     ③解析:将符号引用解析为实际引用。(比如我们在编写代码中,需要使用某种方法,但其实并不知道这些方法的内存地址,因为这个时候都没有被加载到虚拟机中,无法获得实际的地址,对于一个方法的调用,编译器会生成符号引用,用来指代要调用的方法,解析阶段的目的就是为了把符号引用解析为实际的引用,可以和操作系统里面的逻辑地址映射到实际物理地址的情况做类比~)

        - 初始化:虚拟机调用< clinit>方法,对类变量进行初始化操作。这个方法由编译器收集,顺序执行所有类变量(static 修饰的成员变量)显式初始化和静态代码块中语句。子类在执行初始化之前,父类会先被初始化完毕。

        简单的来说,在构造子类之前,会先构造其父类!然后按照顺序进行初始化类变量。

        - 卸载:垃圾回收器(GC)将无用的对象从内存中卸载

加载器加载顺序

       刚开始介绍类加载器的时候咱们就说到类加载器的层次结构保证类加载的安全性,现在咱们来具体看看是怎样的层次结构

        学过操作系统的同学都知道,一个作业被调度,是遵循某种调度规则的,那么类被加载的时候,JVM 加载 Class同样也会有它自己的一种优先级规则,通过这种优先级规则,JVM 会选择合适的类加载器进行加载,我们按照优先级的高低来进行讲解。

启动类(根)加载器

        Bootstrap ClassLoader,Java虚拟机的内置类加载器,负责加载JDK核心类,如 rt.jar(JRE的核心类库),是被 JVM 自身实现的。

扩展类加载器

        Extension ClassLoader,用来加载Java的扩展类库,如 jre/lib/ext 目录下的 jar 包,默认情况下与Bootstrap ClassLoader没有直接的父子关系,因为此类加载器是由JVM自身实现的。

应用程序加载器(系统加载器)

        App ClassLoader,负责加载应用程序类,是最常用的加载器,用来加载用户类路径(classpath)上的所有类,一般使用ClassLoader.getSystemClassLoader()方法获取

自定义类加载器

        Custom ClassLoader,自定义类加载器可以按照程序员的愿望实现类的加载,可以用于实现一些特殊需求,比如实现热部署功能。

双亲委派机制

        层次结构说清楚了,那么如何保证类加载器的安全性呢?

        我们现在已经知道了类加载器的优先级顺序,那它是通过什么来实现的?正如作业调度里面,知道了优先级,还需要确定调度算法,因为我们可以选择低优先级的先调度,也可以规定高优先级的先调度,类的加载同样需要依照某种理论依据。

        说答案,优先级的实现机制,是“双亲委派模型”!所遵循的优先级顺序,就是上面谈到的加载器顺序。我们来看看具体的执行过程:

  1. 类加载器收到类加载的请求
  2. 将请求向上委托给父类加载器去完成,一直向上委托,直到最顶级加载器——启动类加载器
  3. 启动加载器检查是否能够加载当前这个类,能加载就结束,并使用当前的加载器,否则抛出异常,并通知子加载器进行加载
  4. 重复步骤3
  5. 所有子加载器都无法加载,抛出 ClassNotFoundException 或 NoClassDefFoundError 等异常

        总结来说,JVM 通过“双亲委派机制”,根据上面的优先级顺序,来实现类加载!

        这样做的一个优点就是避免重复加载,可以确保Java程序中的类都是由同一个类加载器所加载,整个Java应用的类加载关系就成了一个树状结构。

沙箱安全机制

        还有一种沙箱安全机制,这个可以只作为了解部分。

        沙箱安全机制是一种限制程序在执行过程中访问资源和执行特定操作的安全控制机制。通过隔离、权限控制和安全审查等手段,保护系统的安全,限制代码对资源和操作的访问,防止恶意行为和异常操作对系统造成危害。它为执行不受信任的代码提供了一个受控且安全的环境,并为代码的测试和验证提供了安全保障。

本地方法栈Native

        还记得 JVM 的位置吧,紧挨着操作系统,但有时候 Java 语言不方便对操作系统或者是硬件进行处理,这个时候就需要用到其他语言了!

        本地方法栈里保存了本地方法的一些接口,这些接口是JVM调用操作系统和其他语言(如C、C++等)实现的方法和库的接口。比如线程Thread类的源码,会有一个带关键字 native 修饰的 start0 方法,这就是用 C++ 实现的~

PC寄存器

        程序计数器(Program Counter Register),每一个线程都有一个程序计数器,是线程私有的,它的本质就是一个指针指向方法区中的方法字节码(用来存储指向下一条指令的代码,也即将要执行的指令代码),在执行引擎读取下一条指令。换句话来说,保存着下一条要执行的指令的地址。另外,PC寄存器也是内存区域中唯一一个不会出现 OutOfMemoryError 的一个非常小的内存空间,几乎可以忽略不计。如果执行的是 native 方法,那这个指针就不工作了。

方法区

        Method Area,方法区是被所有线程共享,用于存储类的结构信息、常量、静态变量等数据。

        - 类的结构信息:方法区存储类、接口、枚举以及注解定义等信息。

        - 运行时常量池:类变量、方法和接口名,以及字面量和符号引用值,用于存储编译期间生成的各种字面量和符号引用。

        - 静态变量:所有的静态变量都被分配在方法区中,它们与类一起加载,类的实例化过程不会改变它们的值

        - 即时编译器的编译代码:方法区还存储一些即时编译器生成的编译代码,用于提高程序的执行效率

        由于方法区是被共享的区域,在高并发的情况下,如果不加以限制,就会造成线程安全问题,比如线程A、B同时读写某个静态变量。所以,在JDK 8 以后,方法区就替换为了元数据空间(Meta Space),但用途作用和方法区相同,我们在下一篇中再进行讲述。

        注意,方法区中存储的这些信息在Java虚拟机的生命周期中是保持不变的,只有在JVM关闭时才会被释放。所以,合理使用和管理方法区,是可以保障程序运行稳定和高效的~

写在后面

        还记得那个 JVM 的体系结构图吧?再放一次,方便小伙伴们看图回顾!剩下的内容咱们明天再进行补充,欢迎评论区讨论~~

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值