JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。 虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。 常见的虚拟机:JVM、VMwave、Virtual Box。 JVM和其他两个虚拟机的区别:
JVM是一台被定制过的现实当中不存在的计算机。
JVM是Java运行的基础,也是实现一次编译到处执行的关键 JVM的执行流程: 程序在执行之前要把Java代码转换成字节码(class文件),JVM首先需要把字节码通过一定的方式类加载器(ClassLoader)把文件加载到内存中的运行时数据区(Runtime Data Area),而字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解释器执行引擎将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其它语言的接口本地库接口来实现整个程序的功能,这就是这4个主要组成部分的职责与功能 Java 程序从代码到执行,就像是建造一座大厦。编译器把设计图纸(Java 代码)转化成施工蓝图(字节码),类加载器把蓝图放进仓库(内存),执行引擎把蓝图上的专业术语翻译给工人(CPU),调用本地库接口就像是从外部获取特殊的材料和工具,最终让大厦(程序)得以建成并正常运作。

在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。

线程私有的:
线程共享的:
在一个Java进程中,元数据和堆只有一份(同一个进程中的所有线程都是共用一份数据的) 程序计数器和栈则可能有多份(当一个Java进程中有多个线程的时候,每个线程都有自己的程序计数器和栈) 线程就代表一个"执行流",每个线程就需要保存自己的"程序计数器",记录自己的调用关系
程序计数器是一块较小的内存空间,可以看作是当前线程所指向的字节码的行号指示器.字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 作用:
Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。 方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。 栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

局部变量表 方法运行时的 “临时储物柜”,专门用来放方法里用到的各种临时数据,而且这些数据的类型在编译时就已经确定好了 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
boolean(布尔值,真 / 假)、byte(字节)、char(字符)、short(短整数)、int(整数)、float(单精度浮点数)、long(长整数)、double(双精度浮点数)。
比如方法里写 int a = 5; float b = 3.14f;,这两个变量 a 和 b 就存在局部变量表里,需要用的时候直接从这里拿。
String str = new String("hello");,这里的 str 就是引用,它可能是:
操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。 方法执行时的 “临时工作台”,专门用来放计算过程中用到的 “数字” 和 “中间结果”,就像咱们做算术题时在草稿纸上写写画画的过程
动态链接 主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。 假设你要找 “隔壁部门那个戴眼镜的小张”—— 这是个 “符号引用”(用特征描述一个人,但不知道具体座位)。 但你不知道他具体在哪,于是问前台,前台告诉你 “他在 3 楼 302 办公室靠窗的位置”—— 这就变成了 “直接引用”(精准地址)。 你按这个地址就能找到人,这个 “问前台→拿到精准地址” 的过程,就类似动态链接。

程序运行中栈可能会出现两种错误:
StackOverFlowError: 如果栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。OutOfMemoryError: 如果栈的内存大小可以动态扩展, 那么当虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。本地方法栈 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
public void sayHello()),编译后变成字节码(.class 文件里的指令),由 JVM(虚拟机)直接执行。native 关键字的方法(比如 public native void start()),它的代码不是 Java 写的(可能是 C、C++),JVM 自己执行不了,得调用操作系统或其他底层语言的代码。Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。 类加载的 “三步走”:读文件→解析→存方法区 简单说,方法区就是虚拟机的 “类档案库”,存着类的 “基因蓝图”(结构、变量、方法等),所有用到类的地方,都从这里查 “说明书”,保证程序能正确创建对象、调用方法~
Java程序被编译为.class文件后存储在硬盘上。
当运行Java程序时,JVM需要读取.class文件的内容并执行其中的指令。这个过程称为类加载,即将类相关的字节码从硬盘加载到内存中的元数据区。
类加载生成类对象,把硬盘上的 .class 文件,变成 JVM 内存里可用的类
类对象的两个关键作用:
反射(Reflection):通过类对象,在运行时 “动态操作类的信息”(比如创建对象、调用方法、访问字段),而不需要提前写死代码
多线程中给静态方法加锁(synchronized 修饰静态方法):静态方法属于 “类” 而不是 “对象”,所以用 synchronized 修饰静态方法时,锁的是 类对象(而不是某个实例对象)
public class User {
public static synchronized void login() {
// 锁的是 User.class 这个类对象
}
}
多个线程调用 User.login() 时,会竞争 “类对象的锁”,保证同一时间只有一个线程能执行这个方法,避免多线程混乱。
补充: 类锁 vs 对象锁
静态方法的 synchronized,就像 “全网点共用的大门锁”—— 不管多少人来,同一时间只能放一个人进 ATM 机;而实例方法的 synchronized,像 “每个用户自己的小房间锁”—— 各自锁各自的,互不影响。
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
这 7 个阶段的顺序如下图所示

