面试-JVM-运行时数据区结构-内存泄露-垃圾回收机制

文章目录

运行时数据区结构

说下JVM的主要组成部分?及其作用?

JVM = java Virtual Machine,java虚拟机。
JVM 包含两个子系统 和 两个组件。

组成说明
类加载系统
ClassLoader
子系统
根据全限定类名装载class文件到运行时数据区的方法区中
运行时数据区
Runtime Data Area
组件
也就是JVM内存,包含线程共享区(方法区、堆),线程独占区(本地方法栈、程序计数器、虚拟机栈)
把字节码加载到内存中,即运行时数据区的方法区
执行引擎
Execution Engine
子系统
就是解释器,负责解释class中的指令
将字节码翻译成底层系统指令,交由CPU去执行
本地方法接口
Native Interface
组件
连接本地方法库(native lib),是与其他编程语言交互的接口
在此过程中需要调用其他语言的本地库接口来实现整个程序的功能

扩展
java程序运行机制:

1 集成开发环境编写java程序,文件后缀名是.java
2 javac编译器,将.java文件编译为字节码文件(.class3 类加载器,将.class文件加载到运行时数据区中的方法区
4 执行引擎(解析器),将字节码文件翻译成底层系统指令,交由CPU执行
5 这个过程中,需要调用其他语言的本地库接口

谈谈对运行时数据区(内存)的理解?

JVM组成图示:
在这里插入图片描述

2022/8/4
JVM将内存分为主内存和工作内存。
主内存 : 本的方法区 和 堆
Java内存模型规定了所有变量都存储在主内存中。

工作内存/本地内存:java虚拟机栈、本地方法栈、程序计数器
( 有时会将java虚拟机栈和本地方法栈合二为一 )
每个线程都有自己的工作内存,保存了该线程使用的变量,该变量是主内存中的共享变量的副本拷贝。

运行时数据区 组成介绍?

私有

  • 程序计数器 Program Counter Register
条目说明
概念较小的内存空间,可以看成是当前线程执行字节码文件的行号指示器
作用字节码解释器就是需要通过改变程序计数器的值来获取下一条要执行的字节码指令
是私有的原因java虚拟机的多线程是通过线程轮流切换并分配CPU执行时间来实现的。
任何一个确定的时刻,一个处理器只会执行一个线程中的命令。
为了线程切换后能恢复到正确的执行位置,所以每个线程都需要一个独立的程序计数器,各个线程之间的计数器互不影响。
  • java虚拟机栈 Java Virtual Machine Stacks
条目说明
概念描述Java方法执行的内存模型
作用每个方法在执行的同时都会创建一个栈帧(stack Frame),用于存储局部变量表、操作数栈、动态链表、方法出口等信息
解释每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
性质它是线程所私有的,生命周期与线程相同
异常线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

局部变量表
存放了编译期可知的各种基本数据类型(boolean、byte、short、char、int、long、float、double),对象引用和returnAddress类型(指向了一条字节码指令的地址)。
空间单位是槽(Slot),64位的long和double类型会占用两个Slot。
局部变量表所需要内存空间在编译期完成分配,当进入一个方法时,该方法需要在帧中分配的局部变量表内存区域是确定的,在方法运行期间不会改变局部变量表的大小。

  • 本地方法栈 Native Method Stack
条目说明
概念为虚拟机使用到的Native方法服务
(与java虚拟机栈类似)
抛出异常StackOverflowError 和 OutOfMemoryError 异常
备注java虚拟机规范没有对本地方法栈中方法使用的语言、方式和数据结构做出强制规定,所以具体的虚拟机可以自由地实现它。
例如:Sun HotSpot虚拟机直接把Java虚拟机和本地方法栈合二为一。

共享

  • Java堆
条目说明
概念被所有线程所共享的一块内存区域,在虚拟机启动时创建
存在目的存放对象实例,几乎所有的对象实例都在这里分配内存
又被称为GC堆java堆是垃圾收集器管理的主要区域。
从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以java堆又被细分为:新生代 和 旧生代。
从内存分配的角度,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allcation Buffer,TLAB),为了更好地回收内存和分配内存。
抛出异常当在堆中没有完成实例分配,且堆无法扩展时,将会抛出OutOfMemoryError异常
内存区域规定java虚拟机规定,java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
实现时,可以是固定大小的,也可以是可扩展的
  • 方法区(Method Area)
条目说明
概念各个线程共享的内存区域
作用用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
理解虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但它有别名非堆,目的为了与java堆区分开来
特点不需要连续的内存,可选固定大小,可扩展,可选择不实现垃圾收集(垃圾回收主要针对常量池、类型的卸载)
异常方法区无法满足内存分配需求时,抛出OutOfMemoryError异常

运行时常量池 Runtime Constant Pool
方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一些信息在常量池中,用于存放编译器生成的各种自变量和符号引用。该内容将在类加载后进入方法区的运行时常量池中存放。

堆 和 栈 的区别是什么?

区别
存储内容不同存储Java中的对象实例
成员变量、局部变量、类变量,它们指向的对象都存储在堆内存中
存储基本数据类型和方法引用
共享 or 私有共享,其中对象对所有线程可见 且 可访问私有,每个线程都有一个栈内存,存储的变量只能在所属线程中可见
栈内存可以理解为线程的私有内存
异常错误没有空间,抛出OutOfMemoryError没有空间,抛出StackOverflowError
空间大小较大较小
作用存储单位
数据存储问题,数据该怎么放、放哪里
运行时单位
决定程序如何执行、如何处理数据

