2.JVM

JVM.png

(一)初识 JVM

(1)什么是 JVM

JVM 全称是 Java Virtual Machine,中文译名 Java虛拟机。
JVM 本质上是一个运行在计算机上的程序,他的职责是运行Java字节码文件

HelloWorld.java(源代码文件) ———使用javac编译———>
HelloWorld.class(Java字节码文件) ———使用java运行———>
Java虚拟机(将字节码指令实时解释为当前平台的机器码) ————> 机器码

(2)JVM 的功能

  1. 解释和运行 :对字节码文件中的指令,实时解释成机器码,让计算机执行
  2. 内存管理 :自动为对象、方法等分配内存;自动的垃圾回收机制,回收不再使用的对象
  3. 即时编译 :对 热点代码 (很短的时间内被多次调用)进行优化,提升执行效率(在运行时候编译成机器码)
  • Java需要实时解释,主要是为了支持跨平台特性;
  • 由于JVM需要实时解释虚拟机指令,不做任何优化性能不如直接运行机器码的C、C++等语言;
  • JVM提供了即时编译(Just-In-Time 简称JIT)进行性能的优化,最终能达到接近C、C++语言的运行性能,甚至在特定场景下实现超越。
  • 原理:字节码文件中有一段字节码指令,虚拟机如果发现这段代码是热点代码,就主动将这段代码进行优化,并解释成计算机可以执行的机器码,将这个机器码保存在内存,这样就可以在第二次执行的时候将这个机器码从内存中取出,直接进行调用,这样就省略了一次解释的步骤,提高了性能。

(3)Java虚拟机的组成

1.类加载器 ClassLoader —— 加载Class字节码文件中的内容到内存中
2.运行时数据区域(JVM管理的内存) —— 负责管理Jvm使用到的内存,比如创建对象和销毁对象(存放类、对象的内存区域)
3.执行引擎(即时编译器、解释器、垃圾回收器等) —— 将字节码文件中的指令解释成机器码,同时使用即时编译器优化性能
4.本地接口 —— 调用本地已经编译的方法,比如虚拟机中提供的c/c++的方法(执行引擎负责本地接口调用,同时本地接口也会创建对象)

(4)常见的 JVM

Hotspot(Oracle JDK版) Oracle 所有版本 高(闭源) 使用最广泛,稳定可靠,社区活跃 JIT支持 OracleIDK默认虚拟机

(二)字节码文件详解

推荐使用 jclasslib 工具查看字节码文件
安装插件:
查看方式:View -> Show Bytecode With Jclasslib

(1)字节码文件的组成

  1. 基本信息 —— 魔数、字节码文件对应的Java版本号访问标识(public final等等)父类和接口

    • Java字节码文件中,将文件头称为magic魔数(不同的文件类型对应不同的文件头)
  2. 常量池 —— 保存了字符串常量、类或接口名、字段名,这些都主要在字节码指令中使用

    • 字节码文件中常量池的作用:避免相同的内容重复定义,节省空间。
  3. 字段 —— 当前类或接口声明的字段信息

  4. 方法 —— 当前类或接口声明的方法信息,字节码指令

    • 操作数栈(临时存放)

    • 局部变量表(局部变量存放位置)

    1
    2
    int i = 0;
    int j = i + 1;

    字节码:
    iconst_0 将常量0放入操作数栈
    istore_1 从操作数栈取出放入局部变量表1号位置
    iload_1 将局部变量表1中的数据放入操作数栈
    iconst_1 将常量1放入操作数栈
    iadd 将操作数栈顶部的两个数据进行累加,结果放入栈中
    istore_2 从操作数栈取出放入局部变量表2号位置
    return 方法结束,返回

    iconst 常量放到操作数栈(到操作数栈)
    istore 从操作数栈放入局部变量表中指定位置(=赋值 拉下来)
    iload 从局部变量表数据放入操作数栈(推上去)
    最终结果看局部变量表

  5. 属性 —— 类的属性,比如源码的文件名内部类的列表等

