0
点赞
收藏
分享

微信扫一扫

浅谈JVM(面试常考)

幸福的无所谓 2022-07-12 阅读 106

JVM 简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。

常见的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:

  1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
  2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进
    行了裁剪。
    JVM 是一台被定制过的现实当中不存在的计算机。

1. JVM 内存区域划分

在这里插入图片描述

JVM 内存是从操作系统这里申请的,划分了不同的区域,不同的区域完成不同的功能

1.1 程序计数器(线程私有)

它是内存中最小的区域,保存了下一条要执行的指令的地址在哪…

1.2 Java 虚拟机栈(线程私有)

描述了局部变量和方法调用信息,方法调用的时候,每次调用一个新的方法,就涉及到"入栈"操作,每次执行完了一个方法,都涉及到"出栈"操作.
栈空间是比较小的,在 JVM 中可以配置栈空间的大小,但是一般也就几 M 或 几十 M,因此栈是很有可能会满的(正常我们写代码一般没事,就怕递归,一旦递归条件没设好,就会出现栈溢出:StackOverflowException)

1.3 本地方法栈(线程私有)

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

1.4 堆(线程共享)

一个进程只有一份,多个线程共用一个堆,也是内存中空间最大的区域,我们 new 出来的对象,就是在堆中,对象的成员变量,自然也在堆中.

1.5 方法区(线程共享)

方法区中,放的是"类对象",所谓的"类对象":我们所写的.java这样的代码会变成.class(二进制字节码),.class会被加载到内存中,也就是 JVM 构造成的类对象(加载的过程就称为"类加载"),"类对象"就描述了这个类长啥样,类的名字是啥,里面有哪些成员,有哪些方法,每个成员叫啥名字是啥类型(public/private…),每个方法叫啥名字,是啥类型(public/private…),方法里面包含的指令…
"类对象"里面还有一个很重要的东西,静态成员(static)

被static 修饰的成员,成为了"类属性",而普通成员,叫做"实例属性"

2. JVM 类加载机制

类加载,其实是设计一个 运行时环境 的一个重要的核心功能,类加载是干啥的?他是把 .class文件,加载到内存中,构建成类对象

2.1 类加载过程

2.1.1 加载(Loading)

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 Class Loading 是不同的,一个是加载 Loading 另一个是类加载 Class Loading,所以不要把二者搞混了

在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

会根据上面图中的格式,把读取并解析到的信息,初步的填写到类对象中

2.1.2 连接(Linking)

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

2.1.3 初始化(Initializing)

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

2.2 双亲委派模型

这个东西在我们工作中用处不大,但是面试的时候可是经常被问到…
这个东西就是类加载中的一个环节,这个环节处于 Loading 阶段的(比较靠前的部分),双亲委派模型,其实就是 JVM 中的类加载器,如何根据全限定名(java.lang.String)来找到 .class文件的过程

我们的双亲委派模型,就描述了这个找目录过程,也就是上述类加载器是如何配合的…
在这里插入图片描述

这一套查找规则,就称为"双亲委派模型"(这是音译过来的,parent 既可以是父亲也可以是母亲,按规矩,叫他"单亲委派模型"也不是不可以,当然,起名字这回事不是我们能决定的)

3. JVM 垃圾回收机制(GC)

3.1 什么是垃圾回收

垃圾回收(GarbageCollection,GC),我们在写代码的时候,经常会申请内存的,创建变量,new 对象,加载类…这些都是在申请内存,都是从操作系统申请,既然申请了内存,那我们在不用的时候肯定也是要归还内存的。

一般来说,申请内存的时机都是明确的,(需要保存某些数据,就需要申请内存),而释放内存的时期,就不是那么清楚了.我们也不清楚自己还用不用这块内存

3.2 为啥会出现垃圾回收机制

