JVM虚拟机面试典籍30+ | 大别山码将

JVM面试题总结

介绍java内存区域(运行时数据区)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n6UJczTB-1632497218260)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210822155357541.png)]

线程共享的:方法区,堆,直接内存 (⾮运⾏时数据区的⼀部分)

线程私有的:虚拟机栈,程序计数器,本地方法栈

Java 虚拟机在执⾏ Java 程序的过程中会把它管理的内存划分成若⼲个不同的数据区域。JDK1.8 和之前的版本略有不同

JDK1.8之前:运行时常量池在方法区中

JDK1.8时:运行时常量池在本地内存的元空间中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-If2A0F91-1632497218271)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210822154548728.png)]

程序计数器

程序计数器:

  • 物理地址是寄存器
  • 作用是记住下一条jvm指令的执行地址
  • 特点是线程私有的,是唯一不会存在内存溢出OutOfMemoryError 的内存区域,它的⽣命周期随着线程的创建⽽创建,随着线程的结束⽽死亡

程序计数器是⼀块较⼩的内存空间,可以看作是当前线程所执⾏的字节码的行号指示器。字节码解释器⼯作时通过改变程序计数器的值来依次读取下⼀条需要执⾏的字节码指令。分⽀、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

另外,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要有⼀个独⽴的程序计数器,各线程之间计数器互不影响,独⽴存储,我们称这类内存区域为线程私有的内存(在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了)

img

java虚拟机栈

栈:先进后出

  • 每个线程运行时所需要的内存称为虚拟机栈
  • 每个栈由多个栈帧(每个方法运行时所需要的内存)组成
  • 每个线程只能有一个活动栈帧(正在执行的那个方法)
  • Java 虚拟机栈也是线程私有的,每个线程都有各⾃的 Java 虚拟机栈,⽽且随着线程的创建⽽创建,随着线程的死亡⽽死亡

每个栈帧中都拥有:局部变量表、操作数栈、动态链接、⽅法出⼝信息。局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引⽤(reference 类型)

栈内存分配大小问题:栈内存分配越大,物理内存反而越来越少,而线程在物理内存中运行,线程会越来越少;栈内存大了通常只是能进行更多次的方法递归调用,而不会提高运行效率,反而会影响线程数目变少

方法内的局部变量如果是每个线程私有的,就不会有线程安全问题;方法内的局部变量如果是每个线程共享的(static),就会出现线程安全问题

外防输入,内防输出,才能保证线程安全

Java 虚拟机栈会出现两种错误:StackOverFlowError和OutOfMemoryError

  • StackOverFlowError:若 Java 虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最⼤深度的时候,就抛出 StackOverFlowError 错误
  • OutOfMemoryError 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也⽆法提供更多内存的话。就会抛出 OutOfMemoryError 错误

栈内存溢出StackOverflowError 问题:

通过-Xss设置虚拟机栈内存大小

  • 栈帧过多导致内存溢出(如:方法递归调用时如果处理不当,方法一直调用便会一直产生栈帧,导致栈内存溢出)
  • 栈帧过大导致内存溢出(这种情况一般出现概率很小)
  • 有时候在日常写代码时 第三方的库也会导致栈内存溢出

线程运行诊断:

案例一:cpu占用过多

img

案例二:程序运行长时间没有结果

img

本地方法栈

本地方法栈:为本地方法的运行提供内存空间

和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的Native⽅法服务。 在 HotSpot 虚拟机中和 Java虚拟机栈合⼆为⼀

本地⽅法被执⾏的时候,在本地⽅法栈会创建⼀个栈帧,⽅法执⾏完毕后相应的栈帧会出栈并释放内存空间,也会出现 StackOverFlowError 和OutOfMemoryError 两种错误

