• JVM【八股文】


    JVM【八股文】

    JVM内存区域划分

    1. 程序计数器
    2. 方法区

    一块大的区域,需要根据功能,来划分不同的小区域。

    JVM内存是从操作系统里申请来的,之后堆这部分区域进行了划分。

    1.程序计数器

    内存中最小的区域,保存了下一条要执行指令的地址~~

    指令 => 字节码~

    程序要想运行,JVM就得把字节码加载起来,放到内存中,程序就会一条一条把指令从内存中取出来,放到CPU上执行,此时也就需要随时记住,当前执行到哪一条了~

    CPU是并发式的执行程序,CPU不是只给你一个进程提供服务。

    也正因此,操作系统以线程为单位进行调度,每个线程都得记录自己的执行位置,每个线程就都会有一个程序计数器

    2.栈

    局部变量 和 方法调用信息

    方法调用的时候,每次调用一个新的方法,就会涉及“入栈”操作,每次执行完一个方法,就会涉及到“出栈”操作。

    每个线程有一个

    3.堆

    内存中空间最大的区域

    new出来的对象+对象的成员变量 在其中。

    一个进程一个堆

    多个线程共用一个堆

    局部变量在栈上;成员变量和new对象,在堆上。

    4.方法区

    类对象

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

    JVM类加载机制

    1.类加载是要干啥?

    把.class文件,加载到内存中,构成类对象

    在这里插入图片描述

    1. 加载

      先找到对应的 .class文件,然后打开并读取.class文件,同时初步生成一个 类对象

      Loading中的一个关键环节,会把读取并解析到的信息,初步的填写到 类对象中。

    2. 连接

      1. 验证
      2. 准备 ->给静态变量分配内存,并且设置上0值
      3. 解析
    3. 初始化

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

    2.典型面试题

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

    大的原则:

    1. 类加载阶段会进行 静态代码块 的执行。要想创建实例,势必要先进行类加载。
    2. 静态代码块代码块只是在类加载阶段执行一次。
    3. 构造方法和构造代码块,每次实例化都会执行,构造代码块在构造方法前面。
    4. 父类执行在前子类执行在后

    在这里插入图片描述

    注意:只要这个类被调用到,就会先对这个类进行类加载(实例化、调用方法、调用静态方法、被继承等)

    3.关于“双亲委派模型”

    它是 类加载 中的一个环节

    这个环节处于 Loading 阶段的

    • 双亲委派模型,描述的就是JVM中的类加载器,如何根据类的全限定名(java.lang.String)找到.class文件的过程
    • 类加载器:JVM提供了专门的对象,叫做类加载器,负责进行类加载,当然找到文件的过程也是类加载来负责
    • .class文件,可能放在的位置有好多,有的在JJDK目录中,有的在项目目录中,还有的在其他指定位置,因此JVM里面提供了多个类加载器,每个类加载器负责一个片区
    • 默认的类加载器有三个:
      • (1): 引导类加载器(Bootstrap类加载器)
        它是由本地代码(c/c++)实现的,你根本拿不到他的引用,但是他实际存在,并且加载一些重要的类,它加载(%JAVA_HOME%\jre\lib),如rt.jar(runtime)、i18n.jar等,这些是Java的核心类。 他是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
      • (2): 扩展类加载器(Extension类加载器)
        虽说能拿到,但是我们在实践中很少用到它,它主要加载扩展目录下的jar包, %JAVA_HOME%\lib\ext
      • (3): 系统类加载器(System类加载器)
        它主要加载我们应用程序中的类,如Test,或者用到的第三方包,如jdbc驱动包等。
        这里的父类加载器与类中继承概念要区分,它们在class定义上是没有父子关系的。

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

    AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath 。

    而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包。

    JVM的垃圾回收【重点】

    JVM中的垃圾回收机制(GC)

    申请内存的时机一般都是明确的(需要保存某个数据,就需要申请内存),

    但是释放内存的时间,则是不清楚的。

    代码里,创建一个变量(申请一个内存),这个变量啥时候不再使用了?也不是那么容易能确定的

    如果内存释放的时机有问题(内存还想要用,结果就被丢了),此时就很难受了

    上面是内存释放太早了

    要是我迟一点释放行不行呢?就像图书馆,占座~~

    所以内存的释放,早了也不行,晚了也不行,需要恰到好处。

    垃圾回收的劣势:

    • 消耗额外的开销
    • 可能会影响程序的流畅运行(垃圾回收经常会引入STW问题)

    垃圾回收要回收些啥?

    1. 程序计数器:固定大小,不涉及到释放,也就不需要GC
    2. 栈:函数执行完毕,对应的栈帧就自动释放了,也不需要GC
    3. 堆:需要GC,代码中大量的内存都在堆上
    4. 方法区:类对象,类加载来的~~ 进行“类卸载”就需要释放内存,卸载其实是一个非常低频的操作。

    垃圾回收具体咋回收?

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

      1. 基于引用计数(不是Java)

        简单可靠高效 ,但是有两个致命缺陷!

        • 空间利用率比较低
        • 会有循环引用问题
      2. 基于可达性分析(Java)

        起始位置GCroot

        • 栈上的局部变量
        • 常量池中的引用指向的对象
        • 方法区中的静态成员指向的对象

        优点:

        • 克服了引用计数的两个缺点:空间利用率低,循环引用

        自身缺点:

        • 系统开销大,遍历一次可能比较慢

    第二阶段:释放垃圾

    明确了谁是垃圾之后,接下来就是回收(释放)垃圾了

    回收垃圾(释放垃圾)的三种基本策略:

    1. 标记-清除

      标记就是可达性分析的过程

      此时如果直接放掉,虽然内存还是还给了系统,但是被释放的内存是离散的(不是连续的)

      这个问题非常影响程序的执行

    2. 复制算法

      为了解决内存碎片,引入复制算法

      直接把不是垃圾的,拷贝,把原本的空间整体释放

      此时内存碎片问题迎刃而解

      复制算法的问题:

      • 内存空间利用率低
      • 如果要保留的对象多,要释放的对象少,此时复制的开销很大
    3. 标记-整理

      针对复制算法,再进行改进。

      类似于顺序表删除中间元素

      这个方案空间利用率是高了,但是仍然没有解决复制/搬运元素的开销。

    上述方案虽然能解决问题,但是都有缺陷

    实际JVM中的实现,会把多种方案结合起来使用~~

    分代回收!!

    争对对象进行分类(根据对象年龄),一个对象熬过一轮GC扫描,就称涨了一岁

    针对不同的年龄的对象,采取不同的方案!!

    区域划分:

    1. 新生代
      • 伊甸区
      • 幸存区(两个)
    2. 老年代
    • 刚创建出来的对象在伊甸区。
    • 如果伊甸区的对象熬过一轮GC扫描,就会被拷贝到幸存区。(应用了复制算法)
    • 在后续的几轮GC中,幸存区的对象就在两个幸存区之间来回拷贝(复制算法) 每一轮都会淘汰一波幸存者
    • 持续若干轮之后,对象进入老年代(老年代扫描频率降低)

    分代回收中,还有一种情况,有一类对象可以直接进入老年代(大对象,占有内存多的对象)

    大对象拷贝开销大,不适用于复制算法

    上面 找垃圾 释放垃圾都是算法思想,不是具体落地实现

    在JVM中,真正实现上述算法的模块,就是垃圾回收器

    1. Serial回收器:串行回收

      Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。

      Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器。

      Serial收集器采用复制算法、串行回收和"stop-the-World"机制的方式执行内存回收。

      除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial Old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。

    2. ParNew回收器:并行回收

      ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。

      ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。

    3. Parallel回收器:吞吐量优先

      上述是比较老的来及回收器

    4. CMS回收器:低延迟

      • 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
      • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
      • 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
      • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
    5. G1回收器:区域化分代式

      把整个内存,分成了很小的区域,Region

      给这些Region进行了不同的标记

      有的Region放新生代对象,有的放在老年代(不追求依赖GC就扫描完,分多次来扫)对于业务代码影响更小

    重点理解:引用计数+可达性分析+标记清除+标记整理+复制算法+分代回收

    重点记忆:Java11开始,垃圾回收器G1.

  • 相关阅读:
    uni-app —— uni-app的生命周期
    Unity 致社区公开信,调整 runtime fee 政策
    Nginz静态资源缓存
    鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
    uniapp 解决计算时精度丢失问题
    VR虚拟展厅与传统实体展厅相比,有哪些优势?
    从零开始学习软件测试-第44天笔记
    U3D外包开发框架及特点
    Python图像处理初探:Pillow库的基础使用
    电子学会C/C++编程等级考试2023年05月(三级)真题解析
  • 原文地址:https://blog.csdn.net/weixin_53939785/article/details/128056909