JVM的学习

目录

[JVM 运行流程](#JVM 运行流程)

[JVM 执行流程](#JVM 执行流程)

[JVM 运行时数据区(面试重点)](#JVM 运行时数据区(面试重点))

堆(线程共享)

Java虚拟机栈(线程私有)

本地方法栈(线程私有)

程序计数器(线程私有)

方法区(线程共享)

实战(面试类型)

[JVM 类加载](#JVM 类加载)

类加载过程(背)

1) 加载 加载)

2) 验证 验证)

3) 准备 准备)

4) 解析 解析)

5) 初始化 初始化)

双亲委派模型(优先级)

垃圾回收相关(GC)

1.找到垃圾

2.释放垃圾


JVM 运行流程

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

JVM 执行流程

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

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

  1. 类加载器(ClassLoader)

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

  3. 执行引擎(Execution Engine)

  4. 本地库接口(Native Interface)


JVM 运行时数据区(面试重点)

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

堆(线程共享)

1. 堆(Heap)的作用

堆是 JVM 内存管理中最大的一块区域 ,用于存放所有对象实例和数组。它的主要作用是:

  • 存储对象实例 (几乎所有 new 关键字创建的对象都会存放在堆中)。

  • 垃圾回收的主要管理区域(垃圾回收器会在堆上进行内存回收)。

  • 线程共享(堆是 JVM 运行时数据区中所有线程共享的内存区域)。

2. 堆主要存放的是:

  1. 对象实例 (如 **new**创建的对象)。

  2. 数组(JVM 在堆中分配数组对象)。

  3. 对象的成员变量 (除 static 变量外的成员变量)。

  4. 运行时常量池(存储编译期确定的常量)。

  5. 类元数据(方法区的一部分) (如 Class 对象的实例)


Java虚拟机栈(线程私有)

Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。

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

  2. 操作栈 :每个方法会生成一个先进后出的操作栈(用于执行方法时的中间计算 ,类似于 CPU 的寄存器)

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

  4. 方法返回地址:记录方法调用完成后,应该返回的地址,即调用方法的下一条指令地址


本地方法栈(线程私有)

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


程序计数器(线程私有)

程序计数器的作用 :用来记录当前线程执行的行号的。

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址

如果正在执行的是一个Native方法,这个计数器值为空。

程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!


方法区(线程共享)

方法区的作用:用来存储被虚拟机加载的类信息常量静态变量、即时编译器编译后的代码等数据的。

运行时常量池

运行时常量池是方法区的一部分,存放字面量符号引用
字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符

方法区存储的内容:


实战(面试类型)

题型:给出一个代码,问你哪个变量,事处于内存中那个位置?

示例代码:

java 复制代码
class Test{
    public int n = 100;
    pubilc static int a = 10;
}
void main(){
    Test t = new Test();
}

1.new Test(): 这个对象是new出来的,所以这个变量在堆中。

2.n: n这个变量是被包含在new Test()中的,所以也在堆中

3.t: 但是t这个变量并不是对象,它是一个局部变量 ,所以存储在栈帧 中的局部变量表 中,因此t是存储在栈中

4.a :a这个变量是类中静态变量 ,存储在元数据区类元信 息中,也就是在方法区中

总结:局部变量处于栈中;成员变量处于堆中;静态变量处在方法区


JVM 类加载

类加载过程(背)

对于类加载来说总共分为以下几个步骤:

1) 加载

加载:找到**.class文件**,打开文件,读取文件的内容

2) 验证

验证:.class文件是一个二进制的格式.(某个字节都是有某些特殊含义的),就需要验证你当前读到的这个格式是否符合要求。

3) 准备

准备:给类对象分配一个内存空间(最终目标,是要构造出类对象)【这里只是分配内存空间,还没开始初始化.此时这个空间上的内存数值,就全是0(此时如果尝试打印类的static成员,就全是0的)】

4) 解析

解析:针对类对象中包含的字符串常量进行处理,进行一些初始化的操作。【Java代码中用到的字符串常量,在编译之后,也会进入到.class文件中。】

复制代码
final String s = "test";

如上代码,.class文件的二进制指令中,也会有一个s这样的引用被创建出来。但是由于引用本质保存的是一个变量的地址,在.class文件中,这是文件,是不涉及到内存地址的。因此在.class文件中,s的初始化语句,就会先被设置成一个"文件偏移量",通过偏移量,就能找到"test"这个字符串所在的位置。当我们这个类真正得被加载带内存中的时候,再把这个偏移量替换回真正的内存空间。

5) 初始化

初始化:针对类对象进行初始化,把类对象中需要的各个属性都设置好【需要初始化好static成员,需要执行静态代码块,加载父类(可能)】

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


双亲委派模型(优先级)

双亲委派模型是属于**"类加载"**中的一个环节"加载"过程中,其中的一个环节,负责根据全限定类名,找到.class文件。

类加载器(JVM中的一个模块):

1.BootStrap ClassLoader(爷)

2.Extension ClassLoader(父)

3.Application ClassLoader(子)

类加载的过程(找.class文件的过程):

