JavaEE初阶—详解JVM(八股文)

前言

      一般 java 程序员一般情况下是不需要 jvm 内部的东西的,一般是 C++ 程序员来开发 JVM,所以在这里为什么要学,因为这一块是面试中要考的,主要是三个方面:JVM 内存区域划分,JVM 类加载机制,JVM 垃圾回收机制【重点】


目录


一、JVM 内存区域划分

问题一、JVM 的内存区域可以划分为四个区域:

  • ①、程序计数器
  • ②、方法区
  • ③、栈
  • ④、堆

问题二、那为什么要划分出这么多区域 ????

      ▶️ 是为了让不同内存区域去完成不同的功能

问题三、程序计数器

      ▶️ 内存中最小的区域,用来保存下一条要执行指令的地址在哪,指令 ——> 字节码,程序要想运行,JVM 就需要把字节码加载起来,放到内存中去,程序会一条一条的把指令从内存中取出来,放到 CPU 上执行,也就需要随时记住当前执行到哪一条了。为什么需要记住执行到哪一条?因为 CPU 是并发执行的,CPU 并不是给单独一个进程提供服务,要伺候所有进程,正因为操作系统是以线程为单位进行调度执行的,每个线程都得记录自己的执行位置,程序计数器每个线程都有一个

问题四、栈

      ▶️ 存放的是局部变量和方法调用信息,方法调用的时候,每次调用一个新的方法,都会涉及一个入栈操作,每次执行完一个方法,会涉及到出栈的操作

在这里插入图片描述
      ▶️ 当入栈完成后,再依次执行完方法后,按照先入后出的顺序再进行出栈,这里的栈,虽然指的是 JVM 内存中的一部分,但是这里的工作过程是和数据结构中的栈,非常类似。栈里面保存的这些信息被称为栈帧,每个栈帧里面数据是怎么排列的,也有一些规则,入栈出栈是怎样的具体实现的,里面也有一些技巧和细节,这里不做过多介绍( C++ 需要详细研究)。栈的空间其实是比较小的,在 JVM 中可以配置栈空间的大小,但是一般也就几 M 或 几十 M ,因此栈是很有可能会满了的,如果正常写代码,就怕递归并且递归条件没整好……就会出现 Stack Overflow 异常

问题五、堆

      ▶️ 堆只有一份,多个线程公用一个堆,new 出来的对象就是在堆中,对象的成员变量也是在堆中

问题六、判断内置类型变量在栈上,引入类型变量在堆上是否正确 ????

      ▶️ 这样说法是错误的,应该是局部变量在栈上,成员变量在堆上

在这里插入图片描述

问题七、方法区

      ▶️ 存放类对象,.java文件——>.class(二进制字节码),.class会被加载到内存中,也就被 JVM 构造成类对象(加载过程被称为 “类加载” ),这里的类对象就是放在方法区里,类对象就描述了这个类长啥样:类的名字,里面有哪些成员,有哪些方法,每个成员加啥名字,叫啥名字,每个方法叫啥名字,是啥类型,方法里面包含的指令……类对象中还有个很重要的东西,静态成员,static 修饰的成员,称为类属性,普通的成员,叫做实例属性。

总结:

      ▶️ 上述的内存区域划分,不一定是符合实际情况的,JVM 在实现的时候,具体怎么划分这个区域,不一定完全相同,不同厂商,不同版本的 JVM 实现上可能会存在一些差异


二、JVM 类加载机制

      ▶️ 类加载是设计一个运行时环境的一个重要核心功能,此处还是以面试为目的,只学习常见面试问题

问题一、类加载要去完成的核心操作

      ▶️ 将 .class 文件,加载到内存中,构建成类对象
在这里插入图片描述
      ▶️ 上面的类的生命周期,很多资料上都有,其实这个东西来自 java 的官方文档