把 .class文件找到,代码中先见到类的名字,然后进一步的找到对应的 .class 文件(设计一系列的目录查找过程),打开并读取文件内容
验证读到的 .class 文件的数据是否正确,是否合法 验证阶段主要由四个检验阶段组成:

文件格式验证这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。
根据刚才读取到的内容,确定出类对象需要的内存空间,申请这样的内存空间,并且把内存空间当中的所有内容都初始化为0
针对类中的字符串常量进行处理 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量 符号引用是 “名字”(写在文件里的符号),直接引用是 “实际地址”(内存里的定位)。解析阶段就是 “把名字换成地址”,让程序运行时能直接找到目标,就像用导航把 “商场名字” 换成 “精准定位”,跑起来更快!
针对类对象做最终的初始化操作,执行静态成员的赋值语句
static int a = 10;,真正把 10 赋值给 a);static {...} 里的逻辑);类加载就是:取快递(加载)→ 验货(验证)→ 腾货架(准备)→ 查字典找位置(解析)→ 摆商品 / 执行逻辑(初始化),最终把
.class文件变成 JVM 能用的类,随时创建对象、调用方法
类加载五个步骤中第一个步骤中的一个环节 规定了类加载器如何协作加载 Java 类,主要目的是确保 Java 核心类的安全性和唯一性。简单来说,就是 “遇到类加载请求时,先让父加载器尝试加载,父加载器无法加载时才由自己加载”。 拿着类的全限定名(类似java.lang.String,包名+类名),通过 “父加载器优先尝试” 的规则,最终找到并加载对应的
.class文件的过程 核心结构(三层类加载器)

Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。 Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。 程序计数器和栈都是跟随线程的不需要GC,元数据区中的类对象要进行类加载,不会出现无限增长的情况
垃圾回收是以对象为维度进行回收的

GC具体是怎么回收的?
给对象中添加一个引用计数器:
存在的问题:


Java采用的方案 用时间来换空间 再JVM中专门高了一波周期性的线程来扫描代码中所有的对象,判断某个对象是否是"可达"(可以被访问到),对应的不可达的对象就是垃圾了
基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

哪些对象可以作为GC root
直接针对内存中的对象进行释放

这样的做法会引入"内存碎片问题",释放的对象是随机的而不是连续的,虽然把上述内存释放掉了,但是整体的空闲内存并没有连在一起,后续申请内存的时候就可能申请不了(申请的内存一定是连续的)
为了解决标记清除算法的效率和内存碎片问题,复制收集算法就出现了,它可以将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉,这样就使每次的内存回收都是对内存区间的一半进行回收

缺点
是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

JVM根据对象的年龄把对象进行区分,分为新生代和老年代 对象的年龄,靠 “可达性分析 + 存活次数” 计算:

延伸面试问题: HotSpot 为什么要分为新生代和老年代? 当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
分代回收是JVM的GC中的基本思想方法,具体落实到了JVM的实现层面上,JVM还提供了多种"垃圾回收器"
CMS收集器的运作步骤 把 GC(垃圾回收)想象成 “家里大扫除”,root 对象就是 “必须留着的重要东西”(比如冰箱、电视),其他东西是不是垃圾,得看和这些重要东西有没有关联




优点
缺点:



