JVM的初步认识

文章目录

  • JVM 简介
    • JVM 发展史
      • 1.Sun Classic VM
      • 2.Exact VM
      • 3.HotSpot VM
      • 4.JRockit
      • 5.J9 JVM
      • 6.Taobao JVM(国产研发)
    • JVM 和《Java虚拟机规范》
  • JVM 运行流程
    • JVM 执行流程
  • JVM 运行时数据区
    • 程序计数器
    • Java虚拟机栈
    • 本地方法栈
    • 方法区
  • JVM类加载
    • 类加载过程
      • Loading(加载)
      • Linking(连接)
      • Initialization(初始化)
    • 双亲委派模型
      • 什么是双亲委派模型
      • **双亲委派模型的优点**
  • 垃圾回收相关
    • 垃圾回收是什么
    • 垃圾回收回收的是什么
    • 死亡对象的判断算法
      • 基于引用计数
      • 基于可达性分析
    • 垃圾回收算法
      • 标记 - 清除算法
      • 复制算法
      • 标记-整理算法
      • 分代算法
    • 垃圾收集器

JVM 简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。

虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。

常见的虚拟机:JVM、VMwave、Virtual Box。

JVM 和其他两个虚拟机的区别:

  1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;

  2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。

JVM 是一台被定制过的现实当中不存在的计算机。

JVM 发展史

1.Sun Classic VM

早在1996年Java1.0版本的时候,Sun公司发不了一款名为Sun Classic vm的java虚拟机,它同时也是世界上第一款商业java虚拟机,jdk1.4 时完全被淘汰。

这款虚拟机内部只提供解释器。

如果使用JIT编译器,就需要进行外挂。但是一旦使用了JIT编译器,JIT就会接管虚拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作。

现在Hotspot内置了此虚拟机;

2.Exact VM

为了解决上一个虚拟机问题,jdk1.2时,sun提供了此虚拟机。

Exact 具备现代高性能虚拟机的雏形,包含了一下功能:

  1. 热点探测(将热点代码编译为字节码加速程序执行);

  2. 编译器与解析器混合工作模式。

只在Solaris平台短暂使用,其他平台上还是 classic vm

英雄气短,终被Hotspot虚拟机替换。

3.HotSpot VM

HotSpot 历史

  1. 最初由一家名为“Longview Technologies”的小公司设计;

  2. 1997年,此公司被Sun收购;2009年,Sun公司被甲骨文收购。

  3. JDK1.3时,HotSpot VM成为默认虚拟机

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

不管是现在仍在广泛使用JDK6,还是使用比较多的JDK8中,默认的虚拟机都是HotSpot;Sun/Oracle JDK和OpenJDK的默认虚拟机。从服务器、桌面到移动端、嵌入式都有应用。

名称中的HotSpot指的就是它的热点代码探测技术。它能通过计数器找到最具编译价值的代码,触发即时编译(JIT)或栈上替换;通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡。

4.JRockit

JRockit 是专注于服务器端应用,目前在HotSpot的基础上,移植JRockit的优秀特性。

它可以不太关注程序的启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行;

大量的行业基准测试显示,JRockit JVM是世界上最快的JVM。

使用JRockit产品,客户已经体验到了显著的性能提高(一些超过了70%)和硬件成本的减少(达50%);

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

JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒级的JVM响应时间,适合财务、军事指挥、电信网络的需要;

MissionControl服务套件,它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具;2008,BEA被Oracle收购。

Oracle表达了整合两大优秀虚拟机的工作,大致在JDK8中完成。整合的方式是在HotSpot的基础上,植JRockit的优秀特性。

5.J9 JVM

全称:IBM Technology for Java Virtual Machine,简称IT4J,内部代号:J9。

市场定位于HotSpot接近,服务器端、桌面应用、嵌入式等多用途JVM,广泛用于IBM的各种Java产品。

目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机(在IBM自己的产品上稳定);

2017年左右,IBM发布了开源 J9 VM,命名 OpenJ9,交给Eclipse基金会管理,也称为Eclipse OpenJ9。

6.Taobao JVM(国产研发)