(2)类的生命周期

类的生命周期描述了一个类加载、使用、卸载的整个过程
生命周期概述
加载、连接、初始化、使用(使用new关键字创建对象 反射操作)、卸载(垃圾回收篇中)

①加载阶段

概括: 类加载器将类的字节码信息加载到内存,并在方法区堆区各分配一个对象,方法区生成 instanceKlass 对象,堆区会生成class类的对象,静态字段主要是放在堆区里。

  1. 加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。程序员可以使用Java代码拓展的不同的渠道
    (将字节码信息加载到内存中)
    类和接口文件来自渠道:
  • 本地文件(磁盘上的字节码文件)

  • 动态代理生成(程序运行时使用动态代理生成)

  • 通过网络传输的类

  1. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息(基本信息、常量池、字段、方法)保存到方法区中
    生成一个 instanceKlass 对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
    (将字节码中的信息保存到方法区中)
  • 方法区只是java虚拟机规范中设计出来的虚拟概念
  1. 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的 java.lang.Class 对象。
    作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)
    (即方法区和堆区创建对象) 方法区内容.png 方法区和堆区.png

②连接阶段

即校验和前期的准备工作
可以分为仨阶段:

  1. 验证: 验证内容是否满足《Java虚拟机规范》 魔数、版本号等验证,一般不需要
    程序员关注。

  2. 准备: 给静态变量分配内存赋初值(虽然在加载阶段,Java虚拟机给对象分配了内存,但是静态变量还未处理)

    • 而每一种基本数据类型和引用数据类型都有其初始值
    • final修饰的基本数据类型的静态变量准备阶段直接会将代码中的值进行赋值
  3. 解析: 将常量池中的符号引用替换为指向内存的直接引用

    • 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据

此阶段完成之后类的信息已经被加载到内存中

③初始化阶段

初始化阶段最重要,程序员可以干涉

初始化阶段会执行静态代码块中的代码,并为静态变量赋值
初始化阶段会执行字节码文件中clinit部分的字节码指令。

以下几种方式会导致类的初始化:

  1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化(在连接阶段就已经赋值好了)。
  2. 调用Class.forName(String className)。 是 Java 中的一个静态方法,常用于在运行时根据字符串表示的类名加载类,而不是在编译时就已经确定了类的类型。
  3. new一个该类的对象时。
  4. 执行Main方法的当前类。

一个类被加载并初始化一般来说只会执行一次,而对于构造方法,由于可以创建多个对象,每次构造都会执行一次(先执行代码块,再执行构造方法中的代码)

clinit 指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的。

  1. 无静态代码块且无静态变量赋值语句。
  2. 有静态变量的声明,但是没有赋值语句。
    • public static int a;
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
    • public final static int a= 10;

有继承关系的情况:
直接访问父类的静态变量,不会触发子类的初始化。
子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

注意:
数组的创建不会导致数组中元素的类进行初始化。
final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化。

(3)类加载器 ClassLoader

是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。

类加载器接收二进制流,接收到这些数据之后会执行JNI(本地接口),调用Java虚拟机中的方法,这些方法使用C++编写

①类加载器的分类

类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的

  1. Java虚拟机底层源码实现的

    • 加载基础类,比如java.lang.String,确保类被加载的可靠性
    • Bootstrap 启动类加载器 —— 加载Java中最核心的类
    • Bootstrap 是由 Hotspot 虚拟机提供的/使用c++编写的
  2. Java代码中实现的

    • 所有Java中实现的类加载器都需要继承自 ClassLoader

    • Extension 扩展类加载器 —— 允许扩展Java中比较通用的类

    • 加载Java安装目录/jre/lib/ext下的类文件

    • Application 应用程序类加载器 —— 加载应用使用的类

    • 加载classpath下的类文件 包括自己的项目中编写的类和接口的文件 以及第三方jar包的文件

②双亲委派机制(核心考点)

类加载器的双亲委派机制:
由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题。

