JVM八股

Java内存区域详解

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

JVM的内存结构指的是JVM定义的运行时数据区域,包括程序计数器、虚拟机栈、本地方法栈、方法区、堆。

程序计数器

 

虚拟机栈

每个线程在创建的时候都会创建一个虚拟机栈,每次方法调用都会创建一个栈帧,每个栈帧都包含:局部变量表、操作数栈、动态链接和返回地址,栈帧随着方法的调用而创建,随着方法的结束而销毁。虚拟机栈作用:保存方法的局部变量,部分变量的计算并参与方法的调用和返回

本地方法栈

虚拟机栈用于管理Java函数的调用,而本地方法栈用于管理本地方法的调用
方法区

方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

 

堆被划分为新生代和老年代,新生代又分为Eden区和幸存区,幸存区由From和to组成

Eden区是类诞生、成长、死亡的地方。大多数新建的对象都位于伊甸园区,当伊甸园区对象被填满之后就会执行 MintorGC,并把所有存活下来的对象转移到一个幸存者区。幸存区(from 区、to 区)会不停的交换,谁清空谁是 to 区。

老年代内存里包含了长期存活的对象和经过多次 Minor GC 后依然存活下来的对象

Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)

Step1:类加载检查

检查类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。

Step3:初始化零值

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

Step4:设置对象头

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

Step5:执行 init 方法

执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。

  1. 对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  2. 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
  3. 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用,因为对象的大小必须是 8 字节的整数倍

对象的访问定位的两种方式(句柄和直接指针两种方式)

目前主流的访问方式有:使用句柄、直接指针

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

如果使用直接指针访问,reference 中存储的直接就是对象的地址。

 

Java回收垃圾详解

如何判断对象是否死亡(两种方法)

我们在进行垃圾回收(Garbage Collection,简称 GC)之前肯定要先判断哪些是垃圾

在堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡

(即不能再被任何途径使用的对象)。

  1. 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器+1,当引用失效时,计数器-1,任何时候当计数器为 0 的时候,该对象不再被引用。引用计数器这个方法实现简单,判定效率也高。但是,当前主流的虚拟机都没有采用这个算法来管理内存,其中最主要的原因是它很难解决对象之间互相循环引用的问题。
  2. 可达性分析算法:通过一系列的称为 “GC Roots”的对象作为起点,从这些节点开始

向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

 

简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)

  1. 强引用:如果一个对象具有强引用,,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
  2. 软引用:如果一个对象只具有软引用,如果内存空间不足了,垃圾回收器就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列( ReferenceQueue )联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
  3. 弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
  4. 虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收

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

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

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

方法区主要回收无用的类,类需要同时满足下面 3 个条件才能算是 “无用的类”

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

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并 不是和对象一样不使用了就会必然被回收。

垃圾收集算法

标记-清除算法

标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

这种垃圾收集算法会带来两个明显的问题:

  1. 效率问题:标记和清除两个过程的效率都不高
  2. 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集操作。

复制算法

它将可用内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。实现简单,运行高效。

虽然改进了标记-清除算法,但依然存在下面这些问题:

  1. 可用内存变小:可用内存缩小为原来的一半。
  2. 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。

标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,分代收集算法可以认为是一种思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

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

垃圾收集器

类加载详解

类加载器步骤

 

  1. 加载:在加载阶段,虚拟机需要完成以下三件事情:
  1. 1.通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 3.在内存中⽣成一个这个类的 java.lang.Class 对象,作为这个类的各种数据的访问接口。
  1. 验证

主要是为了确保 Class ⽂件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致上分为 4 个阶段的检验动作:⽂件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 准备
  1. 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存 都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而 不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。实例化不是 类加载的一个过程,类加载发⽣在所有实例化操作之前,并且类加载只进行一次,实例化可 以进行多次。
  2. 初始值是默认值 0 或 false 或 null。如果类变量是常量(final) , 那么会按照表达式来进 行初始化,而不是赋值为 0。public static final int value = 123;
  1. 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  1. 初始化

在准备阶段,变量已经赋过一次系统要求的初始值了,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。

类加载器总结

 

类加载器工作机制

双亲委派机制(Parent Delegation Model)是 Java 类加载器(ClassLoader)的一种 工作机制。通过这种双亲委派机制,Java 类加载器可以保证核心 Java 类库由最顶层的根加 载器加载,而不会被恶意代码替换。同时,这种机制也确保了类加载的一致性和可靠性,避 免了类的重复加载和冲突。

Java 类加载器采用了层次化的结构,形成了父子关系的层级结构。除了根加载器,每

个类加载器都有一个父加载器,当一个类加载器需要加载某个类时,它会先委派给父加载器

去尝试加载。只有当父加载器无法加载时,子加载器才会尝试自己加载。

 

这个双亲委派机制的工作流程如下:

1. 当一个类加载器收到加载类的请求时,它首先检查是否已经加载过该类。如果已经

加载过,则直接返回该类的 Class 对象。

2. 如果该类还没有被加载,加载器将请求委派给父加载器。

3. 父加载器接收到请求后,先检查自己是否已经加载过该类。如果已经加载过,则直

接返回 Class 对象。

4. 如果父加载器仍未加载该类,则将请求继续委派给它的父加载器,以此类推,形成

递归委派的过程。

  1. 如果所有的父加载器都无法加载该类,则最后由当前的类加载器尝试自己加载。

通过自定义类加载器,并重写它的加载类的逻辑,可以打破双亲委派机制,实现自定义

的类加载策略。这在一些特殊的应用场景下是有用的,例如实现模块化的类加载、动态代码

生成等。

总结来说,双亲委派机制是 Java 类加载器的一种工作机制,通过委派给父加载器来保

证类加载的安全性和一致性。它是 Java 安全模型的一部分,也是实现类加载器的重要机制

之一。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值