Java 虚拟机

Java 虚拟机

Java 虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是 Java 语言的运行环境,它是 Java 最具吸引力的特性之一。同时,Java具有跨平台的特性,也是通过 JVM 进行实现的

为什么 Java 是跨平台语言

首先,Java 源代码会被编译成 .class 文件, 而该文件是运行则是在 JVM 上的。在下载 JDK 的时候,我们可以很轻松的发现 JDK 是分不同的操作系统的,而 JDK 是包含 JVM 的,所以 Java 依赖着 JVM 实现了跨平台。

JVM 是面向操作系统的,它负责把 Class 字节码解释成系统所能识别的指令并执行,同时也负责程序运行时内存的管理。

从 Java 源代码到运行的过程

简单分为 4 个步骤 :编译——>加载——>解释——>执行

编译

将源码文件编译成 JVM 可以解释的 .class 文件,比如对泛型的擦除和经常使用的 Lombok 就是在编译阶段完成的

下图是详细的编译过程

在这里插入图片描述

经过上面的步骤,Java 源文件就会被编译成 .class 文件。

加载

将编译后的 .class 文件加载到 JVM 中,而加载可以细化为:装载——>连接——>初始化

  • 装载
    • 装载时机:为了节省内存的开销,并不会一次性将所有的类都装载到 JVM,而是等到有需要的时候才进行装载(比如 new 和反射等)
    • 装载发生:class 文件是通过 类加载器装载到 JVM 中,为了防止内存中出现多份同样的字节码,使用了双亲委派机制(后面会讲)
    • 装载规则:JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader)实现装载,而程序中的类文件则有系统加载器(AppClassLoader)实现装载。
    • 装载这个阶段可以总结为,查找并加载类的二进制数据,在 JVM 堆中创建一个 java.lang.Class 类对象,并将类相关信息存储在 JVM 方法区中。
  • 连接,也可以细化为几个步骤:验证——>准备——>解析
    • 验证:验证类是否符合 Java 规范和 JVM 规范
    • 准备,为类的静态变量分配内存,初始化为系统的初始值
    • 将符号引用转为直接引用的过程
    • 通过连接这个步骤后,现在已经对 class 信息做校验并分配了内存空间和默认值
  • 初始化,为类的静态变量赋予正确的初始值
    • 收集class 的静态变量、静态代码块、静态方法至方法,随后从上往下开始执行
    • 如果实例化对象,则会调用方法对实例化变量进行初始化,并执行对应的构造方法内的代码。

解释

将字节码转换成操作系统识别的指令,两种方式,一个是字节码解释器、一个是即使编译器(JIT)

JVM 会对热点代码做编译,非热点代码直接进行解释。当JVM发现某个方法或代码块运行特别频繁时,就会有可能把这部分代码认定为热点代码。

使用热点探测,来检测是否是热点代码,由两种方式,计数器和抽样。HotSpot 使用的是计数器的方式,为每个方法准备了两类计数器:方法调用计数器和回边计数器。这两个计数器都有一个确定的阈值,当计数器超过该值则会触发 JIT 编译。而JIT 会把热点方法和指令码保存起来,下次执行的时候就无须重复进行解释,直接执行缓存的机器语言。

HotSpot是 JIT 技术的一种实现,它从运行解释开始,并观察应用程序的实际性能。然后选择应用程序的某些部分作为本机代码完全编译并缓存,以便更快地执行HotSpot是 JVM 部分的实现

执行

操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令。

双亲委派机制

上面说到,class 文件是通过 类加载器装载到 JVM 中,为了防止内存中出现多份同样的字节码,使用了双亲委派机制

它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上

JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。

如图:

在这里插入图片描述

  • 根加载器(Bootstrp loader)

该加载器使用C++实现(不会继承ClassLoader),是虚拟机自身的一部分。该类加载器主要是负责加载存放在JAVA_HOME\lib目录,或者被-Xbootclasspath参数指定路径存放的,并且是java虚拟机能识别的类库加载到虚拟机内存中。(eg:主要是加载java的核心类库,即加载lib目录下的所有class)

  • 扩展类加载器(ExtClassLoader )

这个类加载器主要是负责加载JAVA_HOME\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有类库

  • 应用类加载器(AppClassLoader )

这个类的加载器是由sun.misc.Launcher$AppClassLoader来实现,因为该加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以一般也称为该加载器为系统类加载器。该加载器主要是加载用户类路径上所有的类库,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序的默认加载器。

Java 内存模型