一个类在被加载过程中的两点最基本的要求:

  1. 保证类加载的安全性(例如:恶意代码替换JDK中的核心类库。确保核心类库的完整性和安全性)
  2. 避免重复加载(避免一个类被多次加载)

双亲委派机制指的是:
当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。

  • 向上查找如果已经加载过,就直接返回Class对象,加载过程结束。这样就能避免一个类重复加载

  • 如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载。所以看上去是自顶向下尝试加载。

  • 向下委派加载起到了一个加载优先级的作用(启动类加载器优先级最高)

    Bootstrap 启动类加载器

    Extension 扩展类加载器

    Application 应用程序类加载器

③打破双亲委派机制

为什么要打破双亲委派机制
如何打破?

三种方式:

  1. 自定义类加载器
  2. 线程上下文类加载器
  3. Osgi框架的类加载器(了解即可)

④JDK9之后的类加载器

由于JDK9引入了module的概念,类加载器在设计上发生了很多变化。
1.启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。
2.扩展类加载器被替换成了平台类加载器(Platform Class Loader)

(三)JVM 的内存区域(运行时数据区)

运行时数据区:
运行时数据区.png
Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区
《Java虚拟机规范》中规定了每一部分的作用。

(1)程序计数器

线程不共享的,每一个线程都有自己的程序计数器

程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址。

作用:

  1. 指向当前执行的字节码地址:当线程执行Java字节码时,程序计数器记录下当前正在执行的指令地址,从而在执行过程中可以准确地跳转到正确的指令
  2. 支持线程切换:由于每个线程都有自己的程序计数器,JVM可以方便地在多个线程之间进行切换,每次切换都能准确地恢复到线程之前执行的位置,确保程序的正确性和一致性。(线程来回切换时,用来保存接下来要执行的指令,方便线程继续执行)
  3. 实现多线程的控制流:在多线程执行时,程序计数器能够指明各个线程的执行位置,因此可以支持线程的暂停和恢复,强化了多线程的执行机制。
  4. 处理异常和返回:在方法调用和异常处理过程中,程序计数器也起到了重要的作用,它可以帮助记录方法调用前的位置,确保异常处理或方法返回时能正确无误地跳转到合适的指令。

内存溢出指的是程序在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。
因为每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。
程序员无需对程序计数器做任何处理。(一切由JVM掌控)

(2)栈(栈只是存引用)

Java虚拟机栈: 保存在java中实现的方法,每次执行方法,都会把这个方法的信息往栈里保存。
本地方法栈: 在方法上加上native本地关键字的、用c++实现的方法

  • 每个线程都有一个私有的栈,用于存储局部变量方法调用操作数栈等。
  • 栈是线程私有的,生命周期与线程相同。
  • 栈帧(Stack Frame)是栈的基本单位,每个方法调用都会创建一个栈帧。

Java虚拟机栈: (java Virtual Machine stack)采用 栈的数据结构 来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个 栈帧 (Stack Frame)来保存。

每个线程在创建时都会有一个独立的虚拟机栈。这个栈用于管理该线程的所有方法调用。
栈帧则是在这个栈中用于具体存储每个方法调用信息的单位。

  • Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。
  • 由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈。

栈帧sloat的组成(了解即可)

  • 每个栈帧都是4bytes
  1. 局部变量表 —— 在运行过程中存放所有的局部变量,编译成字节码文件时就可以确定局部变量表的内容。

    • 栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),long和double类型占用两个槽,其他类型占用一个槽。

    • 实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。

    • 局部变量表保存的内容有:
      ①实例方法的this对象(当前实例对象的引用),
      ②方法的参数,
      ③方法体中声明的局部变量。

    • 为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。

  2. 操作数栈 —— 栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域

  3. 帧数据 —— 主要包含动态链接、方法出口、异常表的引用

    • 动态链接:当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系

    • 方法出口:方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址

    • 异常表的引用:异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。

