JVM的学习

发布于:2025-04-01 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

JVM 运行流程

JVM 执行流程

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

堆(线程共享)

 Java虚拟机栈(线程私有) 

本地方法栈(线程私有)

程序计数器(线程私有)

方法区(线程共享)

实战(面试类型)

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常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符

方法区存储的内容:


实战(面试类型)

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

示例代码:

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 ClassLoader,不会立即区扫描自己负责的目录(负责的是搜索项目当前目录的第三方库对应的目录),而是查找的任务交给它的父亲,Extension ClassLoader

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

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

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

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

7.如果没有扫描到,就会回到Application ClassLoader,Application ClassLoader就会负责扫描当前项目和第三库的目录。如果找到就执行后续的类加载操作。如果没有找到,就会抛出一个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有什么不一样吗?



网站公告

今日签到

点亮在社区的每一天
去签到