问题二、类加载,可以分为几个步骤 ????

      ▶️ 类加载可以分为三个步骤,建议回答这个问题的时候回答英文:

  • ①、Loading 环节

      先找到对应的 .class 文件,然后打开(使用字节流打开)并读取 .class 文件,同时初步生成一个类对象,loading 中一个关键环节 .class 文件到底是啥样的,会把读取并解析到的信息,初步填写到类对象中

  • ②、Linking 环节

      ▶️ Linking 环节也可以分为三个环节:

      a、Verification 校验过程:主要就是验证读到的内容是不是和规范中规定的格式完全匹配,如果这里读到的数据格式不符合规范就会类加载失败,并且抛出异常

      b、Preparation 准备阶段:正式为类中定义的变量(即静态变量,被 static 修饰的变量),分配内存并设置类变量初始值的阶段(设置 0 值)

      c、Resolution 解析阶段:java 虚拟机将常量池中的符号引用替换为直接引用的过程,也就是初始化常量的过程。.class 文件中的常量是集体放置的,每个常量都有一个编号。.class 文件的结构里初始情况下只是记录了编号,需要根据编号找到对应内容,填充到类对象中

  • ③、Initializing 初始化阶段

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


三、典型面试题

一、先看这样一个代码会输出怎样的结果:

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Lenovo
 * Date: 2022-06-27
 * Time: 10:34
 */
class A{
    public A(){
        System.out.println("A的构造方法");
    }

    {
        System.out.println("A的构造代码块");
    }

    static {
        System.out.println("A的静态代码块");
    }
}

class B extends A{
    public B(){
        System.out.println("B的构造方法");
    }

    {
        System.out.println("B的构造代码块");
    }

    static {
        System.out.println("B的静态代码块");
    }
}
public class Test extends B{
    public static void main(String[] args) {
        new Test();
        new Test();
    }
}

      ▶️ 运行结果图:

在这里插入图片描述

      ▶️ 首先大的原则:

在这里插入图片描述

      ▶️ 解析:程序从 main 开始执行,main 这里是 Test 方法,因此要执行 main ,就要先加载 Test。Test 继承 B 要加载 Test ,就要先加载 B。B 继承 A 要加载 B ,就要先加载 A。只要这个类被用到就要先加载这个类(实例化,调用方法,调用静态方法,被继承都算被用到)。可以看到前两个是 AB的静态代码块,这是加载 Test 类时候发生的,此时还没有执行 main ,下一步执行具体的 main 方法,要想构造 Test 先构造 B,要想构造 B 先构造 A。对于 A 来说,构造过程 = 构造代码块的执行 + 构造方法的执行,所以 new Test 就会执行第3至第6条指令

二、关于双亲委派模型

      ▶️ 这个环节处于 Loading 阶段(比较靠前),双亲委派模型,描述的就是 JVM 中的类加载器,如何根据类的全限定名(java.lang.String)找到 .class 文件的过程(找文件的过程)

问题一、什么是类加载器

      ▶️ JVM 里面提供了专门的对象,叫做类加载器,负责进行类加载,当然找文件的过程也是类加载器来负责的。.class 文件可能放置的位置有很多,有的可能放置在 JDK 目录里,有的放置到项目目录里,还有放到特定目录里,还有放置在特定位置,因此 JVM 里面提供了多个类加载器,每个类加载器负责一个片区,默认的类加载器主要是三个:

①、BootStrapClassLoader:主要负责加载标准库中的类(String,ArrayList,Random,Scanner……)

②、ExtentsionClassLoader:负责加载一些 jdk 扩展的类(很少用到)

③、ApplicationClassLoader:负责当前项目中目录中的类

④、程序员还可以自定义类加载器:Tomcat 就是自定义的类加载器,用来专门加载 webapps 里面的 .class

问题二、双亲委派模型工作过程

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

①、考虑加载 java.lang.String

  • a、程序启动,先进入 ApplicationClassLoader 类加载器

  • b、ApplicationClassLoader 就会检查下,他的父加载器是否已经加载过了,如果没有,就调用 父 类加载器 ExtentsionClassLoader

  • c、ExtentsionClassLoader 也会检查下,他的父加载器是否已经加载过了,如果没有,就调用 父 类加载器 BootStrapClassLoader

  • d、BootStrapClassLoader 也会检查下,他的父加载器是否已经加载过,自己没有父亲,于是自己扫描自己负责的目录

  • e、 java.lang.String 这个类在标准库中可以找到,直接由 BootStrapClassLoader 来负责后续的加载过程,查找环节就结束了