(3)堆(堆存对象)

  • 堆是Java内存中最大的一块,用于存储对象实例数组
  • 堆是所有线程共享的内存区域,是垃圾回收器管理的主要区域。
  • 堆可以进一步分为新生代(Young Generation)和老年代(Old Generation),新生代又分为Eden区、Survivor区(From和To)。

一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。

堆内存是会溢出的,抛出 OutOfMemory 的错误

堆空间有三个需要关注的值,used total max。
used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。

(4)方法区

  • 存储类信息、常量、静态变量、即时编译器编译后的代码等

方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。

方法区是存放基础信息的位置,线程共享,主要包含三部分内容:

  1. 类的元信息 —— 保存了所有类的基本信息
  2. 运行时常量池 —— 保存了字节码文件中的常量池内容
    • 字节码文件中通过编号查表的方式找到常量,这种常量池称为**静态常量池**;
    • 当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为**运行时常量池**;
    • 静态常量池经过加载+连接之后,成为运行时常量池
  3. 字符串常量池 —— 保存了字符串常量

方法区存放的位置:

  1. JDK7 将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数-xx:MaxPermsize=值来控制。
  2. JDK8 将方法区存放在直接内存中的元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用-x:MaxMetaspacesize=值将元空间最大大小进行限制。

字符串常量池

字符串常量池 or 堆内存

String s1 = new String("abc");
这个字符串对象是通过 new 关键字创建出来的,”abc”对象会放在堆内存中,
在栈内存中使用s1这个局部变量,保存堆内存中的地址

String s2 ="abc";
并没有使用 new 关键字创建新的对象,所以s2这个局部变量里存放的是字符串常量池中的”abc”的地址

String.intern()方法是可以手动将字符串放入字符串常量池中

  • JDK6 版本中 intern()方法会把第一次遇到的字符串实例复制永久代的字符串常量池中,返回的也是永久代里面这个字符串实例的引用。JVM启动时就会把java加入到常量池中。
  • JDK7 及之后版本中由于字符串常量池在堆上,所以 intern()方法会把第一次遇到的字符串的
    放入字符串常量池。

问题:静态变量存储在哪里呢?
静态变量的存放位置和JDK存放版本有关。
JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。
JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。

(5)直接内存

直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。
直接内存的作用:

  • 直接内存的出现,只要是为了在NIO使用过程中,减少对用户使用的影响,以及提升写文件读文件的效率。
  • 在JDK8之后用来保存方法区中的数据

(四)JVM 的垃圾回收

自动垃圾回收(GC)

Java中为了简化对象的释放,引入了自动的垃圾回收 (Garbage Collection简称GC) 机制
通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对上的内存进行回收。

自动垃圾回收:
自动根据对象是否使用由虚拟机来回收对象。
优点: 降低程序员实现难度、降低对象回收bug的可能性
缺点: 程序员无法控制内存回收的及时性
手动垃圾回收:
由程序员编程实现对象的删除
优点: 回收及时性高,由程序员把控回收的时机
缺点: 编写不当容易出现悬空指针、重复释放、内存泄漏等问题

  1. 解决系统僵死的问题(大厂系统出现的许多系统僵死问题都与频繁的垃圾回收有关)(程序还在运行中,但是用户去访问这个程序得不到回应)

  2. 性能优化(对垃圾回收器进行合理的设置可以有效地提升程序的执行性能)

  3. 高频面试题

    • 常见的垃圾回收器

    • 常见的垃圾回收算法

    • 四种引用

    • 项目中用了哪一种垃圾回收器

(1)方法区的回收

线程不共享的部分,都是伴随着线程的创建而创建,线程的销毁而销毁。
而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存。

方法区中的类到底是怎么被回收的

判定一个类可以被卸载。需要同时满足下面三个条件:
1、此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
2、加载该类的类加载器已经被回收。
3、该类对应的java.lang.Class 对象没有在任何地方被引用。

(2)堆回收

堆一般是整个java程序中最大的一部分,里面包含了很多对象,如何判断哪些对象需要被回收,哪些不需要被回收?