扩展
每一个线程都会有独立的线程栈,因为每个线程的执行逻辑不同。

栈中存储什么?堆中存储什么?

基本数据类型 和 对象的引用
java中的对象实例

扩展

  • 为什么将对象实例存储在堆中?
    因为对象实例的大小是不可估量 且 可能会动态改变的,由于栈内存较小,存储在栈中,可能会出现StackOverflowError。
    而堆内存较大,出现OutOfMemoryError的可能性较小。
  • 为什么基本数据类型 和 对象引用要放在 栈中?
    因为基本数据类型占用的空间一般为1-8个字节,需要空间较少;且基本数据类型不会出现动态增长的情况,长度固定,所以栈内存就够用了。

为什么要把堆和栈区分出来?栈中不是也可以存储数据吗?

区分堆和栈的原因
原因具体介绍
软件设计角度分而治之的思想,栈代表处理逻辑、堆代表数据,使得处理逻辑更加清晰
为了实现共享堆中的数据可以被多个栈共享
共享提供了有效的数据交换方式(共享内存);堆中的共享常量和缓存可以被所有线程访问,节省了空间
动态增长栈只能向上增长,限制了栈存储内容的能力
堆中的对象可以根据需要动态增长
两者拆分,使动态增长成为可能性,栈中只需要记录一个地址即可

扩展
保存系统运行的上下文,需要进行地址段的划分。

栈中可以存储数据

栈中一般存放的是基本数据类型和java对象的引用,不存储java对象的实例。
因为栈内存空间较小,而java对象的实例大小不固定且可能会动态改变,存储在栈中可能会出现StackOverflowError。

内存间的交互操作有哪些?需要满足什么规则?

主内存与工作内存中间的交互,具体表现是:一个变量如何从主内存中拷贝到本地内存、如果从本地内存同步回主内存。
主要涉及两个部分:主内存和本地内存,本地内存是主内存的拷贝。

java内存模型定义了8种操作,每一种都是原子的,不可再分的。

操作名作用于说明
lock(锁定)主内存变量把一个变量标识为一个线程独占的状态
unlock(解锁)主内存变量把一个处于锁定状态的变量释放,释放后的变量才能被其他线程所访问
read(读取)主内存变量把一个变量的值从主内存传输到本地内存中,以便load操作使用
load(载入)本地内存变量把read操作从主内存中得到的变量值放入本地内存的变量副本中
use(使用)本地内存变量将本地内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时,就执行执行这个操作
assgin(赋值)本地内存变量将执行引擎接收的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store(存储)本地内存变量把本地内存中的一个变量的值传送到主内存中,以便随后的write操作使用
write(写入)主内存变量把store操作从本地内存中得到的变量的值放入主内存的变量中

内存交互整体流程图如下所示:
在这里插入图片描述
2022/8/4 理解
字节码的执行引擎 = JVM运行时数据区的执行引擎,主要是将.class文件翻译成系统可以运行的指令。
执行引擎是解释器,而不是编译器。

编译器和解释器的区别:

区别编译器compare解释器interpreter
都转换为机器码在程序运行之前将代码转换成机器码在程序运行时将代码转换成机器码
intput整个程序每次读取一行
output生成中间代码不生成中间代码
工作机制编译在执行之前完成编译和执行同时进行
存储存储编译后的机器代码在机器上不保存任何机器代码
修改修改代码需要改源码,并重新编译直接修改就可运行

需要满足的规则
主内存与工作内存变量传递

  • 不允许read和load、store和write操作之一单独出现,且必须保证操作执行顺序;
    且变量从主内存到工作内存 = read 和 load、变量从工作内存到主内存 = store 和 write
    表示不允许一个变量从主内存读取但是工作内存不接受,从工作内存发起写回但是主内存不接受
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变之后必须把该变化同步回主内存中;
  • 不允许一个线程无原因的(没有发生任何assign操作)把数据从线程的工作内存同步回主内存中;
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量 = 在对一个变量实施use和store操作之前,必须执行load和assign操作 = 工作内存中的新变量只能是执行引擎assign操作或主内存read→load操作;(xhj:本地内存的值主内存给的,不是自己的)

加锁与释放操作

  • 一个变量在同一时刻只允许一个线程对其进行lock操作,但lock操作可以被同一个线程重复执行多次,多次执行后,只有执行相同次数的unlock,变量才会被解锁;
  • 如果一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新load(主内存给本地)或assign(执行引擎给本地)操作初始化变量的值;
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量;
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作);

JVM中涉及的java基础

java中的参数传递是值传递 还是 引用传递?

值传递。
传递的参数是 基本数据类型,则传递的是具体数值;
传递的参数是 对象引用,则传递是堆中对象的地址,而对于对象引用的修改实际上修改的是对象引用所表示的对象,而不是引用本身。

扩展
java中没有指针的概念。
程序运行永远都是在栈中进行的,因而参数传递只存在传递基本数据类型和对象引用的问题,不会直接传递对象本身。

java对象的大小是怎么计算的?

基本数据类型的大小是固定的;
非基本类型的java对象,其大小就值得商榷。
2022/8/4总结
java对象的大小 = 8的倍数、空的object对象大小是8byte

