JVM面试题

文章目录

序号内容
1基础面试题
2JVM面试题
3多线程面试题
4MySql面试题
5集合容器面试题
6设计模式面试题
7分布式面试题
8Spring面试题
9SpringBoot面试题
10SpringCloud面试题
11Redis面试题
12RabbitMQ面试题
13ES面试题
14Nginx、Cancal
15Mybatis面试题
16消息队列面试题
17网络面试题
18Linux、Kubenetes面试题
19Netty面试题

有遇到过内存泄漏吗?你们是怎么解决的?

导致OOM的原因

给JVM的内存太小,实际业务需求对内存消耗较多。java代码中存在内存泄漏的问题,或者应用中创建的对象太多,在进行垃圾回收前,虚拟机分配到堆内存的空间已经满了。

-Xms 为jvm启动时分配的内存,比如-Xms2m,表示分配2M、

-Xmx 为jvm运行过程中分配的最大内存,比如-Xmx4m,表示jvm进程最多只能够占用4M内存。

-XX:+HeapDumpOnOutOfMemoryError 表示出现OutOfMemoryError异常时,记录快照。

-XX:HeapDumpPath 表示快照的存储位置(这里可以设置文件名字,也可以不设置),不设置名字它会自己生成的。

OutOfMemoryError: PermGen space

方法区溢出:Permanent Generation Size,用来存储被加载的类的定义(class definition)和元数据(metadata),比如:Class Object和Method Object等。这是内存中的一块永久保存区域,JVM的垃圾回收不会触及这块区域。通常在加载一个非常大的项目的时候才会出现该异常,或者采用cglib等反射机制引起,另外过多的常量,尤其是字符创也会导致方法区溢出。

如果是在tomcat中出现这个问题,解决办法是在{tomcat_dir}/bin/catalina.bat中添加如下一行:

set CATALINA_OPTS=-server -Xms256m -Xmx1024m -XX:PermSize=512m -XX:MaxPermSize=512m

OutOfMemoryError: Java heap space

堆内存溢出:Java应用程序创建的对象存放在这片区域,垃圾回收(Garbage Collection)也发生在这块区域。通常一些比较“重型”的操作可能会导致该异常,比如:需要创建大量的对象,层次比较深的递归操作等。。

如果是在tomcat中,出现的这种问题,解决办法是在{tomcat_dir}/bin/catalina.bat中找到如下几行:

在后面加上一行(数字根据自己的需要调整):set CATALINA_OPTS=-Xms512m -Xmx512m

// 查看代码,看是否存在死循环或者不必要的重复创建对象。可以通过内存监控软件查找泄漏的代码(jps,jconsole)。

线上排查流程

1、首先要配置-XX:+HeapDumpOnOutOfMemoryError 和 -XX:HeapDumpPath= /app 两个参数。
2、复现OOM,生成hprof快照文件(java_pid7876.hprof)
3、使用scp 将生成的hprof快照文件(scp dump.dat root@10.0.31.205:/usr) 文件从容器中copy出来,然后下载到win上
4、通过mat(https://www.eclipse.org/mat/previousReleases.php 1.120版本)工具分析hprof快照文件,找出对应的溢出代码。
5、通过对象池重复使用对象和手动设置对象为null来帮助GC 。
6、在dockerfile 总共设置了-Xmx JVM堆内存为物理内存的80%
MAT:
1、选择File --> 导入生成的hprof快照文件 -->
在这里插入图片描述
找到对应的OOM的方法
在这里插入图片描述

内存泄漏和内存溢出

内存泄漏是指申请完的内存没有释放,导致虚拟机不能再次使用该内存,因为申请者不用了,但是又不能被虚拟机回收,给别的对象使用

内存溢出是指 申请内存超过JVM能够提供的内存大小

对JVM的理解

Java虚拟机,是Java程序运行的基础。其主要工作原理包括代码编译、类加载、执行、内存管理、垃圾回收和反射等。

  • 代码编译:Java程序被编译成Java字节码
  • 类加载:JVM将Java字节码文件加载到内存,并对其进行解析和验证。如果字节码文件有语法错误,JVM会拒绝载入并输出错误信息。
  • 执行:JVM对Java字节码进行解释运行(解释器执行方式)或JIT(Just In Time,即时编译器)编译为本地代码后再执行。
  • 内存管理:JVM负责管理Java程序执行过程中所使用的内存,包括程序计数器、本地方法栈、栈(线程私有的),堆、方法区
  • 垃圾回收:JVM提供自动垃圾回收的机制,定时回收不再使用的对象并释放内存。其垃圾回收机制(GC)是将内存中不再被使用的对象进行回收,以优化内存使用。
  • 反射:Java程序可以在运行时获取自己的类信息并对其进行操作。

JVM的这些特性和功能使得Java成为一种跨平台的语言,因为Java字节码可以在任何支持Java虚拟机的平台上运行,而无需对源代码进行修改。同时,JVM的内存管理和垃圾回收机制也大大简化了Java程序员的内存管理任务,提高了程序的健壮性和可靠性。

jvm基础

程序的执行方式

动态解释为主,动态编译为辅 的执行方式。

字节码和机器码的区别

字节码:是需要虚拟机翻译后才能成为机器码。class 文件这样

机器码:电脑CPU直接读取的运行的机器指令。 0101 这样的。

JDK、JRE、JVM

JDK:JDK包含jre,java开发工具包

JRE:运行类库 包含 jvm

JVM:java 虚拟机

jvm的运行模式

server 模式 和 client 模式。

clinet 启动速度快。 server 模式 启动速度慢 ,但是稳定后 server运行速度要比 client 快

哪些程序代码会被即时编译(JIT)

热点代码。热点数据不要在jvm上执行,可以直接在操作系统上执行。省掉了jvm将class 文件解释给操作系统的步骤。

什么是热点代码

虚拟机发现某个方法或者代码块运行特别频繁的时候,就会把这些代码定义为热点代码。

1、被多次调用的方法

2、被多次执行的循环体

热点代码是如何判断的

基于计数器的热点探测。计数器又分为:方法调用计数器和回边计数器。

即时编译器

C1 、C2。

JIT优化

1、公共表达式消除

抽象表达式。

2、方法内联

方法在执行的时候会进行压栈,方法内联就是 A 方法 调用B方法的时候 ,正常情况下会压栈两次,如果调用的方法过于频繁 ,jvm 会把方法的调用替换成方法的本身。例如 A方法调用 B 方法 , B方法的内容 是 X+Y 。方法内联前 A 方法中 是 class.B , 经过内联后,A方法 不在 是 class.B , 而是 X + Y。就是将B方法的中代码放到A 方法中执行。这样栈内存中的 栈帧数就会减少,

3、逃逸分析

方法逃逸:当一个对象在方法中被定以后,它可能被外部别的方法所引用。就是当成参数传到另一个方法中。称为方法逃逸。

全局变量赋值逃逸:定义一个全局变量,在A方法中中引用这个全局变量对象。称为全局变量赋值逃逸。

方法返回值逃逸: 一个有返回值的方法, 将这个对象返回出来。就是方法返回值逃逸

public static StringBuffer creatStringBuffer(String s1,String s2) {
	Stringuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	return sb;// 这种称为方法返回值逃逸。

}
//优化策略
public static String createStringBuffer(String s1,String s2) {
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	String str = sb.toString();
	return str;//String 逃逸了,但是StringBuffer 没有逃逸,String属于字符串常量池,有另一套优化策略。
}

java 代码运行时,可以通过jvm参数指定是否开启逃逸分析

-XX:+DoEscapeAnaylsis (开启), -XX:-DoEscapeAnaylsis(关闭)

4、同步锁消除

基于逃逸分析,当加锁的变量不会发生逃逸的时候,是线程私有的完全没必要加锁,JIT在编译的时候就会将锁去掉。

例如上面 StringBuffer 的代码 , StringBuffer 是加了synchronized , 如果StringBuffer对象没有发生逃逸,则JIT会将synchronized 锁去掉

对象的栈内存分配

JIT编译器可以在编译期间根据逃逸分析的结果来决定是否可以将对象的内存分配从堆转化为栈。

常量池

1、字面量:final修饰的基础数据类型、String 字符串、没有被final修饰的 double、float 、long 会进入常量池。

2、符号引用:类的全限定名、方法的名称和描述符、字段的的名称和描述符。

Long 和 Double 占8个字节空间,在class字节码中该类型的常量是如何存储的

int 占4个字节空间 byte 占一个字节空间。

tag = 5 , height_byte , low_byte 。 线程不安全 , 因为CPU按照最低4个字节处理,long 和 double 8个字节 ,需要拆分为高字节和低字节,去执行底层

SPI Service Provider Interface

JDK内置的动态扩展点的实现。我们可以定一个一个标准接口,然后第三方类库中可以实现这个接口。那么在程序运行的时候,会根据配置信息动态加载第三方实现类,从而完成动态扩展机制。例如数据库驱动:java.jdbc.Driver ,JDK定义了数据库驱动Driver,但是只是一个接口,JDK并没有提供实现,而是有第三方数据库厂商来实现。

JVM类加载

什么时候触发类加载

遇到new 、getstatic、putstatic、invokestatic 这四个指令的时候 会进行初始化

类加载机制

1、全盘负责:当一个类加载器负责加载某个类时,会将这个类所引用的类也通过当前加载器加载。(类加载过程的解析的直接引用)

2、双亲委派:当类加载器加载一个类时,会先把这个请求委托给父类加载器,如果父类加载器还存在其他的父类加载器 , 则依次向上递归,直到最终,如果父类加载器可以完成加载,就成功返回。如果不能完成加载,则子加载器才会去加载。

3、缓存:当程序中需要某个class的时候,类加载会先从缓存中读取class,如果没有,系统会去二进制文件并转为class,存入缓冲区

类加载/启动过程(类的生命周期)

加载(载入):将class文件转换成二进制字节流加载到内存,并生成一个代表该类的java.lang.class对象。

验证:jvm 对二进制字节流进行校验,只有符合jvm规范才能被执行。是用 -Xverify:none 关闭验证。

准备:对静态变量分配内存,并初始化值该数据类型的默认值,比如int 类型 默认0 ,String 类型 默认null。boolean false 。

解析:将常量池中的符号引用转化为直接引用。解析阶段就是将加载阶段的符号引用替换成直接引用。

​ 符号引用:A、B两个类,在编译阶段发现A类中引用B类,在A类被编译的时候,A不知道B类是否被编译,所以A不知道B的实际在内存中的地址,这个时候A的class中使用一个字符串类的全限定名代替B的实际内存地址。在编译阶段,会将B的内存地址用字符串替代。所以符号引用是发生在加载的时候的。

​ 直接引用:在运行的时候,如果A发生了类加载,到解析阶段发现B类还未被加载,这时就会触发B的类加载,将B加载到虚拟机中,这时A中的符号引用会被替换成B的实际地址。如果B类使用了多台,B是一个接口,C类或者D类实现B接口,此时A类不知道B类的具体实现是哪个类。直到运行过程中发生调用,此时虚拟机调用栈中会知道具体的类型信息,这时在进行直接解析。

初始化:在准备阶段已经将静态变量赋默认值,这个阶段就是将变量赋值为期望值。例如成员变量和静态变量的赋值,new 对象。

类加载器

类加载器:根据类的全限定名读取此类的二进制字节流到jvm中,并转为class对象。

bootstrap class load (启动类加载器):C++实现,加载rt.jar

extension class loader(扩展类加载器): 加载java_home/lib/ext的类库

application class loader(引用程序类加载器):加载环境变量classpath,和三方类库。

user class loader(自定义类加载器):

什么是双亲委派

在一个类加载器收到加载请求,它不会首先加载这个类,而是会把这个请求委托给父类加载器,如果父类加载器还存在其他的父类加载器,则进一步委托,一次递归,请求最终会达到顶层的启动类加载器。如果父类可以完成加载,就成功返回。如果父类加载器不能完成请求,则子类加载器才会去加载。

默认情况下一个限定名的类只会被一个类加载器加载并解析使用。

作用:保证JDK核心类优先加载

如何打破双亲委派

为什么打破:因为每个类加载器都有自己的加载范围,当一个类不在 4 个类加载器的范围之内的时候,就需要委托子类加载器去加载这个类。例如:最顶层的Booststrap class loader 无法加载 mysql driver 驱动

1、自定义类加载器,使用 loadClass。如果自定义加载器不想破坏双亲委派,重写findClass。

2、使用线程上下文类加载器。

3、热部署会对双亲委派破坏

能不能自己写一个限定名为"java.lang.String"的类,并在程序中调用

能。但必须交由Bootstrap ClassLoader加载。可以通过-Xbootclasspath/p: 参数指定加载目录。

JVM加载Class文件的原理机制

ava中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

类装载方式,有两种 :

1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,

2.显式装载, 通过class.forname()等方法,显式加载需要的类

Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

JVM内存

运行时数据区

JVM的主要组成部分及作用

JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。

Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。

Execution engine(执行引擎):执行classes中的指令。

Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。

Runtime data area(运行时数据区):这就是我们常说的JVM的内存

作用

首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

常量池的区别

class常量池:存放符号引用,存在于class文件中。物理磁盘中

字符串常量池:存放字符串,字符创常量池全局唯一。存在jvm内存中 (方法区中)

运行时常量池:类被解析后存放解析后的直接引用,每个类一个运行时常量池。存在jvm内存中。

元空间和永久代

元空间和永久代是方法区的不同实现。jvm中方法区被所有线程共享,jvm的线程都能访问方法区。方法区中存放的是运行时常量池,和class结构。JDK1.7之前 HotSpot虚拟机对方法区的实现称为永久代,1.8之后对方法区的实现称为 元空间

永久代:FULL GC时永久区不会被清理。永久区的大小在启动的时候被指定,不能改变。字符串常量位于永久区(1.8以后,字符串常量位于堆。)

元空间:不设定大小的话就是物理机的内存,如果指定大小,满了就会触发GC。会触发FULL GC清理。字符串常量位于堆

元空间的本地内存区域

为什么要用元空间替代永久代

在 Java7 里面,在 Hotspot 虚拟机中,方法区的实现是在永久代里面,它里面主要存储运行时常量池、Klass 类元信息等。永久代属于 JVM 运行时内存中的一块存储空间,我们可以通过-XX:PermSize来设置永久代的大小。当内存不够的时候,会触发垃圾回收。

在 JDK1.8 里面,Hotspot 虚拟机中,取消了永久代,由元空间来实现方法区的数据存储。元空间不属于 JVM 内存,而是直接使用本地内存,因此不需要考虑 GC 问题。默认情况下元空间是可以无限制的使用本地内存的,但是我们也可以使用 JVM参数来限制内存使用大小

jvm 内存模型

堆内存

方法区: 类信息、常量、静态变量、即时编译后的代码。


栈内存

本地方法栈:本地方法的栈。是由其他语言编写的,交由java运行的,使用native关键值修饰的,Thread.sleep()

程序计数器:一个很小的控件用来记录当前线程执行到当前字节码的行号。只有程序计数器不会产生内存溢出

堆内存

虚拟机启动的时候创建堆内存,存对象实例(不是所有的对象都存在堆内存中)和数组。

堆内存负责存放对象引用,发生GC的地方。

堆内存多线程情况下,线程不安全的问题。

CAS:比较交换(compare and swap) 。乐观锁。每次内存分配的时候都不加锁,认为没有冲突,如果有冲突导致失败,就重试,知道成功为止。

TLAB:(Thread Local Alloction Buffer 线程本地分配缓存区)多线程情况下,为了保证线程不冲突。会预先给每个线程都分配一块内存空间。jvm在给线程的对象分配内存的时候,会先在TLAB上分配,如果TLAB剩余的空间不能满足当前对象内存的大小,再采用CAS进行内存分配

堆内存分配

一般来说,创建的对象内存会分配到堆内存中。

如果没有发生对象逃逸,则内存会被分配到栈内存中, 这样对象随着栈帧的出栈而被销毁,会减少堆内存的使用。

TLAB: (Thread Local Alloction Buffer)多线程情况下,为了保证线程不冲突。会预先给每个线程都分配一块内存空间。jvm在给线程的对象分配内存的时候,会先在TLAB上分配,如果TLAB剩余的空间不能满足当前对象内存的大小,再采用CAS进行内存分配

对象内存分配的两种方法

指针碰撞:内存是连续的,serial、parNew 垃圾收集器。

空闲列表:内存地址不连续。CMS和使用标记-整理算法的垃圾收集器。

内存分配流程

类加载发生在内存分配之前。先加载再分配内存

TLAB:Thread Local Alloction Buffer ,线程缓冲区。

创建对象 => 先进行栈分配(根据是否逃逸决定是否在栈进行分配)=>(没有逃逸)TLAB分配(根据对象的大小和TLAB剩余空间的大小决定)=>是否可以进入老年代=>Eden 分配

分配完后 执行 类加载 过程中的 **准备 **步骤

内存分配策略

对象优先在Eden区分配

多数情况下,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。

大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。因为新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配

长期存活的对象进入老年代

虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。

对象的访问方式(对象的访问定位)

句柄:可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。

直接指针: 指向对象,代表一个对象在内存中的起始地址。

句柄访问

Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。它的优势是引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针

如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。优势是速度更快,节省了一次指针定位的时间开销。由于对象的访问在java中非常频繁,因此,这类开销积少成多后也是很多的执行成本。HotSpot中采用的是这种方式。

栈内存

栈负责存放基础数据类型 和方法, boolean byte short int long char double float 。

如果对象没有发生逃逸,栈内存中会存放对象。

栈是线程私有的,最小单位是栈帧,栈帧中包含局部变量表,操作栈,动态链接,方法出口

递归调用,死循环 会形成栈内存溢出。是因为递归会一直调用自己,如果方法一直没有执行完毕,就会一直压栈,直到内存溢出。StackOverFlowError。

也会出现OOM异常:栈可以动态拓展,当拓展无法申请到足够的内存空间的时候会出现OutOfMemoryError。

栈帧

方法调用或者执行过程中,虚拟机使用的最小单位。用于支持虚拟机进行方法调用和执行的数据结构。

每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程

返回地址:

动态链接:将类中的符号引用转化为直接引用。类加载的底四个过程 “解析”。

局部变量表:存方法中声明的局部变量。第一个位置:this引用。第二个:方法形参,第三个:方法声明的局部变量。是有顺序的。

操作数栈:

局部变量表和操作栈的存储容量都是以32位的大小来分配。如果想存储double和long需要两个连续的slot来存储。

为什么会有GC

堆内存中引用的对象在方法结束后不能释放 , 栈内存在方法结束后会自动删除 ,

对象传递到另一个方法中,如何改变了属性值 ,当前方法的对象也会跟随改变 。但是基本数据类型不会跟着改变 , 因为基本数据类型是放在栈内存中, 方法结束 栈内存会将 写入的释放掉

程序计数器

一个很小的控件用来记录当前线程执行到当前字节码指令的行号。只有程序计数器不会产生(OOM异常)内存溢出。

多线程情况下,每个线程都以一个独立的程序计数器。当前线程在等待其他线程执行的时候,会记录当前的行号,等其他线程执行完,再根据记录的行号去接着执行指令。

本地方法栈

本地方法中的非java方法,由其他语言实现的方法。需要通过java方法调用的,使用native修饰的。例如java方法需要与底层操作系统或者硬件信息进行交互信息的时候,只通过调用这些native接口实现就行。

本地方法栈是通过C语言实现的。

和栈(stack)一样是后进先出。栈顶的先出。

怎么知道对象的年龄是15(实例对象的内存结构)

对象在内存存储分为三块区域:

对象头:hash码,GC分代年龄(对象头知道对象的年龄),线程持有的锁,8字节的整数倍

实例数据:对象真正有效的信息。根据数据大小占用不同的内存空间

对象填充:保证内存大小是8字节的整数倍而去进行对齐填充

实例对象内存中存储在哪

如果实例对象存储在堆内存中:实例对象内存存在堆区,实例的引用存在栈上,实例的元数据(class)存在方法去或者元空间。

如果对象没有发生逃逸,实例对象会分配到栈内存中。

对象一定存在堆内存吗

不一定。创建对象会先在栈内存中进行分配,如果发生逃逸,则进入线程缓冲区(TLAB),如果对象过大,则进入堆内存。如果是大对象会直接进入老年代,不是大对象会进入Eden区。

方法的调用

jvm 是如何找到要执行的方法的

根据类名方法名和方法描述符

JVM的静态绑定和动态绑定

静态绑定:在编译期间就能确定目标方法。叫静态绑定。final,static,private修饰的方法,构造方法。invokestatic,invokespecial指令是调用静态绑定的方法

动态绑定:在运行期间才能确定目标方法。叫动态绑定。invokevirtual,invokeinterface指令是调用动态绑定的方法。

动态绑定是如何找方法的 : 先从本地栈的变量表中找实际类型,找到实际类型后,就可以找到实际类型的方法表,最后根据调用信息去找实际方发表中对应的索引的方法

调用指令

invokevirtual:对象实例方法

invokeinterface:接口中的方法

invokespecial:特殊的方法(实例初始化init方法,私有方法,父类方法)

invokestatic:调用静态方法

invokedynamic:lambda表达式

动态绑定优化方法

使用内联缓存:key 是调用者的实际类型,value 是目标方法的内存地址。

oop-klass模型

垃圾回收

垃圾回收机制

在java中,程序员不需要显示的去释放一个对象的内存。在JVM中有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有当虚拟机空闲或者堆内存不足时,才会触发执行,扫描那些没有被引用的对象,并将它们添加到要回收的集合中,进行回收。

GC 是什么?为什么要GC

GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。

如何查看垃圾回收日志

-XX:+PrintGC

-XX:+PrintGCDetail

启动参数中添加

如何判断对象是可以被回收的

垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。一般有两种判断方式:

可达性分析算法:jvm的垃圾回收需要判断对象是否存活,是不是垃圾,可达性分析算法就是通过GC ROOT(根集合)中的对象链式的去找这个根对象所引用的对象,这些对象都是存活的、不能被回收的对象。

引用计数法:给对象做一个计数器,被引用一次就加 1 ,没有被引用就减 1 。一单计数器显示为 0 ,就会被判断为垃圾。缺点:对象间的相互引用。

对象什么时候可以被回收

当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。

永久代会发生垃圾回收吗

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。

垃圾回收过程

如果对象不可达,这时对象处于“缓刑”阶段,需要经过两次标记过程

第一次标记:通过可达性分析算法发现没有于GC ROOT相连的可用链,对象会被第一次标记

第二次标记:第一次标记后,看这个对象是不是必须执行finalize()方法,如果有执行finalize方法且没有建立引用关系 会被第二次标记。

如果建立了引用关系,对象会继续存活。

方法区的垃圾回收

主要回收废弃常量和无用的类。

为什么堆中要分新生代和老年代

主要是为了放置不同的对象。新生代放的是新创建的对象,内存较小,垃圾回收次数比较频繁。老年代主要放置生命周期比较长的对象、大对象,内存占比较大,垃圾回收也不是很频繁。

GC线程是如何和用户线程并行的

GC-ROOT是如何枚举的

使用 oop map

GC是如何处理跨代的

查 card table。 一个对象在新生代,没有被任何对象引用,但是被老年代的对象引用 称为跨代引用。如果遍历老年代的对象代价太大。

卡表:HotSpot 通过写屏障技术来维护卡表

写屏障:可以理解AOP切面,在有对象的操作后 去更新卡表。

新生代和老年代的区别

1、新生代分区会分为三个 , Eden(伊甸园)、survivor0,、survivor1 分布内存比例为 8:1:1 。老年代只有一个区域。

2、新建的对象通常会被分配到新生代的 Eden区, 但是大对象会被直接分配到老年代。

3、堆内存中,新生代和老年代的内存分配比例为 1:2

4、新生代的Eden区,空间不足了之后会发生minor GC,此时位于survivor区的对象年龄会加1 , 默认加到15 ,这个对象就会 晋升到老年代

5、如果老年代内存满了 , 会执行Full GC。

新生代为什么分为Eden和survivor

如果没有survivor区,minor GC后存活的独享都会被复制到老年代,由于新生代垃圾清理次数比较频繁,老年代会很快被minor GC清除后存活的对象占满,从而触发Full GC,由于老年代内存区域较大,每执行一次GC消耗的时间也比较长。survivor存活的意义就是为了减少Eden区像老年代频繁复制对象,减少Full GC的次数

survivor区为什么分为survivor0和survivor1

因为新生代大都使用的是复制算法,需要一个空白的区域让Eden和另一个survivor存活的对象复制进来

垃圾回收算法

1、标记-清除:标记-清除算法是从根集合(GC-ROOT)开始找存活的对象,标记需要回收的对象, 标记完成后,将这些对象清除掉。但是对象的位置不会发生改变,这样就导致了内存碎片化。如果对象比较多会进行大量的标记和清除动作,效率会降低。

2、标记-整理:标记整理算法解决了内存碎片化的问题。标记-清除算法也是从根集合(GC-ROOT)开始查找存活的内存的,因为标记整理算法是先把存活的内存进行标记,标记完成后会先移动对象,再清除未被标记的对象。但是成本更高。

3:、标记-复制:复制算法是将内存按容量划分成两块相同的内存,每次只使用其中一块,当这一块内存被填满后,将存活的对象复制到另一块内存中。浪费内存。

4、分代:将内存划分为新生代和老年代,堆内存外还有一个永久代。新生代使用复制算法,但是并不是将新生代按1:1的比例分配,而是8:1:1,就是所谓的Eden和survivor0 和 survivor1 ,当Eden区满了之后,触发minor GC,将存活的对象用复制算法复制到survivor区中。老年代使用的是标记-整理算法,从根集合进行遍历,将存活的对象打上标记,完成标记后,将存活的对象向前排序,然后再清理未被标记的对象。

Minor GC 和 Major GC(Full GC)

Minor GC:是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;

Major GC(Full GC):发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。

垃圾回收器

1、10种垃圾回收器

六种可以根据新生代和老年代进行划分,分别一一对应:

新生代:serial parallel scavenge parNew

老年代:serial old parallel old CMS

java 9中开始使用 G1 垃圾回收器,

ZGC垃圾回收器,

一、serial 和 serial old

serial 和 serial old 也是分代的, 是单线程模式的垃圾回收器, serial 回收新生代,serial old 回收老年代。单线程、回收的时候必须STW,适合单核CPU。

serial:复制算法

serial old :标记整理算法

二、parallel scavenge 和 parallel old

parallel scavenge 和 parallel old 是多线程的垃圾回收器,关注点是吞吐量(高效率利用CPU) 。CMS关注点是用户线程停顿时间。jdk版本1.8的默认回收器,

parallel scavenge:复制算法,并发多线程

parallel old : 多线程 标记整理算法

三、parNew 和 CMS

parNew:复制算法

CMS: concurrent mark sweep (并行标记-清除)

多线程垃圾回收器。CMS是并发垃圾回收。垃圾回收线程和业务线程同时执行。CMS的垃圾回收线程是一直在运行。

CMS多个垃圾回收线程不会重复标记。

CMS多个垃圾回收线程并行执行,第一次执行的时候会标记存活的线程,等别的垃圾回收线程执行完 ,这个垃圾回收线程再去执行的时候 对象可能会发生改变。

CMS并发执行的时候对象是在不断发生变化的。CMS最主要的目的是为了减少STW时间 ,parallel scavenge 是为了提高CPI利用率

CMS垃圾回收步骤

初始标记:暂停其他所有的线程,并标记GC ROOT 直接引用的对象。速度很快。

并发标记:用户线程和垃圾回收线程同时开启。找出GC ROOT这个链条的所有对象,由于用户线程会不断更新引用,GC线程无法保证可达性分析的实时性

重新标记:修正并发标记因用户线程导致变动的那部分对象,这个阶段会STW.

并发清理:用户线程和GC线程同时执行,GC线程对未标记的对象进行清除。

优缺点

并发收集、低停顿。

1、会和用户线程抢资源

2、无法处理浮动垃圾

3、会产生空间碎片。通过 -XXUseCMSCompactAtFullCollection,让jvm执行完标记清除后再整理。

4、执行过程的不确定性。上次垃圾回收线程还没执行完的时候,由于是并发执行的,可能会导致再次出现垃圾回收的情况。一边回收,系统一边运行,也许还没回收完就触发了 full GC ,此时会STW,使用 serial old 单线程老年代垃圾收集器回收垃圾。remark 阶段。

三色标记算法

三色:是指对象的三种状态,通常使用 黑–>灰–>白。三色标记算法主要是标记对象 标记是重点

黑:垃圾回收线程已经找到当前对象,并且找到了当前对象所有的引用的对象(找到了所有的孩子),当前对象会被标记为黑色,下次不会再查找这个对象

灰:垃圾回收线程已经找到当前对象,但是当前对象所引用的对象诶有被完全找到,当前对象会被标记为灰色,下次查找从这个对象开始

白:垃圾回收线程完全没有找到当前对象,会被标记为白色

漏标

A:黑色,B:灰色,D:白色(A—>引用B,B----->引用D)

当垃圾回收线程被CPU调度休息的时候,业务线程运行,将 B和D 的引用 断开了 , 同时又使 A 引用了D , 但是A 是黑色, 垃圾回收线程再次执行的时候,是不会找A 的。

解决方案:通过写屏障 (), 在A引用D的时候,将A标为灰色,等垃圾回收线程再次执行的时候就会找到A,然后再找到对应的D。还有可能发生漏标。

CMS有remark阶段,这个阶段必须STW,目的是为了将所有的节点重新扫描一遍。

CMS使用增量更新和写屏障的方法解决漏标的问题。

写屏障:

增量更新:

CMS两个问题:

1、浮动垃圾问题

2、remark阶段 , 这个阶段是要STW。无法避免的。

CMS浮动垃圾(漏标)

在垃圾回收线程等待的时候,业务线程执行,将一个对象和另一个已经标记的对象的应用断开了,

CMS的老年代内存满了的时候,会触发STW,然后使用单线程清理整个老年代内存

四、G1

jdk 9 中将java8的默认垃圾回收器parallel scavenge 和 parallel old 替换成 了G1。G1垃圾回收器不再使用分代模型,而是使用分区模型。

G1采用的是分区算法,不再使用分代。把内存均分成一个一个的小块,物理分区,逻辑还是分代(Eden survivor,old,humongous)。

每个区域可能会随时变化。

humongous:特殊区。存放大对象。如果一个对象超过eden区域的 50%的,会被直接放到humougous中,如果大小超过一个区域,会使用多个连续的区域存放。FULL GC会回收humougous区域。

年轻代:堆内存占比 5%,但是年轻代会随着系统的运行而增长,最多捕获超过60%。eden和survivor的占比也是8:1:1

从Eden到old 的过程和 其他分代的过程是一样的。

过程

初始标记:暂停其他所有线程,并记录GC ROOT能引用的所有的对象。

并发标记:用户线程和GC线程同时开启。找出GC ROOT链条上的所有对象,

最终标记:

筛选回收:筛选回收阶段首先会对各个region区域的回收价值和成本进行排序,根据设置的STW时间(-XX:MaxGCPauseMillis,默认200ms)来回收部分垃圾。(根据排序完的结果,估算stw的时间能回收到哪块内存就停止,剩下的内存区域下次再回收)。使用复制算法,复制到相邻的还没指定的区域。

垃圾收集分类

1、young GC:eden区域满了之后不会立即触发young GC,而是会先计算young GC的时间,如果时间远远小于 设置的最大STW时间(-XX:MaxFCpauseMillis),则会增加年轻代的区域,等Eden再次满了之后,在计算时间,如果接近就触发yong GC,如果还是远远小于,就接增加年轻代。

2、Mixed GC:老年代的 堆占有率 达到设定的值(-XX:InitiatingHeapOccupancyPercent)之后触发,回收所有的young 和 部分 old以及大对象区域(Humougous)。

3、Full GC:Mixed GC在清理老年代的时候,需要把老年代的r区域复制到另一个区域中,如果发现没有足够的区域能够承载拷贝,就会触发Full GC。Full GC会STW,使用单线程去标记-清理,压缩整理,回收垃圾。很耗时。

G1核心算法SATB(原始快照)

为了解决浮动垃圾的问题。在灰色指向白色的引用消失的时候,会将这个 引用 放到堆栈中,当垃圾回收线程在次执行的时候,会扫描这个白色的对象,查看这个白色的对象是否有被引用,如果被引用,就将这个白色标记成灰色,如果没有,就当成垃圾。

RSet=RememberedSet

G1使用写屏障和原始快照的方法处理浮动垃圾(漏标)

原始快照(SATB)

RSET和卡表
G1参数设置

1、-XX:UseG1GC:使用G1垃圾收集器。

2、-XX:PraallelGCThread:指定GC工作线程数量

3、-XX:G1HeapRegionSize:指定分区大小(必须是2 的N 次幂),默认将堆划分为2048和区域

4、-XX:MaxGCPauseMillis:目标暂停时间。默认200ms。

5、-XX:G1NewSizePercent:新生代内存初始空间(默认5%。)

6、-XX:G1MaxNewSizePercent:新生代内存最大空间

什么场景适合使用G1

1、8G以上堆内存(4G一下可以使用parllel,4-8G使用CMS,几百G使用ZGC)

2、停顿时间在500ms以内的

3、垃圾回收时间特别长的

4、50%以上的堆被活对象占用的

五、ZGC

jdk11 中使用 ZGC,主要是针对堆空间非常大的时候使用的一种垃圾回收器。java16中ZGC的STW在一毫秒之内。

CMS收集器和G1收集器的区别

1、CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
2、G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
3、CMS收集器以最小的停顿时间为目标的收集器;
4、G1收集器可预测垃圾回收的停顿时间
5、CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
6、G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

什么是GC ROOT,哪些可以作为GC ROOT

一个只有引用对象,而且是没有被别的对象引用过的对象为根集合。

1、栈中的本地变量

2、正在运行的线程

3、本地方法栈中的变量

4、方法区中的静态变量

什么是STW

STOP THE WORLD:程序停止运行。业务线程在运行的时候触发了垃圾回收线程 , 这个时候就会导致业务线程停止运行。业务线程暂停之后再启动垃圾回收线程

什么是可达性分析算法

jvm的垃圾回收需要判断对象是否存活,是不是垃圾,可达性分析算法就是通过GC ROOT(根集合)中的对象链式的去找这个根对象所引用的对象,这些对象都是存活的、不能被回收的对象。

强引用、软引用、弱引用、虚引用

引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用

强引用

我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用

弱引用

无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值