①引用计数法和可达性分析法

如何判断堆上的对象是否可以回收?
Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。

如何判断堆上的对象有没有被引用?
方法一:引用计数法:
会为每个对象维护一个引用计数器,当对象被引用的时加1,取消引用时减1。当某个对象的引用计数变为零时,说明没有任何指针指向该对象,即它不再被使用,可以安全地释放其占用的内存。

引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法,但是它也存在缺点,主要有两点:
1.每次用和取消引用都需要维护计数器,对系统性能会有一定的影响
2.存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题


方法二:可达性分析:※※※
Java使用的是可达性分析算法来判断对象是否可以被回收。
(一个对象如果在它的引用链上没有 GCRoot 对象就可以被回收)

可达性分析将对象分为两类:
垃圾回收的根对象(GCRoot)普通对象,对象与对象之间存在引用关系。

  • 在 Java 中,GC Root 对象是垃圾回收(Garbage Collection, GC)的起点。垃圾回收器通过从 GC Root 对象开始遍历对象引用链,标记所有可达的对象,未被标记的对象则会被回收。

  • GCRoot对象正常情况下不会被回收,如果普通对象能够通过引用链找到它对应的GCRoot对象也不能被回收


哪些对象被称之为 GC Root 对象呢?

(一)虚拟机栈中的局部变量——当前正在执行的方法中的局部变量引用的对象

  • public void method() {
        Object obj = new Object(); // obj 是 GC Root
    }
    
    1
    2
    3
    4
    5

    (二)**方法区中的静态变量** ——类的静态变量引用的对象

    - ```java
    static Object staticObj = new Object(); // staticObj 是 GC Root

(三)方法区中的常量——常量池中的常量引用的对象。

  • static final String CONSTANT = "constant"; // CONSTANT 是 GC Root
    
    1
    2
    3
    4
    5

    (四)**本地方法栈中的 JNI 引用**——通过 JNI(Java Native Interface)调用的本地方法中引用的对象。

    - ```java
    public native void nativeMethod(Object obj); // obj 是 GC Root