非基本数据类型大小
在java中,一个空Object对象的大小是8 byte,这个大小只是在堆中一个没有任何属性的对象的大小。
因为所有java非基本类型的对象都默认继承Object对象,则java对象的大小必须是大于 8 byte 。
java给对象分配内存的时候都是以8的整数倍来分的。
也就是一个空的java对象所使用的大小是4(对象引用)+8(对象)=12 byte。

Object ob = new Object();
该java对象所占内存为 4 byte + 8 byte4 byte 指的是java栈中保存引用所需的空间;
8 byte 则是对象在java堆中所需空间。 

案例

class MaNong{
	int count;
	boolean flag;
	Object obj;
}

MaNong类的大小为:空的Object对象(8 byte) + int 数据(4 byte) + Boolean 数据(1 byte) + 空Object对象在栈中的引用(4 byte) = 17 byte 。因为java给对象分配内存的时候都是以8的整数倍来划分的,所以大于17、满足是8的整数倍、且最小的数就是24,因此该对象MaNong的大小是 24 byte 。

内存泄露和内存溢出

谈谈内存泄露的理解?

内存泄露(Memory Leak),就是存在一些不会再被使用但却没有被回收的对象。
这些对象具有如下性质:
对象是可达的,即在有向图中,存在通路可以与其他相连;
对象是无用的,即程序以后不会再使用这些对象。

内存泄露的根本原因是什么?

根本原因:
长生命周期对象 持有 短生命周期对象的引用 而导致其不能被回收。

举几个可能发生内存泄漏的情况?

内存泄露 : 无用对象(不再使用的对象)持续占有内存 或 无用对象的内存 得不到及时释放 。

具体情况说明
外部类引用内部类发生在非静态内部类(匿名类)中,在类初始化时,内部类总是需要外部类的一个实例。
每个非静态内部类默认都持有外部类的隐式引用。如果在应用程序中使用该内部类的对象,即使外部类使用完毕,也不会对其进行垃圾回收。
未关闭的资源创建一个连接或打开一个流,JVM都会分配内存给这些资源。比如:数据库连接(dataSource.getConnetint())、网络连接(Socket) 和 IO连接,除非显式调用clone()方法将其连接关闭,否则不会自动被GC回收
静态属性java中静态属性的生命周期伴随着应用整个生命周期。
不当的equals和hashcode方法实现定义一个新的类,往往需要重写equal和hashcode方法,重写不当,会造成内存泄露
案例:某A类对象存储入Map集合,map集合不能存储重复值,如果A类没有重写equals方法,执行put方法,Map会认为每次创建的对象都是新的对象,造成内存不断的增长
finalize()每当一个类的finalize()方法被重写时,该类的对象就不会被GC立即回收。GC会将它们放入队列进行最终确定,在以后的某个时间点进行回收。
如果finalize()方法重写的不合理或finalizer队列无法跟上Java垃圾回收器的速度,那么迟早,应用程序会出现OutOfMemoryError异常。
ThreadLocal提供线程本地变量,保证访问到的变量属于当前线程。
ThreadLocal的实现中,每个Thread维护一个ThreadLocalMap映射表,key是ThreadLocal实例本身,value是真正需要存储的Object。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。
如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
单例模式不正确使用单例模式是引起内存泄露的常见原因。
单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄露
集合内对象 remove集合里面的对象属性被修改,remove操作不起作用了。
一个集合中存储的对象是Student类对象,当Student类中的成员属性被修改时,对集合对象的删除remove操作就不起作用了。

扩展

  • 单例模式
    概念:在应用整个生命周期内只能存在一个实例。
    好处:避免实例对象的重复创建,减少创建实例的系统开销,节省内存。

参考

尽量避免内存泄露的方法?

解决方法说明
尽早释放无用对象的引用最好在使用临时变量的时候,让引用变量在退出活动期后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露
进行字符串处理时,避免使用String,而使用StringBuilder因为每一个String对象都会独占内存一块区域
尽量少用静态变量静态变量属于全局,GC不会回收
少使用静态变量,减少生命周期
避免集中创建对象尤其是大对象,如果可以的话尽量使用流操作JVM在突然需要大量内存时,会触发GC优化系统内存环境
尽量运用对象池技术以提高系统性能生命周期长的对象拥有生命周期短的对象时 容易引发内存泄露。
例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后一块处理结束释放一块
不要在经常调用的方法中创建对象,忌讳在循环中创建对象可以适当使用hashtable、vector创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。
基类 析构函数在需要的时候将基类的析构函数定义为虚函数
释放数组释放数组使用delete []

扩展

  • 虚函数
    java的普通成员函数(没有被static、native等关键字修饰)就是虚函数,即每个非静态方法,因为它本身就实现虚函数实现的功能(多态)。
    虚函数的性质是:多态。
    如果java中不希望某个函数具有虚函数特性,可以加上final关键字变成非虚函数。

  • 析构函数
    作用:用于撤销对象前,完成一些清理工作。

JVM常见的OOM异常?

常见的异常包括:

  • 堆内存溢出:

堆用于存储对象实例,如果不断创建对象,并且Root GC根节点与这些节点之间有通路从而避免垃圾回收机制来回收这些对象,就可能会导致堆内存溢出;

  • 栈内存溢出:

虚拟机栈和本地方法栈溢出;
原因:线程请求的栈深度大于虚拟机允许的栈深度抛出stackOverflowError;
虚拟机的栈允许动态扩展,当扩展栈容量无法申请到足够的内存时,抛出OutOfMemoryError;

  • 常量内存溢出:

方法区和运行时常量池溢出;
运行时常量区是方法区的一部分;
方法区的内存溢出;
方法区用于存放类型的相关信息,比如类型、访问修饰符、常量池、字段描述、方法描述等;
该区域内存溢出通过是运行时产生大量的类去填满了方法区。

  • 本机直接内存溢出:

直接内存 = 不属于虚拟机运行时数据区;是由操作系统直接管理的内存,又称为堆外内存;
可以使用Unsafe 或 ByteBuffer分配直接内存。

堆 垃圾回收机制

垃圾回收是从哪里开始的呢?

java栈、系统运行时的寄存器

  • 开始 java栈
    垃圾回收首先要查找哪些对象是正在被当前系统使用的,没有被当前系统使用的对象就可能需要垃圾回收机制去回收;
    栈是真正执行程序的地方;一个线程对应一个栈,如果多个线程的话,就必须对线程对应的所有栈进行检查。
  • 接着 系统运行时的寄存器
    系统运行时的寄存器用于存储程序运行的数据。

查找过程:
以栈或系统运行时寄存器为起点,找到堆中的对象,从这些对象找到堆中其他对象的引用,最终以null引用或基本类型结束,形成了一棵以Java栈中引用所对应的对象为根节点的一棵对象树。(可达性分析法)
当前树上的节点不被垃圾回收;剩余对象节点,会被当做垃圾回收。

判断垃圾可以回收的方法有哪些?引用计数法 可达性分析法

引用计数法:对象有引用计数器、无法得到循环引用;可达性分析法:从GC节点开始,找引用节点
在JVM中,判断垃圾可以回收的方法有:引用计数法 和 可达性分析法
引用计数法 Reference Counting

  • 基本思想:
    堆中的每个对象实例都有一个引用计数器(变量),每当多一个地方引用它,计数器值加1;当引用失效时,计数器值减1;
    任何引用计数器为0的对象实例可以被当作垃圾收集。

理解:a对象 引用对象b(引用计数器:统计引用该对象的对象的数量)
当a对象实例被回收时,它引用的任何对象实例的引用计数器减1。
当a对象被创建时,就给该对象实例分配一个变量,该变量计数设置为0;
当其他对象中的变量被赋值为a对象的引用时,a对象的计数值加1;
当a对象实例的某个引用超过了生命周期或者被设置为新的值时,a对象实例的引用计数器减1;

  • 优点:
    可以快速执行,算法简单易于实现;
    对程序需要不被长时间打断的实时环境比较有利。

  • 缺点:
    无法检测出循环引用。即父对象有一个对子对象的引用,子对象反过来引用父对象。这样他们的引用计数永远不可能到0。

可达性分析算法 Reachability Analysis

  • 基本思想:
    可达性分析算法来自于离散数学中的图论。
    程序把所有的的引用关系看作一张图,从第一个节点GC Root开始,寻找它的引用节点(GC Root中所引用的节点),找到之后继续寻找这个节点的引用节点;当所有的引用节点寻找完毕之后,剩余的节点就被认为是没有被引用到的节点,即无用节点。无用节点则被判定为可回收的对象。

在这里插入图片描述

  • 优点:
    可以解决循环引用的问题;

  • 缺点:
    多线程访问的环境下,其他线程可能会重复访问已回收的对象。

GC Root节点的选取
Java虚拟机栈中引用的对象(栈帧中的局部变量表);
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(Java Native Interface,java本地接口)引用的对象。
.

被标记为垃圾的对象一定会被回收吗?不会。

不会,两次标记、finalize方法、F-Queue队列
即使可达性分析算法中的不可达对象,也并非是“非死不可”。
要判定一个对象死亡,至少要经历两次标记过程。

第一次标记后,会判断是否可以执行finalize()方法。不能执行该方法(没有覆盖finalize方法或者虚拟机已经调用过了),可以执行(将对象放在F-Queue队列中,随后finalizer线程去执行它);然后在F-Queue队列中进行小规模的标记,如果通过可达性对其算法来说还是没有引用链和GCRoot关联,则会被回收掉。

  • 第一次标记:
    如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,它将会被第一次标记并且进行一次判断;
    判断该对象是否有必要执行**finalize()**方法:
    • 当对象没有覆盖finalize()方法 或者 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行finalize()方法”;
    • 如果该对象被判定为“有必要执行finalize()方法”,则该对象会被放置在F-Queue的队列之中,在随后Finalizer线程去执行它(该线程是由虚拟机自动建立的、低优先级的)。

这里的执行 = 虚拟机会触发这个方法,但并不承诺会等待它运行结束。这是为了防止一个对象在finalize()方法中执行缓慢,或者发生了死循环,这时可能会导致F-Queue中的其他对象永久处于等待状态,从而导致整个内存回收系统崩溃。
对象要在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。

  • 第二次标记:
    稍后GC将对F-Queue中的对象进行第二次小规模的标记,这时候如果对象还没有逃脱,基本就会被回收。

finalize() 方法是一次性的免死金牌,只能免死一次。

常用的垃圾收集算法有哪些?

垃圾收集算法有:标记清除算法、标记整理算法、复制算法、分代收集算法等。