②、考虑自己写的 Test 类

  • a、程序启动,先进入 ApplicationClassLoader 类加载器

  • b、ApplicationClassLoader 就会检查下,他的父加载器是否已经加载过了,如果没有,就调用 父 类加载器 ExtentsionClassLoader

  • c、ExtentsionClassLoader 也会检查下,他的父加载器是否已经加载过了,如果没有,就调用 父 类加载器 BootStrapClassLoader

  • d、BootStrapClassLoader 也会检查下,他的父加载器是否已经加载过,自己没有父亲,于是自己扫描自己负责的目录,没有扫描到,就返回 子 类加载器继续扫描

  • e、ExtentsionClassLoader 也扫描自己负责的目录,也没有扫描到,回到 子 类加载器继续扫描

  • f、ApplicationClassLoader 也扫描自己的目录,能找到,进行后续加载,查找目录的环节结束

      ▶️ 如果最终 ApplicationClassLoader 也找不到就会抛出 ClassNotFoundException,上述的这一套查找机制,就是双亲委派机制

在这里插入图片描述

问题三、为啥 JVM 要这样设计

      ▶️ 一旦程序员自己写的类和标准库中的类,全限定名重复了,也能够顺利加载标准库中的类

问题四、如果是自定义的类加载器,是否也要遵守双亲委派模型

      ▶️ 可以遵守,也可以不遵守,看需求,比如 tomcat 加载 webapp 中的类,就没有遵守(没啥意义),因为上面三个默认的类加载器无法加载 webapps 中的类,所以没必要再遵守双亲委派机制

总结:

      ▶️ 双亲委派模型,只是 JVM 实现中的一个小小的细节和规则,只不过说这个东西有个好名字才火的,类似的规则和细节在 JVM 是非常多的


四、JVM 的垃圾回收机制

问题一、什么是垃圾回收机制(GC)

      ▶️ 写代码的时候经常会申请内存,创建变量,new 对象,加载类…………俗话说:有借有还,再借不难 ~ 申请内存的时机一般都是明确的,释放内存的时机不是那么清楚(代码里申请一个内存,啥时候不再使用了,也不是那么容易就能确定的,如果内存释放的时机有问题,如果内存还需要用就被丢了,就很难受。这是释放过早,那迟点释放可以吗?这也会引发一些问题)所以内存释放了早或者晚都不行,只能恰到好处的时候释放

      ▶️ C语言的垃圾回收机制,就是由程序员自行决定,因此在 C 语言就存在一个臭名昭著的问题 “内存泄漏”,忘了释放 or 释放过晚,导致内存越用越少,最终无内存可用,内存泄漏问题是 C / C++ 程序员幸福感的头号杀手(有的泄漏块,有的泄漏慢,暴露时机不确定,如果出现难以排查).C++ 不像 C 语言那样爱 “摆烂”,还是想努力拯救一下,C++ 提出一个内存指针的机制,通过内存指针一定程度上可以减小内存泄漏的几率

问题二、java 中的垃圾回收机制

      ▶️ 现在市面上大部分编程语言(java,php,Python,go……)都采取了一个方案就是垃圾回收机制,大概就是由运行时环境(JVM,Python解释器,go运行时)来通过复杂的策略判定内存是否可以回收,并进行回收的动作,垃圾回收,本质上是靠运行时环境,额外做了很多工作,来完成自动释放内存的操作的,让程序员的心智负担大大降低了(不用纠结内存释放时机)

      ▶️ 同时垃圾回收也有其劣势:更消耗额外的开销,可能会影响程序的流畅运行(垃圾回收机制经常会引发 STW 问题 —— stop the world),这也就是 C++ 没有引入 GC 的原因,因为 C++ 有两条高压线:①、和 C 语言兼容,也能和各种硬件,各种操作系统做到最大化的兼容 ②、追求性能的极致

问题三、垃圾回收要回收啥

      ▶️ 堆是最需要进行 GC 的,代码中大量内存都是在堆上的,方法区存放类对象都是类加载来的,进行类卸载就需要释放内存,卸载操作是非常非常低频的操作

在这里插入图片描述
      ▶️ 对于堆中的对象有的是正在使用内存,有的是不再使用内存,有的是一半使用一半不使用,哪个是需要进行回收释放内存的 ???GC 中不会存在半个对象(主要是为了让垃圾回收更简单,垃圾回收的基本单位是对象,而是字节),所以这里只回收不再使用内存的对象。

      ▶️ GC 会提高程序员开发效率,但是会降低程序的运行效率