可通过-Xmx设置堆内存大小

  • 堆是线程共享的堆中的对象都需要考虑线程安全问题

  • 堆Heap分为:新生代(一般是一个Eden区,两个Survivor区),老年代(old区

  • 通过new关键字,创建对象都会使用堆内存

  • 有垃圾回收机制

Java 虚拟机所管理的内存中最⼤的⼀块,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,Java世界中⼏乎所有的对象都在堆中分配,从jdk 1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆.从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新⽣代和⽼年代:进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存。

在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下⾯三部分:

  • 新⽣代(Young Generation)
  • ⽼⽣代(Old Generation)
  • 永⽣代(Permanent Generation)

JDK 8 版本之后⽅法区(HotSpot 的永久代)被彻底移除了,取⽽代之是元空间,元空间使⽤的是直接内存。

堆内存溢出OutOfMemoryError问题:

堆有垃圾回收机制为什么还会产生内存溢出呢?

当有对象不断地在产生并且这些对象在不断地被使用,那么这些对象就不能当做垃圾被回收,就有可能造成内存溢出

堆内存溢出 OutOfMemoryError 错误出现之后的表现形式还会有⼏种:

  • OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执⾏垃圾回收并且只能回收很少的堆空间时,就会发⽣此错误。
  • java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不⾜以存放新创建的对象,就会发⽣此错误。

堆内存诊断工具:

img

方法区

⽅法区与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 方法区:所有java虚拟机线程共享的区域,它存储了跟类的结构有关的信息,如运行时常量池,类的成员变量,方法数据,成员方法和构造器方法的代码部分
  • 方法区在虚拟机启动时被创建,逻辑上是堆的组成部分
  • 永久代和元空间都是方法区的实现

方法区也被称为永久代⽅法区和永久代的关系很像Java中接⼝和类的关系,类实现了接⼝,⽽永久代就是 HotSpot 虚拟机对虚拟机规范中⽅****法区的⼀种实现⽅式。 也就是说,永久代是 HotSpot 的概念,⽅法区是 Java 虚拟机规范中的定义,是⼀种规范,⽽永久代是⼀种实现,⼀个是标准⼀个是实现,其他的虚拟机实现并没有永久代这⼀说法。

JDK 1.8 的时候,⽅法区(HotSpot 的永久代)被彻底移除了,取⽽代之是元空间,元空间使⽤的是直接内存。

为什么要将永久代 (PermGen)替换为元空间(MetaSpace) 呢?
  • 整个永久代有⼀个 JVM 本身设置固定⼤⼩上限,⽆法进⾏调整,太小容易出现永久代溢出,太大则容易导致老年代溢出;元空间使用的是系统内存(本地内存)便相对充裕很多,不会像永久代一样垃圾回收效率非常的低,导致内存溢出

  • 元空间⾥⾯存放的是类的元数据,这样加载多少类的元数据就不由永久代控制了,⽽由系统的实际可⽤空间来控制,这样能加载的类就更多了

  • 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有⼀个叫永久代的东⻄, 合并之后就没有必要额外的设置这么⼀个永久代的地⽅了。

  • 永久代会为 GC 带来不必要的复杂度,并且垃圾回收效率偏低

img

方法区内存溢出问题:

  • JDK1.8以前会导致永久代内存溢出java.lang.OutfMemoryError:PermGen space,可通过-XX:MaxPermSize设置永久代内存大小
  • JDK1.8及以后会导致元空间内存溢java.lang.OutfMemoryError:Metaspace,可通过-XX:MaxMetaspaceSize设置元空间内存大小
运行时常量池

常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息

运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址;运行时常量池存放编译期生成的各种字面量和符号引用;

运⾏时常量池是⽅法区的⼀部分,⾃然受到⽅法区内存的限制,当常量池⽆法再申请到内存时会抛出 内存溢出OutOfMemoryError 错误

  • JDK1.7之前运⾏时常量池逻辑包含字符串常量池存放在⽅法区,此时hotspot虚拟机对⽅法区的实现为永久代
  • JDK1.7字符串常量池被从⽅法区拿到了堆中,这⾥没有提到运⾏时常量池也就是说字符串常量池被单独拿到堆,运⾏时常量池剩下的东⻄还在⽅法区, 也就是hotspot中的永久代。
  • JDK1.8 hotspot移除了永久代⽤元空间(Metaspace)取⽽代之,在堆,运⾏时常量池还在⽅法区, 只不过⽅法区的实现从永久代变成了元空间

字符串常量池String Table(简称串池):

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池(如果有则不会放入)
  1. JDK1.8将这个字符串对象尝试放入串池,如果有则并不会放入;如果没有则把此对象放入串池,并把串池中的对象返回
  2. JDK1.6将这个字符串对象尝试放入串池,如果有则并不会放入;如果没有会把此对象复制一份放入串池,并把串池中的对象返回

假如不断地往StringTable内存空间中放入字符串对象,并且用一个长时间存活的对象来应用这些字符串对象,势必会造成内存空间不足;如果在1.6环境下运行,便会触发永久代内存运行不足OutOfMemoryError:PermGen space;如果在1.8环境下运行,它触发的内存不足是堆空间不足OutOfMemoryError:Java heap space(JDK1.8串池用的是堆空间,JDK1.6串池用的是永久代)

运行时常量池与字符串常量池?
字符串常量池:在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池.

StringTable调优:

调整-XX:StringTableSize=桶个数

案例:比如有大量的用户地址信息存储占用大量的内存空间,那么采用字符串intern()方法,可以去除重复的地址,相同的地址在串池中只会存储一份,这样就能减少字符串对于内存空间的占用

直接内存
  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高(使用direct memory进行大文件的读写效率会非常的高)
  • 不受JVM内存回收管理
  • OutOfMemoryError:Direct buffer memory 直接内存溢出

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存⼤⼩以及处理器寻址空间的限制。

文件读写流程:由于java代码访问不到存在系统缓存区中的数据,所以还要转存到java缓存区中才能读到,这样就造成了不必要的数据复制,使读取效率降低;这时在操作系统内直接划出一块缓冲区(横跨java缓冲区和系统缓冲区)也就是直接内存,这样使得系统和java代码都能访问,大大提高了数据读写效率

直接内存:释放通过Unsafe对象管理的(手动调用释放)

java内存:释放通过垃圾回收释放(自动调用释放)

使用-XX:+DisableExplicitGC禁用内存直接回收

分配和回收原理:

img

说一下java对象的创建过程

step1:类加载检查

JVM 会先去方法区找有没有所创建对象的类存在,有就可以创建对象了,没有则把该类加载到方法区

虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。

step2:分配内存

在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 中划分出来。

分配⽅式有 “指针碰撞" 和 "空闲列表"两种,选择哪种分配⽅式由Java堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定(取决于垃圾 收集器的算法是"标记-清除",还是"标记-整理",值得注意的是,复制算法内存也是规整的)

指针碰撞:

  • 适用场合:堆内存规整(即没有内存碎片)的情况下
  • 原理:用过的内存全部整合到一边,没用过的全部整合到另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可
  • GC收集器:Serial,ParNew

空闲列表:

  • 适用场合:堆内存不规整的情况下
  • 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录
  • GC收集器:CMS

在创建对象的时候有⼀个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采⽤两种⽅式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS 配上失败重试的⽅式保证更新操作的原⼦性。
  • TLAB:为每⼀个线程预先在 Eden 区分配⼀块⼉内存,JVM 在给线程中的对象分配内存 时,⾸先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时,再采⽤上述的 CAS 进⾏内存分配
step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。

step4:设置对象头

初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。

step5:执行init方法

在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的视⻆来看,对象创建才刚开始,init⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏init方法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来

对象在 JVM 中的创建过程如下:

  • JVM 会先去方法区找有没有所创建对象的类存在,有就可以创建对象了,没有则把该类加载到方法区
  • 在创建类的对象时,首先会先去堆内存中分配空间
  • 当空间分配完后,加载对象中所有的非静态成员变量到该空间下
  • 所有的非静态成员变量加载完成之后,对所有的非静态成员进行默认初始化
  • 所有的非静态成员默认初始化完成之后,调用相应的构造方法到栈中
  • 在栈中执行构造函数时,先执行隐式,再执行构造方法中书写的代码
  • 执行顺序:静态代码库,构造代码块,构造方法
  • 当整个构造方法全部执行完,此对象创建完成,并把堆内存中分配的空间地址赋给对象名

对象的访问定位有哪两种方式

创建对象就是为了使⽤对象,我们的Java程序通过栈上的 reference 数据(指向对象的引用)来操作堆上的具体对象。对象的访问⽅式由虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄②直接指针两种

1.句柄: 如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJseVoFK-1632497218317)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210830165759680.png)]