(五)活跃线程——当前正在运行的线程对象。

  • Thread thread = new Thread(); // thread 是 GC Root
    
    1
    2
    3
    4
    5

    (六)**Class对象**——由类加载器加载的 Class 对象。

    - ```java
    Class<?> clazz = MyClass.class; // clazz 是 GC Root

(七)同步锁对象——被同步锁(synchronized)持有的对象。

  • 当一个对象被Synchronized锁定时,它引用的对象也会成为GCRoot对象。

  • Object lock = new Object();
    synchronized (lock) { // lock 是 GC Root
        // 同步代码块
    }
    

②四种对象引用(引用类型)

1.强引用

可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象引用关系,只要这层关系存在普通对象就不会被回收

  • 由可达性分析算法来判断,通过GC Root这个引用链来找到对应的对象,只要能找到,这个对象就是被强引用了
    除了强引用之外,Java中还设计了几种其他引用方式…

2.软引用

软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收

在JDK 1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。

3.弱引用

  • 弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。
  • 弱引用对象本身也可以使用引用队列进行回收

4.虚引用

虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。

  • 虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知
  • Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
  • 主要是通过这个机制知道对象被回收了

③垃圾回收算法

Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。(把所有用户线程停止)
这个过程被称之为Stop The World简称STW,如果STW时间过长则会影响用户的使用。

评价标准:
(1)吞吐量 = 执行用户代码时间/(执行用户代码时间+GC时间)
(2)最大暂停时间(越短越好,用户使用系统时受到的影响越短)
(3)堆使用效率(复制算法只能使用一半)

1.标记-清除算法

核心思想分为两个阶段:

  • 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GC Root开始通过引用链遍历出所有存活对象
  • 清除阶段,从内存中删除没有被标记也就是非存活对象。(遍历堆中的所有对象,释放没有被标记的对象所占用的内存。)

优缺点:
实现简单
会产生内存碎片
分配速度慢————由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。

2.复制算法

核心思想分为两个阶段:

  • 准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间 (From空间)
  • 在垃圾回收GC阶段,将From中存活对象复制到To空间。
  • 将两块空间的From和To名字互换
  1. 将堆内存分成两部分(比如两个相等的区域),每次只使用一部分。
  2. 在一侧的区域中,标记存活的对象,并将这些对象复制到另一侧。
  3. 清空未使用的那一侧,准备下次回收。

优缺点:
吞吐量高
不会发生碎片化,因为复制时只使用连续的内存块
内存使用效率低,因为每次只使用一半的内存

3.标记-整理算法

(标记-压缩算法)
核心思想分为两个阶段:

  • 标记阶段,将所有存活的对象进行标记。Java中使用可达性分析算法,从GCRoot开始通过引用链遍历出所有存活对象。
  • 整理阶段,将存活对象移动到堆的一端,整理内存,使得所有活对象是连续的。清理掉非存活对象的内存空间。

优缺点:
内存使用效率高(整个堆内存都可以使用)
不会发生碎片化
整理过程需要移动对象,可能导致更多的时间开销,效率不高。

4.分代GC算法

现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)

分代垃圾回收将整个内存区域划分为 年轻代老年代

  • 年轻代:存放存活时间比较短的对象

    • 包含三块内容:Eden区(伊甸园)
      两块幸存区[存放经过垃圾回收仍然存活的对象](为了区分,分为s0、s1)
  • 老年代:存放存活时间比较长的对象

分代GC算法.png

过程:

  • 分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
  • 随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。
  • Minor GC 会把需要Eden区和From区需要回收的对象回收,把没有回收的对象放入 To 区
  • 接下来,S0会变成To区,S1变成From区。
  • 当eden区满时再往里放入对象,依然会发生Minor GC。

对象存活时间久:

  • 注意:每次Minor GC中都会为存活的对象记录他的年龄,初始值为0,每次GC完加1。
  • 如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。
  • 当整个新生代的空间都被使用时,先尝试Minor GC,把未达到年龄阈值但是最老的对象放入老年代,如果老年代空间也被占满了,会触发Full GC(回收整个堆的所有对象、年轻代和老年代都会回收)

Eden区满.png

为什么分代GC算法要把堆分成年轻代和老年代呢?
(尽可能做minor gc ,不要进行full gc)

现在年轻代做 Minor GC,老年代等到全满了才做 Full GC,年轻代更有可能被回收,老年代存活时间更久不用频繁回收。

1、可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能。
2、新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除标记-整理算法,由程序员来选择灵活度较高。
3、分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(fullgc),STW时间就会减少。

④垃圾回收器

垃圾回收器的组合关系.png

1.1 年轻代 Serial
串行
Serial.png

1.2 老年代 SerialOld
SerialOld.png

2.1 年轻代 ParNew
可以多线程回收年轻代的垃圾
ParNew.png
2.2 老年代 CMS
Concurrent Mark Sweep 并发标记清理
更多关注的是暂停时间,尽量减小STW对用户的影响
CMS.png
CMS执行步骤.png

3.1 年轻代 Parallel Scavenge
比较关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小。
Parallel Scavenge.png
3.2 老年代 Parallel Old
Parallel Old.png

4. G1垃圾回收器
2.2 3.1
而G1设计目标就是将上述两种垃圾回收器的优点融合
1.支持巨大的堆空间回收,并有较高的吞吐量
2.支持多CPU并行垃圾回收
3.允许用户设置最大暂停时间

执行流程:


面试题

类的双亲委派机制是什么?

  1. 流程:当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。

  2. 类加载器之间的关系:应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。

  3. 好处:双亲委派机制的好处有两点:

    • 第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。

    • 第二是避免一个类重复地被加载

运行时数据区

Java的内存分成哪几部分?详细介绍一下吧(高频)

Java内存中哪些部分会内存溢出?

  1. 堆内存溢出(OutOfMemoryError: Java heap space)
    • 当堆内存不足以分配新对象时,会抛出OutOfMemoryError
    • 通常是由于内存泄漏或对象生命周期过长导致的。
  2. 方法区/元空间内存溢出(OutOfMemoryError: Metaspace)
    • 在JDK8之前,方法区(永久代)可能会因为加载的类过多而溢出。
    • 在JDK8及以后,元空间使用本地内存,如果元空间内存不足,也会抛出OutOfMemoryError
  3. 栈内存溢出(StackOverflowError)
    • 当线程请求的栈深度超过虚拟机允许的最大深度时,会抛出StackOverflowError
    • 通常是由于递归调用过深或栈帧过大导致的。
  4. 本地方法栈溢出
    • 与栈内存溢出类似,本地方法栈也可能因为递归调用过深或栈帧过大而溢出。

JDK7和8中在内存结构上的区别是什么?

  1. 永久代(PermGen)被移除,元空间(Metaspace)引入
    • 在JDK7及之前,方法区被称为永久代(PermGen),用于存储类信息、常量、静态变量等。
    • 在JDK8中,永久代被移除,取而代之的是元空间(Metaspace)。元空间使用本地内存(Native Memory)来存储类元数据,不再受限于JVM的内存限制。
    • 元空间的大小可以动态调整,默认情况下只受系统可用内存的限制。
  2. 字符串常量池的移动
    • 在JDK7之前,字符串常量池位于永久代中。
    • 在JDK7中,字符串常量池被移动到堆内存中。
    • 在JDK8中,字符串常量池继续位于堆内存中。
  3. 其他改进
    • JDK8引入了Lambda表达式和函数式编程,这些特性对内存管理有一定的影响,但主要是在编程模型上的改进,而不是内存结构的根本变化。

总结来说,JDK8在内存结构上的最大变化是移除了永久代,引入了元空间,这使得类元数据的存储更加灵活,减少了内存溢出的风险。

运行时数据区面试问题:

1.运行时数据区分成那几部分?每一部分的作用是什么?

运行时数据区主要是分成了五部分:
程序计数器、Java虚拟机栈、本地方法栈,这三块内容是线程不共享的,每个线程有一块独立的空间;
方法区和堆是线程共享的,每个线程都可以往方法区和堆中去获取对应的数据
作用:
程序计数器:每个线程会通过程序计数器记录当前要执行的的字节码指令的地址程亨计数器可以控制程序指令的进行实现分支、跳转、异常等逻辑。
Java虚拟机栈:
本地方法栈:
虚拟机栈采用栈的数据结构来管理方法调用中的基本数据(局部变量操作数等),每一个方法的调用使用一个栈帧来保存。 (如何递归的调用没有控制好,就会出现栈内存溢出)
堆:
堆中存放的是创建出来的对象,最容易产生内存溢出的位置,堆内存的溢出是整个内存区域中最复杂的,和垃圾回收机制有关。
方法区:
方法区中只要存放的是类的元信息,同时还保存了常量池。(也会存在内存溢出,当方法区存放了大量的类的元信息,并且方法区的大小设置不合理,可能会出现内存溢出)

JDK.png

2. 不同JDK版本之间运行时数据区域的区别是什么?

JDK6 方法区是存在堆里面的,用一个永久代来作为这块内存区域的名字,字符串常量池存放在方法区里面;
JDK7,字符串常量池从永久代拆出放在堆里面,自己占有一块空间;
JDK8永久代不存在了,方法区叫做元空间,元空间属于直接内存里的一块区域,字符串常量池依旧是放在堆里面

int i = 0;i = i++;最终 i 的值是多少?
iconst_0
istore_1

iload_1
iinc 1 by 1

istore_1
return
答案是0,我通过分析字节码指令发现,i++先把0取出来放入临时的操作数栈中接下来对i进行加1,i变成了1,最后再将之前保存的临时值0放入i,最后i就变成了0。


类的生命周期分成哪几个阶段,每个阶段有什么作用?(高频面试题)

生命周期中初始化阶段

1、 JVM中,new出来的对象是在哪个区?

new出来的对象放在堆里,对象的引用放在栈里

2、 说说类加载有哪些步骤?

类加载分三步:加载、连接(验证、准备和解析)和初始化。

加载:class文件加载到JVM内存(静态变量、常量放到方法区),产生Class对象。

连接:

  • 验证:验证class文件是否格式正确。

  • 准备:为静态变量分配内存并设置默认的初始值

  • 解析:将符号引用替换为直接引用 (栈帧里的动态链接也有一步符号引用转变为直接引用)

初始化:为类的静态变量赋予正确的初始值。

3、 JMM是什么?

JMM是java内存模型,是一种规范,与线程共享有关,堆和方法区所有线程共享,需要对其制定规则规范。主内存:java所有变量都存储在主内存中。工作内存/线程本地内存:本线程用到的变量为主存中的副本拷贝。

4、 说说JVM内存结构?

共享区域:堆、方法区

  • 堆:对象和数组、字符串常量池。

  • 方法区:常量、静态变量,运行时常量池(字面量和符号引用)。

私有区域:虚拟机栈、程序计数器、本地方法栈。

  • 虚拟机栈:局部变量、对象引用。

  • 程序计数器:记录线程执行位置,以便下一次继续执行。

  • 本地方法栈:是一些native方法(c++实现)

5、 MinorGC和FullGC有什么区别?

MinorGC发生在年轻代,当eden区满了之后会触发MinorGC.

FullGC发生在老年代,它会暂停所有线程回收年轻代和老年代的对象。

6、 什么是STW?

STW(STOP THE WORLD)GC前会执行STW操作。线程进入JVM设置的“安全点”,暂停所有运行中的线程,stop the world,然后开始GC。

7、 什么情况下会发生堆/栈溢出?

递归容易发生栈溢出。 堆溢出:对象一直增大,不释放。 比如循环里一直list.add

8、 你了解几种垃圾回收器,简单说说?

Serial单线程串行回收器、Parallel并行回收器、CMS回收器、G1回收器、ZGC回收器。

Serial是年轻代串行回收器,采用复制算法。

Serial Old是老年代串行回收器,采用标记整理算法。

Parallel Scavenge是年轻代并行回收器,采用复制算法。

Parallel Old是老年代并行回收器,采用标记整理算法。

ParNew是年轻代并行回收器,采用复制算法,与Scavenge的区别是它能够与CMS结合。

CMS是老年代回收器,采用标记清除算法,特点是低停顿时间,但牺牲一定的吞吐量。

G1回收器是JDK9默认的回收器,废除了空间上的区域划分,而是采用一个个独立的region区域组成,逻辑上保留了分代策略。

ZGC是JDK11引入的回收器,是一种低延迟的回收器。

9、 垃圾回收算法你了解吗?

有引用计数器算法、复制算法、标记清除算法、标记整理算法。

引用计算器就是对象被别人引用时,它的计数器加1,引用结束就减1,对象的计数器值为0就回收。

标记清除根据根节点寻找可达对象,如果不可达就回收,可达性分析法就是挑选一个稳定的对象作为GCROOT,然后寻找可达的对象,不可达就回收。

复制算法把空间分钟两块,回收的时候就把未使用的复制到另一块区域,然后满了就删除本区域,从头到尾只使用一块空间,另一块作为存放回收时未使用的部分。

标记整理算法在标记-清除算法的基础上做了一些优化。在一块内存空间内,标记可达的对象,压缩到内存的一边,然后删除其他对象,这样就不会产生内存碎片。

10、 如果对象的引用被置为null,会被马上回收吗?

不会,在下一个垃圾回收周期中,这个对象是可被回收的。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2023-2025 Annie
  • Visitors: | Views:

嘿嘿 请我吃小蛋糕吧~

支付宝
微信