问题四、垃圾回收具体是怎么回收的:

      ▶️ 两个大的阶段

  • 第一阶段:找垃圾 / 判断垃圾

问题五、怎么找垃圾 ??? 主流有两个思路:

  • ①、基于引用计数(不是 Java 采取的方案,Python 采取的方案)

      ▶️ 每个对象都会引入一小块内存,保存这个对象有多少个引用指向他,好比这样的代码:Test t = new Test();

在这里插入图片描述
      ▶️ 如果再 Test t2 = t 就是又多了一个引用,引用计数变成 2

在这里插入图片描述

      ▶️ 这个内存不在使用就进行释放(引用计数为 0 时)

void func(){
	Test t = new Test();
	Test t2 = t;
}

func();

      ▶️ 调用此方法,创建对象分配内存,方法执行的时候,引用计数是2,当方法结束,t 和 t2都是局部变量随着栈帧一起释放了,这一释放就导致引用计数为 0 (没有引用指向这个对象,也没有代码能够访问这个对象了),此时认为这个对象就是个垃圾。通过引用来决定对象的生死

      ▶️ 引用计数,简单可靠高效,但是有两个致命缺陷:

  • a、空间利用率比较低:每个 new 的对象都需要搭配计数器(假设四个字节),如果对象本身很大,多出四个字节就不算啥,本身对象很小,多出四个字节,相当于空间被浪费了一倍
  • b、会有循环引用的问题:看下面的一个代码
class Test(){
	Test t = null;
}

Test t1 = new Test();
Test t2 = new Test();

      ▶️ 这几句代码产生的内存布局效果:

在这里插入图片描述
      ▶️ 再此基础上再加两句代码:

t2.t = t1;
t1.t = t2;

在这里插入图片描述
      ▶️ 再此基础上再加两句代码:

t1 = null;
t2 = null;

在这里插入图片描述
      ▶️ 此时两个对象的引用计数不为 0 所以无法释放,但是由于引用长在彼此的身上,外界的代码无法访问到这两个对象,此时此刻这两个对象就被孤立了,既不能使用又不能释放,这就出现了内存泄漏问题

  • ②、基于可达性分析(Java 采取的方案)

      ▶️ 通过额外的线程,定期对整个内存空间对象进行扫描,有一些起始位置(称为GCRoots——>栈上的局部变量,常量池中的引用指向的对象,方法区中的静态成员指向的对象),会类似于深度优先遍历一样,把可以访问到的对象都标记一遍(带有标记的星号就是可达对象),没被标记的对象就是不可达的,就是垃圾

在这里插入图片描述

      ▶️ 代码中只要拿到树根结点,就可以掌握所有的结点,树上的任意结点都可以通过 a 直接 / 间接的获取到,GC 在进行可达性分析的时候,当 GC 扫描到 a 的时候,就会把 a 能访问到所有的元素都去访问一遍,并进行标记,如果 c.right = null 那么 f 就不可达,f 就是垃圾,f 应该被回收掉,这就是可达性分析,如果内存对象特别多,这个遍历就会特别慢

      ▶️ 可达性分析的优点,克服了引用计数的两个缺点:循环引用,空间利用率低(没有额外的内存空间),系统开销大,遍历一次可能比较慢

      ▶️ 找垃圾就是核心就是这个对象未来是否还被用到,什么算不用了?没有引用指向了,就不用了

  • 第二阶段:释放垃圾

问题六、回收垃圾的三个基本策略:

  • a 、标记清除

      ▶️ 标记就是可达性分析的过程,清除就是直接释放内存
在这里插入图片描述
      ▶️ 如果这样直接释放,就发现,被释放的内存是分散的(不是连续的),分散开带来的问题是,内存碎片。空闲的内存有很多有很多,假设 1G 如果申请 500 M内存,也可能申请失败,每次申请都是申请的连续内存空间,而 1G可能是多个碎片才加在一起的才 1G,这个问题很影响程序的执行

  • b、复制算法

      ▶️ 为了解决内存碎片,引入复制算法,主要思想:用一半丢一半,如下图:挑 √ 的是垃圾,直接把不是垃圾的拷贝到另外一半,把原来这整个空间都释放掉,可以保证左右内存空间都是连续的,这样内存碎片问题就解决了。复制算法的主要问题:空间利用率低、要保留的多,释放的少,复制开销就很大

