JVM基础初篇-内存与垃圾回收

Java虚拟机

运行在操作系统之上的,它与硬件没有直接的交互

作用:

  • Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。

特点:

  • 一次编译。到处运行
  • 自动内存管理
  • 自动垃圾回收功能

JVM整体结构:
在这里插入图片描述
Java代码的执行流程:
在这里插入图片描述

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

JVM的生命周期

虚拟机的启动:
Java虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。

虚拟机的执行:

  • 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
  • 程序开始执行时他才运行,程序结束时他就停止(或者错误导致终止)
  • 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程

虚拟机的退出:

  • 程序正常执行结束
  • 过程中遇到异常或者是错误
  • 操作系统错误导致
  • 某线程主动调用方法结束
1、类加载器子系统(Class loader)

类加载器子系统负责从文件系统中或者网络中加载Class文件。
在这里插入图片描述

类的加载过程

1、加载
在这里插入图片描述
2、链接
在这里插入图片描述
3、初始化
在这里插入图片描述
任何一个类声明以后,内部至少存在一个类的构造器 —> init

类的加载器分类
  • JVM支持两种类型的类加载器,分别为引导类加载器自定义类加载器

  • 引导类加载器(启动类加载器):虚拟机自带,没有父加载器;用来Java的核心类库;加载 扩展类和应用程序类加载器,并指定为他们的父类加载器

  • 自定义类加载器:将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器(扩展类加载器和应用程序类加载器都属于自定义类加载器

  • 扩展类加载器:虚拟机自带;派生于ClassLoader类;父类加载器为启动类加载器(注意:如果用户创建的JAR放在jre/lib/ext子目录下,也会自动由扩展类加载器加载)

  • 应用程序类加载器(系统类加载器):Java语言编写;派生于ClassLoader类;负责加载环境变量classpath或系统属性 java,class.path 指定路径下的类库;该类是程序中默认的类加载器;通过ClassLoader.getSystemClassLoader() 方法来获取到该类加载器

在这里插入图片描述

  • 注意:几种类加载器之间的关系时包含关系,不是上层和下层,也不是子父类的继承关系。

  • 对于用户自定义类来说:默认使用系统类加载器进行加载

  • Java的核心类库:都是使用引导类加载器进行加载的

用户自定义类加载器:
为什么需要自定义类加载器?

  • 隔离加载类
  • 修改加载类的方式
  • 扩展加载源
  • 防止源码泄露
    在这里插入图片描述
    关于ClassLoader类:
    是一个抽象类,其后所有的类加载器都在继承自ClassLoader(不包括启动类加载器)
    在这里插入图片描述

双亲委派机制

  • Java虚拟机对class文件采用的是按需加载的方式,也就是说需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。

  • 工作原理:
    在这里插入图片描述

  • 优势:
    避免类的重复加载
    保护程序安全,防止核心API被随意篡改

沙箱安全机制

  • 在这里插入图片描述

补充内容:

  • 在JVM中表示两个class对象是否为同一个类存在两个必要条件
    类的完整类名必须一致,包括包名
    加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
  • 换句话说,在JVM中,及时这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的

类的主动使用和被动使用:
在这里插入图片描述

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

在这里插入图片描述
其中:

  • 红色区域是多个线程共享的,也就是与进程对应
  • 灰色的区域为单线程私有的,也就是与线程一一对应

有关线程:

  • 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行
  • 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射
    当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程终止后,本地线程也会回收。
  • 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法

JVM系统线程:
#在这里插入图片描述

2.1、程序计数器(PC寄存器)

作用:PC寄存器用来存储指向下一条指令的地址,也就是即将要执行的指令代码。由执行引擎读取下一条指令。

因为是线程私有的,所以生命周期与线程的生命周期保持一致。

有关PC寄存器的两个常见问题:

  • 1、使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址呢?
    因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪里开始继续执行;JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
  • 2、PC寄存器为什么会被设定为线程私有?
    所谓的多线程在一个特定的时间段内只会执行其中一个线程方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况;
    由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程的一条指令;
    这也必然导致经常中断或恢复,如何保证分毫无差?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响

CPU时间片:
CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片

2.2、虚拟机栈

内存中的栈与堆:

栈是运行时的单位,而堆是存储的单位,即:栈解决程序的运行问题,即程序如何执行。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用(一个栈帧对应一个方法)

虚拟机栈是线程私有的,生命周期与线程一致

作用:

  • 主管Java程序的运行,它保存方法的局部变量(8中基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回

局部变量 VS 成员变量(或属性)
基本数据类型 VS 引用类型变量(类、数组、接口)

JVM对Java栈的操作只有两个:

  • 每个方法执行,伴随着进栈(入栈、压栈)
  • 执行结束后的出栈工作

对栈来说不存在垃圾回收的问题,但存在栈内存溢出问题

栈的存储单位

  • 每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

每个栈帧中存储着:

  • 局部变量表
  • 操作数栈(或表达式栈)
  • 动态链接(或指向运行时常量池的方法引用)
  • 方法返回地址(或方法正常退出或者异常退出的定义)
  • 一些附加信息

在这里插入图片描述

局部变量表:

  • 局部变量表也被称之为局部变量数组或和本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用,以及returnAddress类型
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
  • 局部变量表,最基本的存储单元是Slot(变量槽),32位以内的类型占一个Slot,64位的类型占用两个Slot
  • 如果当前帧是右构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余参数按照参数表顺序继续排列

变量的分类:

按照数据类型分:

  • 基本数据类型
  • 引用数据类型

按照在类中声明的位置分:

  • 成员变量:(在使用前,都经过默认初始化赋值)
    类变量(静态修饰):linking的prepare阶段:给类变量默认赋值;initial阶段:给类变量显式赋值即静态代码块赋值
    实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
  • 局部变量:(在使用前,必须进行显式赋值!!!否则,编译不通过)

操作数栈(表达式栈)

  • 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈
  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间(之后取出存入局部变量表)
  • 如果被调用的方法有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令

动态链接(或指向运行时常量池的方法引用)

  • 在Java源文件被编译成字节码文件中时,所有变量和方法引用都作为符号引用保存在class文件的常量池里。比如,描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

方法的调用

  • 关键在于什么时候被确定下来
    在这里插入图片描述
    在这里插入图片描述

非虚方法:

  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
  • 其他的方法称为虚方法
    在这里插入图片描述

方法返回地址:

  • 存放调用该方法的pc寄存器的值
  • 一个方法的结束,有两种方式:
    正常执行完成
    出现未处理的异常,非正常退出
  • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧一般不会保存这部分信息

一些附加信息

  • 栈帧中还允许携带跟Java虚拟机实现相关的一些附加信息
(附加)本地方法接口

什么是本地方法:

  • 简单地讲,一个Native Method就是一个Java调用非Java代码的接口

注意:标识符native可以与其他的Java标识符连用,但是abstract除外

2.3、本地方法栈
  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
  • 本地方法栈,也是线程私有的
  • 允许被实现成固定或者是可动态扩展的内存你大小(在内存溢出方面与Java虚拟机栈是相同的)
  • 本地方法是使用c语言实现的
  • 它的具体做法是Native Method Stack中登记Native方法,在Execution Engine 执行时加载本地方法库
  • 并不是所有的JVM都支持本地方法
  • 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一
2.4、堆(Heap)(重要)

堆的一些核心概述:

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大的一块内存空间(堆内存大小是可以调节的)
  • 堆可以处于物理上不联系的内存空间中,但是在逻辑上它应该被视为连续的
  • 所有线程共享Java堆,在这里还可以划分线程私有的缓冲区
  • 所有的对象实例以及数组都应当在运行时分配在堆上
  • 数组和对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
    在这里插入图片描述
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
  • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域

内存的细分:

  • Java7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
  • Java8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间

堆空间大小的设置:

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项“-Xmx”和“-Xms”来进行设置

  • “-Xms”:用来设置堆空间(新生区+养老区)的初始内存空间
  • “-Xmx”:用来设置堆空间(新生区+养老区)的最大内存空间
  • 默认情况下:
    初始内存大小:电脑物理内存大小 / 64
    最大内存大小:电脑物理内存大小 / 4
  • 手动设置时建议将初始堆内存和最大堆内存设置为相同的大小

新生区和养老区(年轻代和老年代)
在这里插入图片描述
其中新生区又可以划分为Eden空间、Survivor 0空间和Survivor 1空间(有时也叫做from区、to区)

可以配置新生区和老年区在堆结构中的占比(默认比例是1:2)
在这里插入图片描述

对象分配的过程:

  • 在这里插入图片描述

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to

  • 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集

常用的调优工具:
在这里插入图片描述

Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都对前文中三个各内存(新生区、老年区:方法区)区域一起回收的,大部分时候回收的都是新生代

针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集:不是完整收集整个Java堆的垃圾收集。其中由分为:
    新生代收集(Minor GC / Young GC):只是新生代的垃圾收集器
    老年代收集(Major GC / Old GC):只是老年代的垃圾收集
    目前,只有CMS GC会有单独收集老年代的行为
    注意:很多时候Major GC和Full GC混淆使用,需要具体分辨老年代回收还是整堆回收
    混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
    目前,只有G1 GC会有这种行为
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
堆空间的分代思想

理由:优化GC性能

内存分配策略

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

TLAB(Thread Local Allocation Buffer)

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

堆空间的参数设置

堆是分配对象存储的唯一选择吗?

  • 在Java虚拟机中,对象是Java堆中分配内存的,这是一个普遍的常识。但是存在一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出去的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收了。这也是常见的堆外存储技术
  • 在这里插入图片描述
  • 在这里插入图片描述

堆的小结:
在这里插入图片描述

2.5、方法区

1、栈、堆、方法区的交互关系

  • 在这里插入图片描述

交互关系

  • 在这里插入图片描述

方法区的基本理解:

  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者扩展
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:OutOfMemoryError:PermGen Space或者OutOfMemoryError:Metaspace
  • 关闭JVM就会释放这个区域的内存

逻辑上方法区是属于堆的

  • 在这里插入图片描述
  • JDK8之后完全废弃了永久代的概念,改用元空间来代替
  • 元空间与永久代的最大区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存

方法区存储什么?

  • 类型信息、运行时常量池、静态变量、JTL代码缓存、域信息(成员变量、属性)、方法信息

方法区中的运行时常量池

  • 字节码文件中包含了常量池:一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表,包括各种字面量和对类型、域和方法的符号引用
  • 字节码文中的常量池中有什么:
    数量值
    字符串值
    类引用
    字段引用
    方法引用
  • 字节码文件中的常量池中的内容,经过类加载器加载后,放入到方法区的运行时常量池中
  • 在这里插入图片描述

方法区随着JDK版本变化的演进:

  • HotSpot中方法区的变化:
    在这里插入图片描述
  • 永久代为什么要被元空间替换:随着Java8的到来,HotSpot VM 中再也见不到永久代了,但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间
    由于类的元数据分配到本地内存中,元空间的最大可分配空间就是系统可用内存空间
  • 永久代被元空间替代的原因
    1、为永久代设置空间大小是很难确定的
    2、对永久代进行调优是很困难的

方法区中静态变量的存放:

  • 静态引用对应的对象实体始终都存在堆空间
  • new 出来的对象不管是不是静态都是存放在堆空间中

方法区的垃圾回收
方法区中的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型

2.6、运行时数据区总结

在这里插入图片描述

3、对象的实例化内存布局与访问定位
3.1、对象的实例化
3.2、对象的内存布局
  • 在这里插入图片描述

  • 对象头主要包含什么?对象头中的信息是什么?

  • 在这里插入图片描述

3.3、对象的访问定位
  • 在这里插入图片描述
4、直接内存(Direct Memory)

概述:

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

对直接内存的简单理解:进程内存 = 堆内存 + 直接内存

5、执行引擎

概述:

  • 执行引擎是Java虚拟机核心的组成部分之一
  • “虚拟机”是一个相对“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是建立在处理器、缓存、指令集合操作系统层面上的,而虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式
  • JVM的主要任务是负责装载字节码文件到其内部,但字节码并不能直接运行在操作系统之上,如果想要一个Java程序运行起来,执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以

执行引擎的工作过程:

  • 在这里插入图片描述
5.1、Java代码编译和执行过程中的细节问题
  • 在这里插入图片描述

  • 橙色部分由前端编译器完成(javac),如下图
    在这里插入图片描述

  • 在这里插入图片描述

什么是解释器(Interpreter),什么是JIT编译器?

  • 解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行
  • JIT编译器:就是虚拟机将源代码直接编译成本地机器平台相关的机器语言

解释器:

  • 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行
  • 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作

(Just in time)JIT编译器:

  • 速度快

为什么要同时存在解释器和JIT编译器?

  • 在这里插入图片描述

热点代码及探测方式:

  • 在这里插入图片描述
  • 在这里插入图片描述
6、String
6.1、String的基本特性
  • String声明是为final的,不可被继承
  • 代表不可变的字符序列,简称:不可变性(底层是数组)
  • 字符串常量池是不会存储相同内容的字符串的
6.2、String的内存分配

String类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中
  • 如果不是双引号直接声明的对象,可以使用String提供的intern()方法

Java7以及以后,字符串常量池被调整在堆结构中。

6.3、intern()的使用

字符串拼接操作:

  • 常量与常量的拼接结果在常量池,原理是编译期优化
  • 常量池中不会存在相同内容的常量
  • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  • 如果拼接的结果调用inern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址

例如:
在这里插入图片描述

  • 上图中,如果拼接字符号前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果。
  • intern():判断字符串常量池中是否存在()值,如果存在,则返回常量池中()值的地址;如果字符串常量池中不存在()值,则在常量池中加载一份()值,并返回该对象的地址

intern()的使用:

  • 在这里插入图片描述
  • 如何保证变量指向的是字符串常量池中的数据?
    方式一:例如,String s = “hello”; //字面量定义的方式
    方式二:例如,String s = new String(“hello”).intern();也就是调用intern()方法

面试题1: new String(“ab”)会创建几个对象?

  • 2个对象
  • 如何证明:看字节码文件;其中,一个对象是通过new关键字在堆空间中创建;另一个对象是字符串常量池中的对象

面试题2: new String(“a”) + new String(“b”) 呢?

  • 5个对象
  • 对象1:new StringBuilder()
  • 对象2:new String(“a”)
  • 对象3:常量池中的"a"
  • 对象4:new String(“b”)
  • 对象5:常量池中的"b"
  • 如果深入StringBuilder()
    对象6:new String(“ab”)
    注意:toString()的调用,在字符创常量池中,没有生成"ab"

intern()使用总结:

  • 在这里插入图片描述
  • 在空间使用上:对于程序中大量存在的字符串,尤其其中存在很多重读字符串时,使用intern()可以节省很多内存空间
7、垃圾回收的概述

什么是垃圾?

  • 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
  • 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出

为什么需要GC?

  • 如果不进行垃圾回收,内存迟早都是会被消耗完
  • 除了释放没用的对象,垃圾回收也可以清除内存里面的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象
  • 没有GC不能保证程序的正常运行

主要是对方法区和堆区进行垃圾回收(GC),垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区回收

  • 其中,Java堆是垃圾收集器的工作重点
  • 从次数上来说:
    频繁收集年轻代
    较少收集老年代
    基本不动元空间(方法区)
8、垃圾回收相关算法
  • 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称之为垃圾标记阶段
  • 判断对象存活一般有两种方式:引用计数法和可达性分析法
8.1、垃圾标记阶段的算法之引用计数算法
  • 引用计数器,对每一个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况
  • 对于一个对象,只要有任何一个对象引用了这个对象,则这个对象的引用计数器就+1;失去引用时,引用计数器-1;只要对象的引用计数器的值为0,表示该对象不能再被使用,可进行回收
  • **优点:**实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
  • 缺点:
    1、它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
    2、每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
    3、引用计数器有一个严重的问题,即无法处理循环引用的情况。所以Java垃圾回收器一般不适用它
8.2、垃圾标记阶段的算法之可达性分析算法
  • 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄露的发生
  • 相比较于引用计数算法,这里的可达性分析就是Java的选择,这种类型的垃圾收集通常也叫作追踪性垃圾收集(根搜索算法)

基本思路:

  • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链
  • 如果目标对象没有任何引用链相连,则是不可到达的,这就意味着该对象已经死亡,可以标记为垃圾对象
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象

  • 虚拟机栈中引用的对象
    比如:各个线程被调用的方法中使用到的参数,局部变量等。
  • 本地方法栈内JNI(通常说的本地方法)引用的对象
  • 方法区中类的静态属性引用的对象
    比如:Java类的引用类型静态变量
  • 方法区中常量引用的对象
    比如:字符串常量池里的引用
  • 所有被同步锁(synchronized)持有的对象
  • Java虚拟机内部的引用
    基本数据类型对象的class对象,一些常驻的异常对象,系统加载类
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

判断是否为GC Roots的小技巧:

  • 由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么它就是一个Root
8.3、对象的finalization机制
  • Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
  • 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
  • finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等
  • 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用
  • 由于 finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态
    在这里插入图片描述
    在这里插入图片描述
    注意:一个对象的finalize方法只会被调用一次
8.4、垃圾清除阶段的算法

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉对象所占用的内存空间

目前在JVM中比较常见的三种垃圾算法标记-清除算法(Mark-Sweep)、复制算法(Copying)、标记-压缩算法(Mark-Compact)

8.4.1、垃圾清除阶段的算法之标记-清除算法

执行过程:

  • 在这里插入图片描述
  • 注意标记的是可达对象,不是垃圾对象

缺点:

  • 效率不算高
  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 这种方式清理出来的空闲内存不是连续的,产生内存碎片。需要维护一个空闲列表

注意:何为清除?

  • 这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载的时候,判断垃圾的位置空间是否够,如果够,就存放
8.4.2、垃圾清除阶段的算法之复制算法

核心思想:

  • 在这里插入图片描述

在这里插入图片描述
优点:

  • 没有标记和清除过程,实现简单,运行高效
  • 复制过去之后保证空间的连续性,不会出现“碎片问题”

缺点:

  • 此算法的缺点也是很明显的,就是需要两倍的内存空间
  • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小

特别的:

  • 如果系统中的垃圾对象很多,复制算法就不会太理想,因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行(会造成无效移动)

应用场景:
新生代中的幸存者0和1区

8.4.3、垃圾清除阶段的算法之标记-压缩(整理)算法(Mark-Compact)

执行过程:

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

在这里插入图片描述

优点:

  • 消除了标记-清除算法中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可
  • 消除了复制算法中内存减半的高额代价

缺点:

  • 从效率上来说,标记-整理算法要低于复制算法
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用地址
  • 移动过程中,需要全程暂停用户应用程序。即:STW
8.4.4、总结

在这里插入图片描述

8.5、分代收集算法(Generational Collecting)

在这里插入图片描述

CMS是一个针对老年代的垃圾回收器

  • 在这里插入图片描述
8.6、增量收集算法、分区算法
8.6.1、增量收集算法

基本思想:

  • 并发的方式执行
  • 在这里插入图片描述

缺点:

  • 使用这种方式,由于在垃圾回收过程中,间接性地执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
8.6.2、分区算法

在这里插入图片描述

9、垃圾回收相关概念
9.1、System.gc()的理解

在这里插入图片描述

9.2、内存溢出与内存泄露

内存溢出(OOM)

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

内存泄露(Memory Leak)

  • 在这里插入图片描述
  • 内存泄露举例:在这里插入图片描述
9.3、Stop The World

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

9.4、垃圾回收的并行和并发

并发:

  • 在这里插入图片描述

并行:

  • 在这里插入图片描述

两者对比:

  • 并发,指的是多个事情,在同一个时间段内同时发生了
  • 并行,指的是多个事情,在同一个时间点上同时发生了
  • 并发的多个任务之间是相互抢占资源的
  • 并行的多个任务之间是不相互抢占资源的
  • 只有在多CPU或者一个CPU多核的情况下,才会发生并行;否则,看似同时发生的事情,其实都是并发执行的
9.5、安全点与安全区域

程序执行时并非所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”(Safe Point)

在这里插入图片描述

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来?在这里插入图片描述

安全区域:

  • 在这里插入图片描述
  • 实际执行时:
    在这里插入图片描述
9.6、引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用强度依次减弱

简要概述:

  • 在这里插入图片描述

强引用(Strong Reference)——不回收:

  • 在这里插入图片描述

软引用(Soft Reference)—— 内存不足即回收:

  • 在这里插入图片描述

简单来说:
当内存足够时,不会回收软引用的可达对象
当内存不够时,才会回收软引用的可达对象

弱引用(Weak Reference)—— 发现即回收

  • 在这里插入图片描述
  • 弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收

虚引用(Phantom Reference)—— 对象回收跟踪

  • 在这里插入图片描述
  • 在这里插入图片描述
10、垃圾回收器
10.1、垃圾回收器的分类和性能指标

垃圾回收器的分类
1、按线程数分,可以分为串行垃圾回收器并行垃圾回收器

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

2、按照工作模式分,可以分为并发式垃圾回收器独占式垃圾回收器

  • 并发式垃圾回收器与应用程序交替工作,以尽可能减少应用程序的停顿时间
  • 独占式垃圾回收器一旦运行,就停止运行程序中的所有用户线程,直到垃圾回收过程完全结束
  • 在这里插入图片描述

3、按照碎片处理方式分,可分为压缩式垃圾回收器非压缩式垃圾回收器

  • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片
  • 非压缩式的垃圾回收器不进行这步操作

4、按工作的内存区间分,又可以分为年轻代垃圾回收器和老年代垃圾回收器

评估垃圾回收器的性能指标

  • 在这里插入图片描述
  • 上图中红色三个性能指标共同构成一个“不可能三角”。三者总体表现会随着技术进步而越来越好。一款优秀的垃圾收集器通常最多同时满足其中两项
  • 简单来说,主要注意两点:
    吞吐量
    暂停时间

性能指标:吞吐量

  • 在这里插入图片描述

性能指标:暂停时间

  • 在这里插入图片描述

在这里插入图片描述

因此,我们需要在最大吞吐量优先的情况下, 降低停顿时间

10.2、不同的垃圾回收器概述

7款经典的垃圾收集器:

  • 串行垃圾回收器:Serial、Serial Old
  • 并行垃圾回收器:ParNew、Parallel Scavenge、Parallel Old
  • 并发垃圾回收器:CMS、G1

7款经典垃圾回收器与垃圾分代之间的关系

  • 在这里插入图片描述
  • 新生代垃圾收集器:Serial、ParNew、Parallel Scavenge
  • 老年代垃圾收集器:Serial Old、Parallel Old、CMS
  • 整堆垃圾收集器:G1

垃圾回收器的组合关系(-JDK14):

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

如何查看默认的垃圾回收器:

  • -XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾回收器)
  • 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID(jps 查看当前所有进程)
10.3、Serial回收器:串行回收

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

在这里插入图片描述

10.4、ParNew回收器:并行回收

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

10.5、Parallel回收器:吞吐量优先

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
参数配置:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.6、CMS垃圾回收器:低延迟

在这里插入图片描述
CMS的工作原理:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
特点与弊端:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以设置的参数:
在这里插入图片描述
在这里插入图片描述

10.7、垃圾回收器小结

垃圾回收器的小结:

  • 在这里插入图片描述
  • 在这里插入图片描述
10.8、G1(Garbage First)垃圾回收器:区域化分代式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
G1垃圾回收器的特点:

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

G1垃圾回收器的参数设置:

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

有关region的详细说明:

  • 在这里插入图片描述

  • 在这里插入图片描述

  • 在这里插入图片描述

G1垃圾回收器的垃圾回收过程:

G1 GC的垃圾回收过程主要包括如下三个环节:

  • 年轻代GC(Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)
  • (如果需要,单线程、独占式、高强度的Full GC)还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收)
  • 在这里插入图片描述
  • 在这里插入图片描述

记忆集(Remembered Set):

  • 在这里插入图片描述

具体过程——年轻代的回收:

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

具体过程——并发标记过程:

  • 在这里插入图片描述

具体过程——混合回收:

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

具体过程(可能会出现的过程)—— Full GC:

  • 在这里插入图片描述

G1垃圾回收的优化建议:

  • 在这里插入图片描述
10.9、垃圾回收器总结

在这里插入图片描述
垃圾回收器的选择:
在这里插入图片描述

有关垃圾回收器的面试问题或者注意点:

  • 垃圾收集的算法有哪些?如何判断一个对象是否可以回收?
  • 垃圾收集器工作的基本流程
10.10、GC日志分析

内存分配与垃圾回收的参数列表:
在这里插入图片描述

10.11、垃圾回收器的新发展

在这里插入图片描述
JDK11新特性:
在这里插入图片描述
open JDK12中:
在这里插入图片描述

ZGC的简单介绍:
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值