Java 为了屏蔽硬件和操作系统访问内存的各种差异,提出了 Java 内存模型的规范,保证了 Java 程序在各种平台下对内存的访问都能得到一致效果。Java 内存模型是一种规范!

Java 内存模型抽象结构

Java内存模型定义了:Java线程对内存数据进行交互的规范。

规范如下:线程之间的共享变量存储在主内存中,每个线程有自己私有的本地内存,本地内存存储了该线程以读/写共享变量的副本。

下图所示:

在这里插入图片描述

Java 内存模型规定:线程对变量的操作都必须在本地内存进行,不能直接读写主内存的变量

Java 内存模型定义了 8 中操作来完成变量如何从主内存到本地内存,以及变量如何从本地内存到主内存

分别是read(读)、load(加载)、use(使用)、assign(分配)、store(存储)、write(写入)、lock(上锁)、unlock(解锁)

下图来解释这些操作

在这里插入图片描述

happen-before 规则

由Java 内存模型定义,目的是为了阐述操作之间的内存可见性。

因为在 CPU 和编译器层面上都有指令重排的问题,指令重排虽然能提高运行效率**,但在一些情况下,这一组操作都不能进行重排序,**即前面一个操作的结果对后续操作必须是可见的。

所以,Java 内存模型提出了 happen-before 这套规则:

1、单线程happen-before原则:

        在同一个线程中,书写在前面的操作happen-before后面的操作。

2、锁的happen-before原则:

        同一个锁的unlock操作happen-before此锁的lock操作。

3、volatile的happen-before原则:

        对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。

4、happen-before的传递性原则:

        如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

5、线程启动的happen-before原则:

        同一个线程的start方法happen-before此线程的其它方法。

6、线程中断的happen-before原则:

        对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

7、线程终结的happen-before原则:

        线程中的所有操作都happen-before线程的终止检测。

8、对象创建的happen-before原则:

        一个对象的初始化完成先于他的finalize方法调用。

有了这些规则,我们写代码只要在这些规则下,前一个操作的结果对后续操作时可见的,是不会发生重排序的。

Java 内存结构

class 文件会被类加载器装载到 JVM 中,并且 JVM 会负责程序运行时和内存管理。而 JVM 内存结构,往往指的是 JVM 定义的运行时数据区域。简单来说分为 5 大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈。

注意:上述 5 大块,是 JVM 规范的分区概念,到具体实现落地,不同厂商实现有所区别。

在这里插入图片描述

下面逐一介绍:

程序计数器

Java 是多线程的语言,我们都知道线程数大于 CPU 数,就有可能发生线程切换现象,切换意味着中断和回复,那自然需要一块区域来保存 当前线程的执行信息。

程序计数器是为了记录各个线程执行的字节码地址(分支、循环、跳转、异常、线程恢复等都依赖于计数器)

虚拟机栈

每个线程在创建的时候都会创建一个虚拟机栈,每次方法调用都会创建一个栈帧。每个栈帧都会包含:局部变量表、操作数栈、动态连接和返回地址。

如图:

在这里插入图片描述

通过上图,也很好理解虚拟机栈的作用:保存方法局部变量、部分变量的计算并参与方法的调用与返回

本地方法栈

本地方法栈和虚拟机栈功能类似,虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。这里的本地方法指的是非 Java 方法,一般本地方法是使用 C 语言实现的。

方法区

方法区主要存放已被虚拟机加载的类相关信息:类信息、常量池

类信息包含:类版本、字段、方法、接口和父类信息。

  • 常量池
    • 静态常量池
      • 存储字面量和符号引用等信息,包括字符串常量池
    • 运行时常量池
      • 存储类加载时生成的直接引用等信息

从逻辑分区的角度而言,常量池是属于方法区的。但从JDK7后,就已经把运行时常量池和静态常量池转移到堆内存进行存储。

堆是线程共享的区域,几乎所有类的实例和数组分配的内存都来自它。

堆被划分为新生代和老年代,新生代又被划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor组成

如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWjo6piC-1661785301729)(Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.assets/008i3skNgy1gs7d4xpm39j31i00ootkz.jpg)]

将堆内存分开了几块区域,主要和内存回收有关(垃圾回收机制),后面再说。

进行存储。**

堆是线程共享的区域,几乎所有类的实例和数组分配的内存都来自它。

堆被划分为新生代和老年代,新生代又被划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor组成

如图

在这里插入图片描述

将堆内存分开了几块区域,主要和内存回收有关(垃圾回收机制),后面再说。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值