1.给定一个类的全限类名,形如java.lang.String

2.从BootStrap ClassLoader作为入口,开始执行查找的逻辑

3.Application ClassLoade r,不会立即区扫描自己负责的目录(负责的是搜索项目当前目录的第三方库对应的目录),而是查找的任务交给它的父亲,Extension ClassLoader

4.Extension ClassLoader,也不会立即区扫描自己负责的目录(负责的是JDK中一些扩展的库,对应的目录),而是把查找的任务交给它的父亲BootStrap ClassLoader。

5.BootStrap ClassLoader (,也不想立即扫描自己负责的目录(负责的是标准库的目录),也想把他的任务交给它的父亲,结果发现自己没有父亲。因此BootStrap ClassLoader(只能亲自负责扫描标准库的目录。

java.lang.String 这种能在标准库中找到对应的.class文件,就可以进行打开文件,读取文件...此时查找.class文件的过程就结束了。但是,如果给定的类不是标准库的类,任务仍然会被交给孩子来执行。

6.没有扫描到,就会回到Extension ClassLoaderExtension ClassLoader 就会扫描负责的扩展库的目录。如果找到,就执行后续的类加载操作,此时加载过程就结束了。如果没有找到,还是把这个任务交给孩子来执行。

7.如果没有扫描到,就会回到Application ClassLoade r,Application ClassLoade r就会负责扫描当前项目和第三库的目录。如果找到就执行后续的类加载操作。如果没有找到,就会抛出一个ClassNotFoundException


垃圾回收相关(GC)

GC回收的目标其实就是内存中的对象 【在Java中,即为new出来的这些对象】

而栈里的局部变量 ,是跟随着栈帧的生命周期走的.(方法执行结束,栈帧销毁,内存自然释放)

静态变量 ,生命周期就是整个程序。这和始终存在就意味着静态变量是无需释放的

因此,真正需要GC释放 的就是堆上的对象。

两大步骤:

如果问"请你介绍垃圾回收",那么你就可以介绍引用计数。如果让你介绍"Java的垃圾回收"就不需要介绍引用计数了。

1.找到垃圾

1)引用计数【Python、PHP】

new出来的对象,单独安排一块空间,来保存一个计数器。保存引用计数,描述这个对象有几个引用指向它。【在Java中,使用对象,必须依靠引用】如果一个对象,没有引用指向了,就可以视为垃圾了(即引用计数为0);

存在问题:

1.比较浪费内存

2.引用技术机制,存在**"循环引用"**问题

2)可达性分析【Java】

可达性分析本质上是时间换空间这样子的手段,有一个/一组线程,周期性扫描我们代码中的所有对象 。从一些特定的对象出发,尽可能得进行访问得遍历,把所有能够访问到得对象,都标记成**"可达"** ,反之,经过扫描之后,未被标记得对象就是**"垃圾"**了。

可达性分析的出发点 有很多,不仅仅是所有的局部变量,还有常量池中引用的对象,还有方法区中的静态引用的变量......这些都统称为GCRoots;

2.释放垃圾

三种基本的思路

1.标记清除: 比较简单粗暴的释放方式(但是会产生很多内存碎片

释放内存 目的是为了让别的代码能够申请,申请内存就是申请到"连续"的内存空间】

**2.复制算法:**通过复制的方式,把有效的对象,归类到一起,再统一释放剩下的空间.

缺点:

1)内存要浪费一半,利用率不高

2)如果有效的对象非常多,拷贝开销就会很大

**3.标记整理:**既能够解决内存碎片的问题,又能处理复制算法中利用率 的问题

类似于顺序表 删除元素的搬运操作

缺点:搬运的开销还是很大

实际上,JVM采取的释放思路,是上述基础思路的结合体。

**4.分代算法:**分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。这就好比中国的一国两制方针一样,对于不同的情况和地域设置更符合当地的规则,从而实现更好的管理,这就时分代算法的设计思想。

当前 JVM 垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

面试题 : 请问了解Minor GC和Full GC么,这两种GC有什么不一样吗?


相关推荐
张张张31226 分钟前
4.1学习总结 拼图小游戏+集合进阶
java·学习
RadNIkMan1 小时前
Python学习(二)操作列表
网络·python·学习
yanxy5121 小时前
【TS学习】(15)分布式条件特性
前端·学习·typescript
lalapanda2 小时前
UE5学习记录 part13
学习·ue5
高林雨露2 小时前
Java对比学习Kotlin的详细指南(一)
java·学习·kotlin
齐尹秦3 小时前
HTML5 Web Workers 学习笔记
笔记·学习
DarkBule_3 小时前
零基础驯服GitHub Pages
css·学习·html·github·html5·web
余多多_zZ3 小时前
鸿蒙学习手册(HarmonyOSNext_API16)_应用开发UI设计:Swiper
学习·ui·华为·harmonyos·鸿蒙系统
淬渊阁4 小时前
汇编学习之《扩展指令指针寄存器》
汇编·学习
lalapanda4 小时前
UE5学习记录part12
学习·ue5