像 C 语言: "内存释放这事我不管,你们程序员自己看着办吧,反正又不扣我的钱…"因此,在 C 语言中,就会遇到一个常见的令人头疼的问题 => "内存泄露"(申请之后,忘了释放) => 可用的内存越来越少,最终无内存可用了!!所以,"内存泄露"是C/C++ 程序员的头疼的问题,有的泄露快,有的泄露慢,暴露的时机不确定,如果出现了,是很难排查的.C++后来就提出了一个智能指针(大概就只是简单依赖了一下 C++ 中的 RAII机制,其实一点也不智能..)这样的机制,通过它就可以一定程度上降低"内存泄露"的风险…但它在 java 众多机制面前就是个弟中弟(滑稽)

所以,像 Java,GO,PHP…现在市面上的大部分主流编程语言,都采取了一个方案,就是垃圾回收机制!!
大概就是有运行时环境(像JVM,Python 解释器,Go 运行时…)来通过更复杂的策略判定内存是否可以回收,并执行回收动作…垃圾回收,本质上是靠运行时环境,额外做了很多的工作,来完成自动释放内存的操作的,让程序员心智负担大大降低了

3.3 垃圾回收要回收啥

回收的是内存,但内存有包括:程序计数器,栈,堆和方法,有些回收,有些不回收:

我们这里就讨论堆上的垃圾回收
首先看一下这张图:
在这里插入图片描述

上面这张图可以把它理解为三个派别:积极派,消极派,中间摇摆派,

GC 中就不会出现"半个对象"的情况,主要是为了让垃圾回收起来更方便,更简单,记住:垃圾回收的基本单位是"对象",而不是字节

3.4 具体是如何实现垃圾回收的

就分为两个大阶段,第一阶段: 找垃圾/判定垃圾.., 第二阶段: 释放垃圾..

就像打扫房间,先把垃圾都清理到垃圾桶里,然后再统一丢出房间…

3.4.1 如何找垃圾/判定垃圾

我们当下主流的思路有两种方案:
1: 基于引用计数(不是Java中采取的方案,这是别的语言,像Python采取的方案)
2:基于可达性分析(这个是Java采取的方案)
注意别人问你:
1: 谈谈垃圾回收机制中如何判定是不是垃圾
2: 谈谈 Java 的垃圾回收机制中如何判定是不是垃圾
这俩问题可是有坑的,这个基于可达性分析才是 Java 的,可不要别人问 Java的 你说的却是 基于引用计数的

① 基于引用计数

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

当引用计数为 0 的时候,就不在使用,就认为是垃圾,就释放掉内存
在这里插入图片描述

② 基于可达性分析

就是通过额外的线程,定期的针对整个内存空间的对象进行扫描,有一些起始位置(称为 GCRoots),会类似于深度优先遍历一样,把可以访问到的对象都标记一遍.(带有标记的对象就是可达的对象),没有被标记的对象,就是不可达的,也就是垃圾…

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

3.4.2 垃圾回收算法

① 标记 - 清除 算法

标记就是可达性分析的过程,清除就是直接释放内存
在这里插入图片描述
此时如果直接释放,虽然内存是还给了系统,但是我们发现被释放的内存是离散的,并不连续,给我们带来的问题就是"内存碎片"

空闲的内存有很多,如果我们假设内存一共是 1G,如果我们申请 500M 内存,他也是有可能申请失败的(因为申请的 500M 是连续的内存),而每次申请,内存都得是连续的空间,而这里的 1G 空闲内存可能只是"内存碎片",加在一起才有 1G

② 复制 算法

为了解决"内存碎片的问题",引入了复制算法,总体来说,就是"用一半,丢一半"
在这里插入图片描述

直接把不是垃圾的,拷贝到另一半,把原本这个空间整体都释放掉!!
在这里插入图片描述

③ 标记 - 整理 算法

在这里插入图片描述

虽然上面说的都有缺陷,但在 JVM 中的实现,会把多种方案结合起来使用

④ 分代回收 算法

针对把对象进行分类(根据对象的"年龄"分类),一个对象熬过了一轮 GC 扫描,就称为"长了一岁",针对不同年龄的对象,采取不同的方案…

在这里插入图片描述

举报

相关推荐

0 条评论