在这里插入图片描述
在这里插入图片描述
c、标记整理(针对复制算法再进行改进)

      ▶️ 类似于顺序表删除中间元素,需要有一个搬运的过程,这个方法空间利用率高了,但是没有解决 / 复制元素的开销

在这里插入图片描述
在这里插入图片描述

总结:

      ▶️ 上述方案,虽然可以解决问题,但是都有缺陷,实际 JVM 中的实现会把多种方案都结合起来使用,称之为:“分代回收”,针对对象进行分类,根据对象的年龄进行分类,一个对象熬过一轮 GC 的扫描,就成 “长一岁”,针对不同的年龄对象,采取不同的方案

在这里插入图片描述

问题:对象是怎么在这个内存区域内进行轮转的 ????

  • 1、刚创建出来的对象,放在伊甸区

  • 2、如果伊甸区的对象熬过一轮 GC 扫描,就会被拷贝到幸存区(应用复制算法),根据实际经验,大部分对象都是“朝生暮死”,真正熬过一轮 GC 的对象,并不多

  • 3、在后续的几轮 GC 中,幸存区的对象就在两个幸存区之间来回拷贝(复制算法),每轮都会淘汰掉一部分幸存者

  • 4、进过多轮后,对象终于可以进入老年代,老年代有个特点:里面的每个对象都是比较老的(年龄大的),基本假设一个对象越老继续存活的可能性越大(要死早死 ~ ),因此老年代的 GC 的扫描频率大大低于新生代,老年代使用标记整理的方式进行回收

      ▶️ 分代回收还有一个特殊情况,有一类对象可以直接进入老年代(大对象,占有内存多的对象),大对象拷贝开销比较大,不适合使用复制算法

问题六、垃圾收集器

①、CMS垃圾收集器

      ▶️ 设计的很巧妙,设计的目的就是尽可能的让 STW 时间短

  • a、初始标记:速度很快,会引起端在的 STW (只是找到 GCRoots)

  • b、并发标记:虽然速度慢,可以和业务线程并发执行,不会产生 STW

  • c、重新标记:在 b 中业务代码代码可能会影响并发标记的结果,针对 b 的结果进行微调,虽然会引起 STW 只是微调速度快

  • d、回收内存:也是和业务线程并发执行

②、G1垃圾回收器

      ▶️ 把整个内存,分成了很小的区域 Region,把这些 Region 进行了不同的标记,有的 Region 方新生代对象,有的放老年代对象,然后再进行扫描,一次扫若干个 Region (不追求一轮 GC 就扫描完,分多次来扫),对于业务代码影响是更小的。当下 G1 可以优化到让 STW 停顿时间小于 1ms


总结:

      ▶️ 需要重点掌握的是垃圾回收算法(引用计数 + 可达性分析 + 标记整理 + 复制算法 + 分代回收),这些垃圾收集器简单了解,java11开始 JVM 开始使用 G1 垃圾收集器

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
回答: "Java八股文"是指Java开发中需要掌握的一系列相关知识,包括Java基础知识、Java几何框架、Java多线程、Java虚拟机、MySQL、Spring相关、计算机网络、MQ消息队列、Redis、Nginx等等。这些知识点非常广泛和复杂。\[1\] 关于消息队列,Java中常用的消息服务是JMS(Java Message Service)。JMS是Java的消息服务,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。JMS提供了两种消息模型,分别是点对点模型和发布/订阅模型。在点对点模型中,消息发送者将消息发送到一个队列,消息接收者从队列中接收消息。而在发布/订阅模型中,消息发送者将消息发布到一个主题,多个消息接收者可以订阅该主题并接收消息。\[2\] 总结来说,"Java八股文"是指Java开发中需要掌握的一系列相关知识,包括消息队列。而消息队列是一种用于实现异步消息传输的机制,Java中常用的消息服务是JMS,它提供了点对点模型和发布/订阅模型两种消息模型。\[1\]\[2\] #### 引用[.reference_title] - *1* *3* [java八股文笔记](https://blog.csdn.net/rs_gis/article/details/121151375)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [【Java八股文总结】之消息队列](https://blog.csdn.net/qq_46111316/article/details/127942518)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦の澜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值