由 AliJVM 团队发布。阿里,国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域,

需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。

基于OpenJDK 开发了自己的定制版本AlibabaJDK,简称AJDK。是整个阿里JAVA体系的基石;

基于OpenJDK HotSpot JVM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机,它

具有以下特点(了解即可):

  1. 创新的GCIH(GC invisible heap)技术实现了off-heap,即将生命周期较长的Java对象从heap中移到heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收评率和提升GC的回收效率的目的。

  2. GCIH中的对象还能够在多个Java虚拟机进程中实现共享。

  3. 使用crc32指令实现JVM intrinsic降低JNI的调用开销;

  4. PMU hardware的Java profiling tool和诊断协助功能;

  5. 针对大数据场景的ZenGC。

taobao JVM应用在阿里产品上性能高,硬件严重依赖intel的cpu,损失了兼容性,但提高了性能,目前已经在淘宝、天猫上线,把Oracle官方JVM版本全部替换了

JVM 和《Java虚拟机规范》

以上的各种 JVM 版本,比如 HotSpot 和 J9 JVM,都可以看做是不同厂商实现 JVM 产品的具体实现,而它们(JVM)产品的实现必须要符合《Java虚拟机规范》,《Java虚拟机规范》是 Oracle 发布 Java 领域最重要和最权威的著作,它完整且详细的描述了 JVM 的各个组成部分。

PS: 本文以下部分,默认都是使用 HotSpot,也就是 Oracle Java 默认的虚拟机为前提来进行介绍的。

JVM 运行流程

JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键,那么 JVM 是如何执行的呢?

JVM 执行流程

程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 **执行引擎(Execution Engine)**将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能.JVM的初步认识_第1张图片

总结来看, JVM 主要通过分为以下 4 个部分,来执行 Java 程序的,它们分别是:

  1. 类加载器(ClassLoader)

  2. 运行时数据区(Runtime Data Area)

  3. 执行引擎(Execution Engine)

  4. 本地库接口(Native Interface)

JVM 运行时数据区

JVM 运行时数据区域也叫内存布局,但需要注意的是它和 Java 内存模型((Java Memory Model,简称JMM)完全不同,属于完全不同的两个概念,它由以下 5 大部分组成:

JVM的初步认识_第2张图片

程序计数器

程序计数器是内存中最小的区域,保存了当前线程执行的指令的地址位置.

这里的指令指的是字节码文件

程序想要运行,哪买JVM就得把字节码加载起来,放到内存中,程序会一条一条的把指令从内存中取出来,放在CPU上执行,也就需要随时记住当前执行到哪一条了

CPU是并发式的执行进程的,CUP不仅仅只给JVM一个进程提供服务,而是要伺候所有的进程,正因为操作系统是以线程为单位进行调度的,每个线程都得记录自己的执行位置,所以每一个线程都有一个程序计数器

正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。

这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

Java虚拟机栈

栈里主要存放的是局部变量方法调用信息

方法调用的时候,每次调用一个新的方法,都会涉及到"入栈"操作

每次执行完一个方法,都会涉及到"出栈"操作

JVM的初步认识_第3张图片

此处的栈虽然指的是JVM内存中的一部分,但是这里的工作过程是和数据结构中的栈,非常类似的

栈里面每一个元素,叫做栈帧

JVM的初步认识_第4张图片

  1. 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。

  2. 操作栈:每个方法会生成一个先进后出的操作栈。

  3. 动态链接:指向运行时常量池的方法引用。

  4. 方法返回地址:PC 寄存器的地址。

像IDEA调试/程序抛异常都能让我们看到当时的调用信息(方法之间是怎么调用过来的),这个过程其实就是靠读取上述的栈空间的数据

本地方法栈

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用的。

jvm在使用操作系统的时候使用,调用native方法

一个进程只有一份,多个线程共用一个堆,同时堆也是 内存空间中最大的区域

new 出来的对象,就是在堆中

对象的成员变量也是在堆中

堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象

会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。

JVM的初步认识_第5张图片

垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清楚掉。

方法区

方法区中,放的是"类对象"

.java文件会被编译成.class文件(二进制文件)

.class文件会被加载到内存中,也就会被JVM构造成类对象(加载的过程叫做类加载)

这里的类对象就是放在方法区中

类对象描述了这个类长什么样,比如类的名字是啥,里面有哪些成员,有哪些方法,每个成员叫什么名字,是什么类型,是private还是public.每个方法叫什么名字,是什么类型,是private还是public,方法里有哪些指令…

类对象里还有个很重要的东西 : 静态成员

static修饰的成员叫做"静态成员",普通的成员,叫做"实例属性"

JVM类加载

类加载过程

类加载要做什么?

将.class文件加载到内存中,构建成类对象

从上面的图片我们可以看出整个 JVM 执行的流程中,和程序员关系最密切的就是类加载的过程了,所以接下来我们来看下类加载的执行流程。

对于一个类来说,它的生命周期是这样的:

JVM的初步认识_第6张图片

根据Java官方文档,类加载分为三个大的步骤

Loading LinLinking Initialization

其中前 5 步是固定的顺序并且也是类加载的过程,其中中间的 3 步我们都属于连接,所以对于类加载来

说总共分为以下几个步骤:

  1. 加载

  2. 连接

    1. 验证

    2. 准备

    3. 解析

  3. 初始化

下面我们分别来看每个步骤的具体执行内容

Loading(加载)

读取对应的.calss文件,打开并读取.class文件,同时初步生成一个类对象

在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。(打开文件)

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(读取文件,进行解析)

  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(产生一个类对象)

Loading的关键环节: .class文件到底长什么样

JVM的初步认识_第7张图片

Linking(连接)

Linking连接一般就是建立好几个实体类之间的联系

JVM的初步认识_第8张图片

Initialization(初始化)

真正对类对象进行初始化,尤其针对于静态成员

初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。

经典面试问题:

JVM的初步认识_第9张图片

双亲委派模型

JVM的初步认识_第10张图片

什么是双亲委派模型

双亲委派模型,描述的就是JVM中的类加载器,如何根据类的全限定名( java.lang.String )找到.class文件的过程~~

双亲委派模型处于Loading阶段的

类加载器

JVM提供了专门的对象,叫做类加载器,负责进行类加载,当然找文件的过程也是类加载负责的

.class文件,可能存放的位置有很多,有的要放在JDK目录里,有的放在项目目录里,还有的放在其他特定位置

因此JVM里面提供的多个类加载器,每个类加载器负责一个片区

默认的类加载器有三个

JVM的初步认识_第11张图片

JVM的初步认识_第12张图片

双亲委派模型,就描述了这个找目录的过程,也就是上述类加载器是如何配合的

JVM的初步认识_第13张图片

JVM的初步认识_第14张图片

双亲委派模型的优点

  1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。

  2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了。

垃圾回收相关

垃圾回收是什么

我们在写代码的时候,经常会申请内存(创建变量,new对象,加载类…),申请内存后可能有两种情况 :

  1. 申请后一直不释放内存,导致最后内存申请的越来越多,知道最后,内存不足,内存泄漏.
  2. 申请后使用,不知道什么时候就将内存给释放了,下次在想使用的时候,发现内存被释放了,还要重新申请内存 .

内存的释放,早了不可以,晚了也不可以,要可以恰到好处才行!!!

在Java中,采取了一个方案,就是垃圾回收机制

大概就是 由运行时环境(JVM)来通过复杂的策略判定内存是否可以回收,并且进行回收的动作

垃圾回收,本质上是靠运行时环境,额外做了很多的工作,来完成自动释放内存的操作,让程序员的心智负担大大降低

垃圾回收的缺点

  1. 消耗额外的开销
  2. 可能会影响程序的流畅运行(垃圾回收经常会引入STW问题)

垃圾回收回收的是什么

上面讲了Java运行时内存的各个区域。对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了。因此我们本次所讲的有关内存分配和回收关注的为Java堆与方法区这两个区域。

Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下几种算法

JVM的初步认识_第15张图片

内存 VS 对象

在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。

JVM的初步认识_第16张图片

垃圾回收阶段分为两个阶段

  1. 找垃圾/判定垃圾

  2. 释放垃圾

死亡对象的判断算法

如果找垃圾/判定垃圾

主流的方案有两种

  1. 基于引用计数[不是Java采取的方案,是其他的语言.例如python采取的方案]
  2. 基于可达性分析[Java采取的方案]

在面试中,要注意面试官的问题,弄清楚面试官要问的是什么

  1. 谈谈垃圾回收机制中如何判定是不是垃圾
  2. 谈谈Java的垃圾回收机制如何判定是不是垃圾

基于引用计数

针对每个对象,都会额外引入一小块内存,保存这个对象有多少引用指向它

JVM的初步认识_第17张图片

通过引用来决定对象的生死.

引用计数简单可靠高效,但是有两个致命缺点

  1. 空间利用率比较低

每次new一个对象都要搭配一个计数器(计数器假设是4个字节),如果对象本身就比较大(几百个字节),多出来4个字节不算什么;但是如果对象本身就比较小(比如4个字节)多出来4个字节,相当于空间被浪费了一倍

2, 循环引用的问题

JVM的初步认识_第18张图片

基于可达性分析

通过额外的线程,定期的针对整个内存空间的对象进行扫描

有一些起始位置(成为GCRoots),会类似于深度优先遍历一样,把可以访问到的对象都标记一遍(带有标记的就是可达的对象),没被标记的就是不可达,就是垃圾

GCRoots

  1. 栈上的局部变量
  2. 常量池中的引用指向的对象
  3. 方法区中的静态成员指向的对象

JVM的初步认识_第19张图片

可达性分析的优点就是克服了引用计数的两个缺点:空间利用率低,循环引用

自身的缺点:系统开销大,遍历一次可能比较慢

找垃圾,核心就是确认这个对象未来是否还会使用,什么算不使用了? 没有引用指向,就不使用了

垃圾回收算法

标记 - 清除算法

JVM的初步认识_第20张图片

标记 : 可达性分析的过程

清除 : 直接释放内存

此时如果直接释放,虽然内存是还给内存了,但是被释放的内存是离散的(不是连续的),分散开,带来的问题就是 “内存碎片”

假设此时空闲的内存有很多,假设有1G

如果要申请500MB内存,可能会申请失败

因为申请的500MB是连续内存,每次申请,都是申请的连续的内存空间

而这里的1G可能是多个碎片加在一起,才有1G

非常影响程序的执行

复制算法

为了解决内存碎片,引入的复制算法

JVM的初步认识_第21张图片

此时内存碎片的问题就迎刃而解了,但是也出现了新的问题

  1. 内存空间利用率第
  2. 如果保留的对象多,要释放的内存少,此时复制开销就很大

标记-整理算法

针对复制算法,在作出改进

JVM的初步认识_第22张图片

分代算法

上述的方法,虽然能解决问题,但是都有缺陷

实际JVM中的实现,会把多种方案结合起来使用

针对对象进行分类(根据对象的 “年龄” 分类) 一个对象熬过一轮GC的扫描,就 “长了一岁”

在Object的对象头里有一个空间存储 “年龄”

JVM的初步认识_第23张图片

  1. 刚创建出来的对象,会放在伊甸区

  2. 如果伊甸区的对象熬过了一轮扫描,就会被拷贝到幸存区(应用了复制算法)

根据实际经验,大部分的对象都是"朝生夕死",真正能傲过一轮GC的对象不多

  1. 在后面的几轮GC中,幸存区的对象会在两个幸存区之间来回拷贝(复制算法),每一轮都会淘汰一波幸存者
  2. 在若干轮之后,幸存区的对象就可以进入老年代 (老年代使用标记 - 整理 的方式进行回收)

老年代的特点:里面的对象大多数是比较老的("年龄"比较大)

基本的经验规律:一个对象越老,继续存活的可能性越大,所以老年代的GC扫描频率大大低于新生代

分代算法中,有一种特殊情况,有一类对象可以直接进入老年代(大对象,占更多的内存) 大对象占内存多,不适合使用复制算法)

垃圾收集器

你可能感兴趣的:(JavaEE,jvm,java,开发语言)