标记清除算法(Mark-Sweep)

条目说明
概念算法分为“标记” 和 “清除” 两个阶段:
①标记出所有需要回收的对象;
②标记完成后统一回收所有被标记的对象;
优点无须移动对象,算法简单
缺点效率问题:标记和清除的过程效率都不高;
会产生大量的碎片空间:可能会造成在申请大块内存的时候没有足够的连续空间导致再次GC。

标记整理算法(Mark-Compact)

条目说明
概念算法分为“标记” 和 “整理”两个阶段:
①标记出所有存活的对象;
②将所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
遍历两次,一次标记,一次压缩清理
优点堆的使用率高,无内存碎片;
缺点暂停时间更长,对缓存不友好(对象移动后顺序关系不存在)

复制算法(Copy)

条目说明
概念将内存分成两块,每次申请内存时都是用其中一块。
当内存不够时,将这一块内存中所有存活的对象复制到另一块内存中,然后再把已使用的内存整个清理掉。
内存划分将内存区域分成相等的两部分:分别用两个指针from 和 to 来维护,分配内存只使用from指针指向的内存区域
优点①吞吐量大(一次能收集整个内存的一半空间);
②分配效率高(对象可以连续分配);
③没有内存碎片
缺点①每次申请内存时,只能使用一半的内存空间;
②内存利用率严重不足。

分代收集算法(Generational Collection)

条目说明
概念根据对象存活周期的不同将内存划分成几块。一般Java堆分成新生代和老生代。
优点组合算法,分配效率高、堆的使用率高
缺点算法复杂
  • 内存划分(java堆)
条目说明
新生代java对象是朝生暮死的,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
新生代又可分为伊甸区和两个幸存区。
伊甸区和两个幸存区的划分比例是:8:1:1
老生代对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记清理”或“标记整理”算法进行回收。
对象划分的依据对象的生存周期或者说经历GC的次数。
对象创建时在新生代申请内存,当经历一个GC还存活,那么对象的年龄+1;
当年龄超过一定值(默认15,通过MaxTenuringThreshold设置),如果对象还存活,则该对象进入老生代。
新/老生代划分新生代:老年代所占内存比例是1:2,也就是新生代占据1/3的内存,老生代占据2/3的内存
  • 内存分配流程
    ①新对象存储在伊甸区;
    ②根据经验表明,伊甸区中大部分对象躲不过一轮GC,所以熬过一轮GC的对象进入幸存区;
    ③两个幸存区中,使用复制算法存储对象,每次经过GC考验的对象就复制到另一幸存区,反复执行该过程;
    ④当对象的年龄超过一定值(默认15)后,就会进入老生代。(特别的:如果一个对象所占内存很大,就不适合在幸存区中来回复制,就直接进入老生代。)

总结2022/8/5

说明伊甸区幸存区A幸存区B老年代
新对象
熬过一个生命周期
A区中对象经过一个GC
B区中对象年龄超过固定值
当对象占用内存很大

具体图示
在这里插入图片描述
备注2022/8/5
新生代 和 老生代 比例图画错了,应该是 1:2。

Java内存堆的垃圾回收机制为什么要采用分代收集算法?

原因:
①分代收集算法:针对不同生命周期的对象采用不同的收集方式,以便提高垃圾回收效率。
②分代垃圾回收采用分治的思想。进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最合适它的垃圾回收方法进行回收。这样减少了遍历存活对象所消耗的时间。每次对整个堆空间中的所有存活对象进行遍历,所花费时间都很长,而具体针对生命周期长的对象来说这种遍历是没有效果的。因为不管多少次遍历,它们依旧存在。

扩展
目前而言,业界各种商业虚拟机堆内存的垃圾收集机制基本都采用分代收集算法。

分代收集下的年轻代和老年代应该采用什么样的垃圾回收算法?

新生代(年轻代)主要以复制算法为主。

  • 所有新生成的对象首先都放在年轻代。新生代目的在于尽可能快速的收集掉那些生命周期短的对象。
  • 年轻代内存按照8:1:1的比例分成一个伊甸区(Eden)和两个幸存区(survivor0,survivor1)。大部分对象在Eden区生成;回收时先将Eden区的存活对象复制到survivor0区,再清空Eden;当survivor0区满时,将Eden区和survivor0存活对象复制到survivor1中,清空Eden和survivor0,然后将survivor0和survivor1交换,继续保持survivor1为空。
  • 当survivor1区不足以存放Eden和survivor0的存活对象时,就将存活对象直接存储在老生代。若老生代也满了则会触发一个Full GC,即新生代、老生代都进行回收。
  • 新生代发生的GC叫做Minor GC,发生频率比较高,不一定等Eden区满了才触发。

老生代(老年代)主要以标记整理为主。

  • 在新生代中经历了N次垃圾回收任然存活的对象,就会被放到老生代中。老生代中存放的都是生命周期较长的对象。
  • 老生代内存比新生代大很多(大概2:1),老生代内存满触发Major GC,但触发频率较低,存储对象存活时间较长,存活率标记高。

备注:2022/8/5
垃圾回收被分为:Minor GC、Major GC、Full GC;

什么是浮动垃圾?

浮动垃圾:
应用程序运行的同时进行垃圾回收,所以有些垃圾可能会在垃圾回收进行的过程中产生,这些垃圾被称为浮动垃圾。
此次垃圾回收不会回收浮动垃圾,它们需要等到下次垃圾回收时才能处理。

什么是内存碎片?如何解决?

内存碎片:
不同Java对象存活时间不一定是相同的。在程序运行一段时候后,垃圾回收机制会回收某些对象,这时就会产生零散的内存碎片。
解决方式:
内存碎片导致的最直接的问题就是:无法分配大块的内存空间,进而使得程序运行效率降低。
可以采用垃圾回收算法解决,比如“复制算法”、“标记-整理”等。

常用的垃圾收集器有哪些?

常见的垃圾收集器有10种:

垃圾收集器分代情况使用情况采用算法
Serial物理/逻辑都分代年轻代停止-复制算法
SerialOld物理/逻辑都分代老年代标记-整理算法
ParallelScavenge物理/逻辑都分代年轻代停止-复制算法
ParallelOld物理/逻辑都分代老年代标记-整理算法
ParNew物理/逻辑都分代年轻代停止-复制算法
CMS物理/逻辑都分代老年代标记-清除算法
G1逻辑分代,物理不分代分代收集算法
ZGC物理/逻辑不分代
Shenandoah物理/逻辑不分代
Epsilonjdk11提出debug使用的,不考虑

垃圾回收器组合
Serial + SerialOld
Parallel Scavenge + ParallelOld
ParNew + CMS

Serial、Parallel Scavenge、Parallel New

介绍SerialParallel ScavengeParallel New
概念单线程新生代复制算法的垃圾回收器多线程新生代复制算法的垃圾回收器多线程新生代复制算法的垃圾回收器
理解让所有线程找到一个安全点然后停止工作,这种行为称之为STW(stop-the-world);
所有线程都停止,此时单线程的Serial开始进行垃圾清理。
当Serial工作时间过长的时候,那么STW的时间就会加长,用户的响应时间也会加长。
是一种高效的多线程复制算法;在Serial基础上的;与Parallel Scavenge 类似,但更注重吞吐量
垃圾回收算法复制算法复制算法复制算法

复制算法: 将内存分成两块区域area0、area1,每次对象内存的分配都在area0中,当该区域满之后,将该区域的存活变量复制到area1中,然后清理掉area0区域的对象,最后将area1的内容复制给area0.

SerialOld、Parallel Old、CMS

介绍SerialOldParallel OldCMS
概念单线程老生代标记整理算法多线程老生代标记整理算法多线程老生代标记清理算法
理解存在STW状态,所有线程都停止后,此时单线程的SerialOld开始执行
适用场景是存活对象较多的情况
一样存在STW,可以看做是SerialOld的多线程版本老年代垃圾回收器,在老年代分配不下时,触发CMS
垃圾回收算法标记-整理算法标记-整理算法标记-清理算法

CMS = Mostly Concurrenct Mark and Sweep Garbage Collector”(最大-并发-标记-清除-垃圾收集器)。

标记-整理算法 :把所有存活的对象标记出来,然后将它们向一端移动,然后直接将边界以外的部分清理掉。
标记-清理算法:把所有需要清理的对象标记出来,垃圾回收机制统一将其清理掉。

ZGC

  • 概念:
    是JDK11中推出的追求低延迟的实验性质的垃圾回收器。
  • 设计目标:
    停顿时间不超过10ms;停顿时间不随堆大小、活跃对象的大小而增加;
    支持4MB~4TB级别的堆;

JVM垃圾回收机制
在这里插入图片描述
JDK版本与垃圾收集器的对应广关系
JDK7 = Parallel Scavenge + Parallel Old
JDK8 = Parallel Scavenge + Parallel Old
JDK11 = G1

谈谈你对CMS垃圾收集器的理解?

概念:
CMS = Concurrent Mark-Sweep、并发标记清理垃圾回收器。

  • 优势:
    是以牺牲吞吐量为代价来获得最短回收停顿时间。
  • 使用算法:
    标记清除算法。

具体实现过程:
有五个阶段——初始标记阶段、并发标记阶段、并发预清理阶段、重新标记阶段、并发清理阶段。

阶段介绍
初始标记阶段暂停所有线程,此时是CMS的第一个STW(Stop the world,所有用户线程暂停)
标记所有GC Roots直接关联的对象 + 被存活的年轻代所直接引用的老年代对象
并发标记阶段此时GC线程和用户线程同时存在,会记录所有可达对象;
此过程结束之后由于用户线程一直在运行则还会产生新的引用更新,即需要下一步;
改变:①GC Root 对老生代对象引用的改变 ② 新生代对象对老生代对象的引用改变
③老生代中对象之间引用的改变 ④ 老生代出现的新生对象
并发预清理阶段由于并发标记阶段没有停止用户线程,老年代的引用可能发生变化 + 新生代也可能有新的引用指向老年代(都需要重新标记),该过程可能发生Minor GC(新生代垃圾清理机制)来减少扫描时间。
引用的改变:老年代引用改变、新生代有新的引用指向老年代
重新标记阶段停止用户线程(第二次STW),将上一步并发标记过程中用户线程引起的更新进行修正,时间会比初始标记时间长、比并发标记时间短;
并发清理阶段在所有需要清理的对象都被标记完成后就会执行最后一步清理操作;GC线程执行同时用户线程可以执行,GC线程只会清理标记的区域。

具体执行过程图示:

备注2022/8/7
GC Root选取方式:java虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈的JNI(java native interface)java本地接口引用的对象;
而垃圾回收的对象 是java堆中的内容。

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

在这里插入图片描述

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

谈谈你对G1收集器的理解?

概述

  • 概念
    G1 = Garbage - First,是面向服务器端应用的垃圾回收器。
  • 思想
    G1收集器,采用分而治之的思想,将整个java堆划分为多个大小相等的独立区域(Region)来实现可预测的停顿时间模型。保留新生代和老生代概念,但它们之间不是物理隔离,它们都是一部分Region(不需要连续)的集合。
  • 标记
    G1采用三色标记算法,用于标记GC Roots、存活节点、垃圾节点,针对可能被误标的情况采用STAB算法缓解。
    白(对象没有被标记到,标记阶段结束后会被回收);灰(对象被标记了,但它的filed还没有被标记或标记完);黑(对象被标记了,且它的所有filed也被标记了)。
    每个region逻辑上属于四种代的一种。四种代分别是:Old区(老对象)、Survivor区(存活对象)、Eden区(新生对象)、Humongous区(大对象:若对象很大,可能跨两个region)。

region分代图示:
在这里插入图片描述

G1垃圾回收器整体过程
分为四个阶段:初始标记阶段、并发标记阶段、最终标记阶段、筛选回收阶段。

阶段说明
初始标记阶段通过可达性分析标记GC Roots的直接关联对象,该阶段需要STW
标记:GC Root的直接关联对象
并发标记阶段通过GC Roots找存活对象,该阶段GC线程和用户线程同时运行,
标记时间比初始标记时间长
标记:关联对象
最终标记阶段重新标记,修正并发标记过程中因用户线程继续运行而导致新的引用更正,该阶段需要STW
筛选回收阶段对每个Region的回收成本进行排序,根据用户期望的停顿时间来制定回收计划,
即体现可预测停顿时间
该阶段GC线程和用户线程同时运行

优势

优势说明
并发收集充分使用CPU、多核环境下的硬件优势
空间整合整体上看是基于“标记-整理”算法、局部(Region之间)基于“复制”算法
分代收集保留新生代和老生代
可预测的停顿除了追求低停顿外,还建立可预测的停顿时间模型
能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

参考

说下你对垃圾回收策略的理解/垃圾回收时机?

JVM内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代,年轻代又分为Eden(伊甸)区和Survivor(幸存)区。
垃圾回收策略或垃圾回收时机 分为 Minor/Scavenge GC、Major GC、Full GC三种。

Minor / Scavenge GC

  • 概念:
    是在年轻代的Eden区进行,不影响到老年代。
  • 使用情况:
    速度快、效率高;
  • 触发条件:
    对象的创建都是在新生代的Eden区,当Eden区满都会触发Minor GC,将Eden区和Survivor0区存活对象复制到Survivor1区,清理掉Eden和Survivor0区,再将Survivor1区内容复制到Survivor0区。因为对象的内存分配都从Eden区开始,但Eden区又不会分配太大,则Eden区的GC会频繁进行。

Major GC

  • 概念:
    发生在老年代的GC,发生一次Major GC就会发生一次Minor GC,但Major GC的速度往往比Minor GC慢10倍。
  • 触发条件:
    对于一个大对象,首先在Eden区存放,创建不了触发Minor GC→继续尝试Eden区存放,还是不行→尝试存入老年区,也存不下→启动Major GC清理老年代的空间。

Full GC

  • 概念:
    对整个堆进行整理,包括年轻代、老年代、永久代。比Minor GC要慢,应该尽可能减少Full GC的次数。
  • 运行场景:
    ①调用System.gc(),会建议虚拟机运行Full GC,但不一定会执行;
    ②老年代空间不足,产生原因:大对象进入老年代、长期存活的对象进入老年代等;
    ③空间分配担保失败,解释:使用复制算法Minor GC需要老年代的内存空间担保,如果担保失败会执行一次Full GC;
    ④JDK1.7及以前的永久代空间不足;
    ⑤Concurrent Mode Failure执行CMS GC的过程中,同时有对象要放入老年代,而此时老年代空间不足(可能是GC过程中浮动垃圾过多导致暂时性的空间不足),便会报Concurrent Mode Failure错误,并触发Full GC。

Java堆分代管理2022/8/7
永久代:Permanent Generation,方法区/元空间。
在这里插入图片描述

谈谈你对对象内存分配的理解?大对象怎么分配?空间分配担保?

内存分配:

具体说明
对象优先在Eden存放大多数情况对象在新生代Eden区分配,空间不够发起Minor GC
大对象直接进入老年代避免存储在Eden和Survivor区导致大量的复制操作
长期存活的对象将进入老年代对象的年龄计数器,对象在Eden出现并经过Minor GC依然存活,并将其移动到Survivor,那么年龄就增加1岁,增加到一定年龄将其移动到老年代
动态对象年龄判定为了适应不同程序的内存情况,虚拟机不再限定到达某个值才进入老年代,而是找到Survivor空间中相同年龄所有对象大小的总和 大于 Survivor空间一半 的年龄,那么该年龄就作为分界线,Survivor空间中年龄大于或等于该分界线的对象 就可以进入老年代,无需等到MaxTenuringThreshold要求的年龄
空间分配担保发生Minor GC前,会先判断老年代是否有最大的连续空间大于新生代所有对象空间,有则认为是安全的
不成立,则查看HandlePromotionFailure设置值是否允许担保失败,允许则继续检查老年代最大可用的连续空间 是否大于 经过晋升到老年代对象的平均大小,大于则再次进行Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,则执行Full GC

在这里插入图片描述

对象的访问定位的两种方式?

对象的访问定位的方式是:句柄访问 和 直接指针访问 。

句柄访问

概念Java堆会划分出来一部分内存来作为句柄池,reference中存储的就是对象的句柄地址
reference存储的是对象的句柄地址
句柄包含 对象实例数据的地址(Java对象) 和 对象类型数据的具体地址(基本数据)
好处reference中存储着稳定的句柄地址,当对象移动之后(垃圾收集时移动对象是非常普遍的行为),只需要改变句柄中的对象实例地址即可,reference不用修改

案例:

Object obj = new Object();

Object obj 表示一个本地引用,存储在java栈的本地变量表中,表示一个reference类型的数据。
new Object() 作为实例对象存放在java堆中,
同时java堆中还存储了Object类的信息(对象类型、实现接口、方法等)的具体地址,
这些信息所执行的数据类型存储在方法区中。

直接指针访问

概念Java堆对象的布局中就必须考虑如何放置访问类型的相关信息(如对象的类型、实现的接口、方法、父类、field等)
reference存储的是对象的地址
好处访问速度快,它减少了一次指针定位的时间开销

扩展
java程序需要通过栈上的reference数据来操作堆上的具体对象。
reference类就是Java为引用类型定义的类,且是与Java垃圾回收机制密切相关的类。
Java虚拟机规范里面规定了reference类型是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问堆中对象的具体位置。

谈谈对java中引用Reference了解?

java中的引用有四种类型:强引用 、 软引用 、 弱引用 、 虚引用 。

类型概念
强引用
strong reference
只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象
Java引用默认是强引用,对任何一个对象的赋值操作就产生了对这个对象的强引用
类似 Object obj = new Object();
软引用
soft reference
用来描述一些还有用但并非必须的对象
只有在内存不足的情况下,被引用的对象才会被回收
使用SoftReference类创建软引用
弱引用
weak reference
被弱引用关联的对象只要垃圾回收机制执行,就会被回收。
使用WeakReference类创建弱引用
虚引用
phantomReference
幽灵引用 或 幻影引用 , 是最弱的一种引用
用于跟踪垃圾回收器收集对象的活动,如果发现该对象,GC会将引用放到ReferenceQueue(队列=存储待回收对象引用),程序员自己处理,执行ReferenceQueue.poll()方法,将引用从该队列移除,该引用变成inactive状态,表示可以回收

扩展
java中引用相关的内容:
通过引用计数算法判断对象的引用数量;通过可达性分析算法判断对象是否在引用链上;判定对象是否存活;

案例

# 强引用 strong reference
Object obj = new Object();
// new了一个新的对象,并将其赋值给obj,那么obj就是new Object()的强引用;

# 软引用 SoftReference
// SoftReference = public class SoftReference<T> extends Reference<T>
// 构造函数 = public SoftReference(T referent) 参数软引用对象、public SoftReference(T referent,ReferenceQueue<? super T> Q)是用来存储封装的待回收Reference对象的,ReferenceQueue中的对象是由Reference类中的ReferenceHandler内部类进行处理的。
Object obj = new Object();
SoftReference<Object> soft = new SoftReference<>(obj);
obj = null;

# 弱引用 WeakReference
// public WeakReference(T referent);public WeakReference(T referent, ReferenceQueue<? super T> q);与软引用类似
Object obj = new Object();
WeakReference<Object> weak = new WeakReference<Object>();
obj = null;

# 虚引用 phantomReference
// 构造函数 public PhantomReference(T referent, ReferenceQueue<? super T> q)
ReferenceQueue<Object> rp = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj,rq);
obj = null;

Reference
Reference是一个抽象类,每个Reference都有一个指向的对象,在Reference中有5个非常重要的属性:referent,next,discovered,pending,queue。
每个Reference都可以看成是一个节点,多个Reference通过next,discovered和pending这三个属性进行关联。

属性名含义
referentReference实际引用的对象
next创建ReferenceQueue
存放待回收的对象引用
discovered构建 Discovered List
pending构建 Pending List

Reference的四大状态
对于虚引用而言,GC会先将该引用放入ReferenceQueue队列中,程序员自行处理,若调用了ReferenceQueue.pull()后,该引用从队列中退出,此时引用的状态变成inactive状态。

状态说明
active如果改变状态,会变成inactive或pending状态
pending表示等待进入Queue,Reference内部有个ReferenceHandler,会调用enqueue方法,将Pending对象入到Queue中。
enqueued进入Queue对象的状态
inactive该状态下的Reference不能改变,会等待GC回收
enqueued状态的对象 通过poll方法弹出ReferenceQueue,该引用变成inactive状态

在这里插入图片描述

Reference 涉及到的三个queue或list
ReferenceQueue,本质是由Reference中的next连接而成,用来存储GC待回收的对象。
pendingList,待进入ReferenceQueue的list。
discovered List,在pending状态,就等于pending list;在Active状态的时候,discovered List实际上维持的是一个引用链。通过这个引用链,可以获得引用的链式结构,当某个Reference状态不再是Active状态时,需要将这个Reference从discovered List中删除。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值