2.直接指针: 如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。如果只是访问对象本身的话,就不需要多一次间接访问的开销

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QuYCAuo-1632497218319)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210830165849800.png)]

这两种对象访问⽅式各有优势。

  • 使⽤句柄来访问的最⼤好处是** reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,⽽** reference 本身不需要修改。
  • 使⽤直接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。

简单聊聊JVM内存分配与回收

Java 的⾃动内存管理根本目标是自动化地解决两个问题:自动给对象分配内存和自动回收分配给对象的内存。对象的内存分配理论上都是在堆上分配

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新⽣代和⽼年代:再细致⼀点有:Eden 空间、From Survivor、To Survivor 空间等。进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存

堆空间的基本结构:

img

上图所示的 Eden 区、From Survivor0(“From”) 区、To Survivor1(“To”) 区都属于新⽣代,OldMemory 区属于⽼年代。

新生代中存放用完就会丢弃了的对象,老年代中存放长期使用的对象

所以新生代中的垃圾回收发生的比较频繁,老年代中垃圾回收长时间才发生一次

一开始对象都会优先分配在伊甸园区域,当伊甸园区域被占满,便会触发新生代的一次垃圾回收,为了提高内存利用率,幸存的对象如果能被幸存区容纳的话就会复制到幸存区,并且将其对象年龄设置为1岁,对象在幸存区每熬过一次minor gc,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中(对象晋升到老年代的年龄阈值可以通过参数设置);为了能更好的适应不同程序的内存情况,虚拟机不是一定要求对象的年龄必须达到阈值才能晋升到老年代,如果在幸存区中低于或等于某年龄的所有对象大小的总和大于幸存区空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到阈值要求的年龄;再工作一段时间,当幸存区的对象也比较多了或者幸存区的对象超过了一定时间,(虚拟机给每个对象定义了一个对象年龄计数器存储在对象头中,内存回收时根据年龄决定存活对象放在新声代还是老年代中)便会再触发新生代的垃圾回收,幸存区的一部分对象会晋升到老年代(垃圾回收时幸存的对象大小比幸存区大,所以只好通过分配担保机制提前转移到老年代去),没有超过时间的对象被复制到新的幸存区中,当然也有一些新生代的对象复制到新的幸存区中;在有限的暂停时间内,也会从老年代区中选取回收价值最高的区(O红色部分)进行垃圾回收,复制到新的老年代区中;新生代空间不足时,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加一并且交换from to,新生代垃圾回收会引发stop the world,暂停其他用户的线程,等待垃圾回收,用户线程才能恢复运行,当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长

空间分配担保:在发生minor gc之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,就可以确保这一次的minor gc是安全的;如果小于,在虚拟机担保失败的前提下,可以再次检查,如果大于,则会尝试进行一次minor gc,如果还是小于,就进行一次full gc;如果小于且不担保失败,也是进行一次full gc(响应时间会变得非常长,影响系统停顿时间和吞吐能力

为什么要担保?

如果大量对象在minor gc后任然存活,就需要老年代进行分配担保提高内存利用率,把幸存区无法容纳的对象直接送入老年代(前提是老年代能够容纳),如果老年代担保失败,就需要进行full gc来让老年代腾出更多的空间

大对象是指需要大量连续内存空间的java对象,比如:很长的字符串,元素数量庞大的数组,一个大对象大于内存区域的一半,虚拟机内存分配最怕大对象,尤其是短命大对象;原因是在垃圾回收时,容易导致内存内存充裕的情况下还要提前触发垃圾收集,复制对象时,大对象会产生高额的复制开销;解决:大对象直接进入老年代进行分配

新生代中大部分对象用过即死,且死亡对象的回收代价是零,minor gc的时间远远低于full gc

如果放入的对象太大,新生代和伊甸园都容纳不下时,就不会触发新生代垃圾回收了,对象会直接晋升到老年代

如果放入的对象太大**,新生代和伊甸园甚至老年代都容纳不下时,会抛出堆内存溢出异常**(它仍然会先尝试进行新生代垃圾回收,再尝试进行老年代垃圾回收,最后才不甘心的抛出堆空间不足异常)

⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到⼀定程度(默认为 15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

修正(issue552):“Hotspot 遍历所有对象时,按照年龄从⼩到⼤对其所占⽤的⼤⼩进⾏累积,当累积的某个年龄⼤⼩超过了 survivor 区的⼀半时,取这个年龄和MaxTenuringThreshold 中更⼩的⼀个值,作为新的晋升年龄阈值”。

经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的⻆⾊,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会⼀直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到⽼年代中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1VqOCfXL-1632497218329)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210901200602656.png)]

如何判断对象是否死亡

引⽤计数法:

给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它,计数器就加1;当引⽤失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使⽤的。

这种方法实现简单,效率高,但是它很难解决对象的循环引用问题,容易造成内存泄漏

可达性分析算法:

扫描堆中的对象,看是否能够沿着以 “GC Roots” 的对象作为起点的引用链找到该对象(从这些节点开始向下搜索,节点所⾛过的路径称为引⽤链),当⼀个对象到 GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的

Java语言中,做作为GC Roots的对象包括以下几种:

1)虚拟机栈(栈帧中的本地变量表)中引用的对象。

2)方法区中类静态属性引用的对象。

3)方法区中常量引用的对象。

4)本地方法栈JNI(即一般说的Native方法)引用的对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztVBqiii-1632497218330)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210902133641823.png)]

强引用,软引用,弱引用,虚引用

(严格说有五种引用:终结器引用)

无论是通过引用计数器算法判断对象的引用数量,还是通过可达性分析算法判断对象引用链是否可达,判断对象是否可活都离不开引用Java中将引用分为四种:

1.强引用是指程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只有强引用还存在对象就不会被回收。(如果所有GC Root对象都不引用该对象,该对象才能被垃圾回收)

2.软引用:软引用是用来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将于发生内存溢出异常之前,将会把这些对象列进回收范围中进行二次回收。

3.弱引用:也是用来描述非必须对象的,强度比软引用还弱一些,被软引用关联的对象只能存活到下一次内存回收之前。(在垃圾回收时,无论内存是否充足,都会回收弱引用对象

4.虚引用:"虚引⽤"顾名思义,就是形同虚设,与其他⼏种引⽤都不同,虚引⽤并不会决定对象的⽣命周期。如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被垃圾回收为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。虚引⽤主要⽤来跟踪对象被垃圾回收的活动。

虚引⽤与软引⽤和弱引⽤的⼀个区别在于:

虚引⽤必须和引⽤队列(ReferenceQueue)联合使⽤。当垃圾回收器准备回收⼀个对象时,如果发现它还有虚引⽤,就会在回收对象的内存之前,把这个虚引⽤加⼊到与之关联的引⽤队列中。程序可以通过判断引⽤队列中是 否已经加⼊了虚引⽤,来了解被引⽤的对象是否将要被垃圾回收。程序如果发现某个虚引⽤已经被加⼊到引⽤队列,那么就可以在所引⽤的对象的内存被回收之前采取必要的⾏动。

特别注意,在程序设计中⼀般很少使⽤弱引⽤与虚引⽤,使⽤软引⽤的情况较多,这是因为软引⽤可以加速JVM对垃圾内存的回收速度,可以维护系统的运⾏安全,防⽌内存溢出(OutOfMemory)等问题的产⽣

如何判断一个常量是废弃常量

运⾏时常量池主要回收的是废弃的常量。那么,我们如何判断⼀个常量是废弃常量呢?

假如在常量池中存在字符串 “abc”,如果当前没有任何String对象引⽤该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发⽣内存回收的话⽽且有必要的话,“abc” 就会被系统清理出常量池。

如何判断一个类是无用的类

⽅法区主要回收的是⽆⽤的类,那么如何判断⼀个类是⽆⽤的类的呢?

类需要同时满⾜下⾯ 3 个条件才能算是 “⽆⽤的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地⽅被引⽤,⽆法在任何地⽅通过反射访问该类的⽅法。

虚拟机可以对满⾜上述 3 个条件的⽆⽤类进⾏回收,这⾥说的仅仅是“可以”,⽽并不是和对象⼀样不使⽤了就会必然被回收。

垃圾收集有哪些算法,各自的特点

标记清除算法

Mark Sweep 先标记后清除

该算法分为“标记”和“清除”阶段:⾸先标记出所有不需要回收的对象在标记完成后统⼀回收掉所有没有被标记的对象。它是最基础的收集算法

优点:速度慢 缺点:容易产生内存碎片(空间不连续)

标记整理算法

Mark Compact 先标记后整理

标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。

优点:没有什么内存碎片 缺点:速度慢

复制算法

Copy 先标记后复制

它将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。

优点:没有什么内存碎片,速度较快 缺点:会占用双倍的内存空间

分代垃圾回收:

当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将堆内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代(主要是为了提升 GC 效率),这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

  • 新生代中存放用完就会丢弃了的对象,老年代中存放长期使用的对象
  • 所以新生代中的垃圾回收发生的比较频繁,老年代中垃圾回收长时间才发生一次

⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集

常见的垃圾回收器有哪些?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1a4LPovT-1632497218334)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210902141927674.png)]

如果说收集算法是内存回收的⽅法论,那么垃圾收集器就是内存回收的具体实现。

没有万能的垃圾收集器,我们能做的就是根据具体应⽤场景选择适合⾃⼰的垃圾收集器

Serial垃圾回收器:

Serial(串⾏)收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( STW),直到它收集结束。

Serial工作在新生代时采用的是复制算法,工作在老年代时采用的是标记整理算法

Serial 收集器由于没有线程交互的开销,所以与其他收集器的单线程相⽐,简单⽽⾼效。

ParNew 垃圾回收器:

ParNew 收集器其实就是 Serial收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和Serial 收集器完全⼀样。它是许多运⾏在 Server 模式下的虚拟机的⾸要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器)配合⼯作。

并⾏和并发概念补充:

并发:比如用户线程和垃圾回收线程是可以同时运行的(NOT STM)

并行:比如多个垃圾回收线程运行期间用户线程不能同时运行(STM)

Parallel Scavenge垃圾回收器:

Parallel Scavenge 收集器也是使⽤复制算法的多线程收集器,它看上去⼏乎和 ParNew 都⼀样。 那么它有什么特别之处呢?

Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS 等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)。所谓吞吐量就是CPU 中⽤于运⾏⽤户代码的时间与CPU 总消耗时间的⽐值。Parallel Scavenge 收集器提供了很多参数供⽤户找到最合适的停顿时间或最⼤吞吐量,如果对于收集器运作不太了解,⼿⼯优化存在困难的时候,使⽤Parallel Scavenge 收集器配合⾃适应调节策略,把内存管理优化交给虚拟机去完成也是⼀个不错的选择。

新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

Serial Old 垃圾回收器

Serial 收集器的⽼年代版本,它同样是⼀个单线程收集器。它主要有两⼤⽤途:⼀种⽤途是在JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使⽤,另⼀种⽤途是作为 CMS 收集器的后备⽅案。

Parallel Old 垃圾回收器

Parallel Scavenge 收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。在注重吞吐量以及CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器

CMS垃圾回收器

CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。是HotSpot虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。它⾮常符合在注重⽤户体验的应⽤上使⽤。

CMS 收集器是⼀种 “标记-清除”算法实现的,整个过程分为四个步骤:

初始标记,并发标记,重新标记,并发清除

**主要优点:**并发收集、低停顿。

但是它有下⾯三个明显的缺点:

  • 对 CPU 资源敏感;
  • ⽆法处理浮动垃圾(在垃圾回收时,用户线程运行产生的新的垃圾叫做浮动垃圾)
  • 它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。

G1垃圾回收器:

G1垃圾回收器

G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征

G1 收集器的运作⼤致分为以下⼏个步骤:

初始标记,并发标记,最终标记,筛选回收

G1收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region。这种使⽤ Region 划分内存空间以及有优先级的区域回收⽅式,保证了 G1 收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。

G1具备以下特点:

并⾏与并发:G1 能充分利⽤ CPU、多核环境下的硬件优势,使⽤多个 CPU来缩短 Stop-The-World 停顿时间。部分其他收集器回收垃圾时需要停顿 Java 线程,G1 收集器仍然可以通过并发的⽅式让 java 程序继续执⾏。

分代收集:虽然 G1 可以不需要其他收集器配合就能独⽴管理整个 GC 堆,但是还是保留了分代的概念。

空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

可预测的停顿:这是 G1 相对于 CMS 的另⼀个⼤优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为 M 毫秒的时间⽚段内。

适用场景:

  • 同时注重吞吐量和低延迟的,默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个内存相等的Region
  • 整体上是标记整理算法,两个区域之间是复制算法

ZGC垃圾回收器

一款低延迟垃圾回收器,ZGC 也采⽤标记-复制算法,不过 ZGC 对该算法做了重⼤改进。在 ZGC 中出现 Stop The World 的情况会更少!适用于大内存低延迟服务的内存管理和回收

img

高吞吐量垃圾回收器:Parallel GC

响应时间快垃圾回收器:CMS,G1,ZGC

谈谈 JVM 中的常量池

JDK 1.8 开始

  • 字符串常量池:存放在堆中,包括 String 对象执行 intern() 方法后存的地方、双引号直接引用的字符串
  • 运行时常量池:存放在方法区,属于元空间,是类加载后的一些存储区域,大多数是类中 constant_pool 的内容
  • 类文件常量池:constant_pool,JVM 定义的概念

谈谈对 OOM 的认识

除了程序计数器,其他内存区域都有 OOM 的风险。

  • 栈一般经常会发生 StackOverflowError。栈发生 OOM 的场景如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
  • Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效
  • 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错
  • 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等
  • 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请

你有哪些手段来排查 OOM 的问题?

谈谈双亲委派模型

Parents Delegation Model,这里的 Parents 翻译成双亲有点不妥,类加载向上传递的过程中只有单亲;parents 更多的是多级向上的意思。

除了顶层的启动类加载器,其他的类加载器在加载之前,都会委派给它的父加载器进行加载,一层层向上传递,直到所有父类加载器都无法加载,自己才会加载该类。

双亲委派模型,更好地解决了各个类加载器协作时基础类的一致性问题,避免类的重复加载;防止核心API库被随意篡改。

首先检查本类类加载器中有没有加载过这个类,如果没有,就看它有没有上级,如果有的话就递归调用上级类加载器的loadClass方法完成类加载,直到没有上级了;没有上级的类加载器便是启动类加载器;如果上级也没有,就回到本类类类加载器中找,如果找到了,就返回对应的值,如果没找到就返回class not find错误

线程上下文类加载器默认是应用程序类加载器

JDK打破了双亲委派模式,在DriverManager类初始化的时候,理应启用启用类加载器所有与它相关联的类的加载,实际上却使用了应用程序类加载器

所以JDK在某些情况下需要打破双亲委派模式,有时候会使用应用程序类加载器进行类加载,否则有些类是找不到的

说说 JVM 如何执行 class 中的字节码

JVM 先加载包含字节码的 class 文件,存放在方法区,实际运行时,虚拟机会执行方法区内的代码。Java 虚拟机在内存中划分出栈和堆来存储运行时的数据。

运行过程中,每当调用进入 Java 方法,都会在 Java 方法栈中生成一个栈帧,用来支持虚拟机进行方法的调用与执行,包含了局部变量表、操作数栈、动态链接、方法返回地址等信息。

当退出当前执行的方法时,不管正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

方法的调用,需要通过解析完成符号引用到直接引用;通过分派完成动态找到被调用的方法。

从硬件角度来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。翻译过程由两种形式:第一种是解释执行,即将遇到的字节一边码翻译成机器码一边执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。在 HotSpot 里两者都有,解释执行在启动时节约编译时间执行速度较快;随着时间的推移,编译器逐渐会返回作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。

什么是逃逸分析?方法内联?

即时编译器JIT将反复执行的代码编译成机器码,存储在代码缓存当中,下次再遇到这种代码时,就不会吧它解释成为机器码了,而是从代码缓存中将编译好的机器码直接拿来用,这样代码的执行效率就会很高

  • C1:做一些基本的优化,需要做信息统计操作profiling,在代码运行期间收集字节码运行状态的数据;如果发现某个方法被频繁的调用了,那么就会上升为C2编译器

  • C2:做一些更完全更彻底的优化

发现频繁执行频繁调用的代码并加以优化,这种优化手段称为逃逸分析

方法内联:为了减少方法调用的开销,可以把长度不长的热点方法内联,就是把方法内代码拷贝,粘贴到目标方法的调用范围之内,这样就少了一次方法调用,提升速度

说说 JVM 如何执行 class 中的字节码

方法区内存溢出怎么处理?

如何找到死锁的线程?

生产环境 CPU 占用过高,你如何解决?

生产环境服务器变慢,如何诊断处理?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李莲花*

多谢多谢,来自一名大学生的感谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值