春招面试题总结--jvm

1、jvm内存区域

写在前面

1、基本问题

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

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

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

2、拓展问题

  • String 类和常量池
  • 8 种基本类型的包装类和常量池

1.1运行时内存区域

1613201278336 1613201320585

**程序计数器:**记录下一条指令地址,从而实现如顺序执行、循环跳转等等;在多线程中,存在线程切换,程序计数器可以记录当前线程地址,方便线程切换回来以后继续执行。

**虚拟机栈:**存放编译期可知的各种基本数据类型和对象引用。

**本地方法栈:**为虚拟机使用到的native方法服务。

**堆:**存放对象实例。另外堆中还有字符串常量池。

关于堆中内存区域的分配:堆中分eden区、s区、old区,新生对象被放在eden区,eden区满出发垃圾收集,并采用可达性分析标记,被标记的存活对象被放到s1区,其余对象杀死;再次向eden区放入新生对象,满后,用可达性分析分析eden区和s1区对象存活情况,存活的对象被放到s2区,此时s1区清空,如此往复循环。当对象年龄达到阈值15以后会被放入老年代,另外如果某个年龄超过了s区内存的一半,取这个年龄和年龄阈值之间更小的值作为新阈值。

**方法区:**存放类名、字段、静态成员、方法。方法区在深入理解java虚拟机这本书中是一个逻辑概念,并不是一个真正意义上的物理分区,真正的物理分区叫做永久代(1.7)/元空间(1.8)。

  • 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

​ 在使用永久代时,使用两个参数限制它的初始大小和最大大小,分别是permsize和maxpermsize,这样可能会抛出outofmemoryerror,而元空间使用的是直接内存,受本机可用内存的限制,虽然也可能会产生溢出,但是几率会小很多,且可以加载的类也更多。

img

1.2、HotSpot 虚拟机对象探秘

1.2.1、对象的创建

**类加载检查:**当遇到new指令时,去常量池检查有没有这个对象的引用,并且检查这个引用代表的类有没有被加载过,如果没有,则进行相应的类加载。

**分配内存:**为新生对象分配内存——指针碰撞(内存规整);空闲列表(内存不规整)。

——>内存分配的并发问题(创建对象操作在实际开发中很频繁,需要确保线程安全):

1、CAS+失败重试,保证更新操作原子性

2、TLAB, 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

**初始化零值:**为分配到的内存空间初始化零值,保证java代码可以不赋初值使用。

**设置对象头:**对对象进行必要的设置。

**执行init方法:**此时从虚拟机角度,对象已经产生了,但从程序视角来看对象创建才刚开始,在new指令后紧接一个init方法,把对象按照程序猿的意愿进行初始化。

1.2.2、对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

**对象头:**第一部分用于存储对象自身的运行时数据mark word(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针class point,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

**实例数据:**程序中定义的各种类型的字段内容。

**对齐填充部分:**仅起占位作用,对象地址大小必须是8字节的整数倍。

2、JVM垃圾回收

写在前面

  • 如何判断对象是否死亡(两种方法)。
  • 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
  • 如何判断一个常量是废弃常量
  • 如何判断一个类是无用的类
  • 垃圾收集有哪些算法,各自的特点?
  • HotSpot 为什么要分为新生代和老年代?
  • 常见的垃圾回收器有哪些?
  • 介绍一下 CMS,G1 收集器。
  • Minor Gc 和 Full GC 有什么不同呢?

2.1 堆内存常见分配策略

  • 对象优先在eden取分配

    当新生代内存不够时,采用担保机制提前将新生代的对象转移到老年代中去。

  • 大对象直接进入老年代

    大对象是指需要大量连续内存空间的对象。为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

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

    存活在新生代中的对象有一个年龄阈值(一般为15)超过这个阈值,就会将对象存入老年代。

2.2 对象死亡

2.2.1 如何判断一个对象已经无效

  • 引用计数法(对象引用数量):给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

  • 可达性分析(引用链是否可达):以GC Roots对象作为起点,向下搜索指定对象,所经过的路径称为引用链,如果某个对象不存在引用链,则说明这个对象是不可用的。

    –>**可以作为GC Roots的对象:**虚拟机栈中引用的对象;本地方法栈中引用的对象;静态属性引用的对象;常量引用的对象;被同步锁锁住的对象。

    凡是在引用链上的对象都会被标记,放到s区,没有被标记的就会被清楚。

2.2.2 对象死亡一定会被回收吗

没有被可达性分析标记的对象并不是非死不可,它们处于一个缓刑阶段,要真正宣告死亡还要经历两次标记。首先是进行可达性分析,没有与GC Roots相连的对象将会被第一次标记并进行一次筛选,筛选条件时这个对象有没有必要执行finalize方法,如果覆盖了会把它放到F-Queue队列里,进行第二次标记。

2.2.3 关于finalize方法

​ 如果一个对象覆盖了 finalize() 方法,那么在真正被宣告死亡的时候,至少需要经过两次标记。第一次被标记的时候会被放在 一个 F-Queue 队列中,finalize() 方法是对象逃脱死亡命运的最后一次机会。在第二次标记的时候,如果该对象成功与引用链(GC-Roots)上的任何一个对象关联,那么它仍然可以存活下来,否则将会被垃圾收集器回收。

2.2.4 Full gc

首先明确三个概念:

Minor GC : 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存 ;

Major GC : 对老年代GC ;

**Full GC :**对整个堆GC, 经常伴随至少一次的Minor GC,但非绝对 。FGC管理的内存比较大,效率比较慢,STW时间较长,所以尽量不要出发FGC。

触发条件:

  1. System.gc()

    调用System.gc()方法会出发full gc.

  2. 老年代空间不足

    老年代当中存放的对象有以下几种情况:新生代当中存活时间较长(超过一定年龄阈值)的对象、大对象、大数组,如果老年代当中空间不足了,就会出发full gc。

  3. 永久代空间不足

    永久代就是我们熟知的方法区,其中存放有类的基本信息、常量、静态变量等数据,当系统当中要加载的类、反射的类、调用的方法太多时,永久代可能会占满,出发full gc.

  4. CMS GC时出现promotion failed和concurrent mode failed

    对于采用CMS进行老年代垃圾收集的程序而言,要注意日志中是否有promotion failed和xonxurrent mode failed两种情况,他们可能会触发full gc.

2.2.5 再谈引用

  • 强引用

    ​ 是类*必不可少*的引用,垃圾回收器绝对不会回收它,及时内存内存空间不足,java虚拟机宁可抛出outofmemeoryerror也不会回收强引用。

  • 弱引用

    ​ 是类可有可无的引用,如果内存空间足够,垃圾回收器就不会回收弱引用,如果内存空间不够,就会回收。

  • 软引用

    ​ 是类可有可无的引用,无论内存空间是否足够,垃圾回收器都会回收软引用,但因为垃圾回收器是一个优先级很低的线程,因此不会很快发现弱引用对象。

  • 虚引用

    ​ 虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都会被垃圾回收。它主要用来跟踪对象被垃圾回收的活动。

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

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

2.3 JVM调优

目标:减少GC时用户线程的暂停时间

方法:JVM参数设置和垃圾收集器选择

2.3.1 关于STW

**为什么:**如果没有STW,假设在程序运行过程中突然发生了gc,通过可达性分析从gc roots找非垃圾对象,假设我们找完了,因为没有STW,线程会继续往下执行,且执行结束,而这个时候垃圾收集还没有做完,此时线程所占的内存区域会全部被释放掉,意味着gc roots存储的内存空间也被释放掉了,这个时候之前找出的非垃圾对象就会变成新的垃圾。堆当中有几十、上百万的对象,我们通过分析找出非垃圾对象,gc还没有结束对象的状态又变了,变成垃圾对象,难道又返回去把这几十万的对象再遍历一次吗,显然不现实。所以Java开发人员在设计之初,索性用了stw,让用户线程暂停住,把所有的非垃圾对象找出来,把垃圾对象干掉,这样一次gc的时间可能会更快,效率会更高。

2.3.2 JVM参数调优

背景介绍:

1618902907740

调优前:每个对象60M,一旦有对象进入S区,由于超过S区大小的一半,会被存入old区,无法被minor gc回收,根据old区大小,五六分钟old区就会满发生full gc。

1618902892180

调优后:把年轻代变大。

1618902852796

2.3.3 垃圾回收器

  • **Serial 收集器:**串行

    只使用一条垃圾收集线程去完成垃圾收集工作,且在进行垃圾收集时需要停止其他所有工作线程,直到收集结束。**新生代采用“标记-复制”,老年代采用“标记-整理”。**简单而高效。

  • **ParNew 收集器:**多线程

    在serial收集器的基础上多条垃圾收集线程并发执行。新生代采用“标记-复制”,老年代采用“标记-整理”。

  • **Parallel Scavenge 收集器:**多线程

    Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。是 JDK1.8 默认收集器新生代采用“标记-复制”,老年代采用“标记-整理”。

  • **CMS 收集器:**并发收集器(用户线程与垃圾收集线程并发执行)

    以获取最短回收停顿为目标,注重用户体验。采用 “标记-清除”算法

    运行过程:**初始标记——**暂停所有用户线程,记录下与root相连的对象,速度很快。

    ​ **并发标记——**同时开启GC和用户线程,记录可达对象。

    重新标记——标记并发标记期间由于用户线程运行而产生变动的那一部分对象。

    ​ **并发清除——**开启用户线程,同时GC对于未标记线程进行清扫。

  • **标记收集器:**满足GC停顿时间要求的同时还具备高吞吐量,主要针对配置多颗处理器和大内存的机器。

1618906659416

1.8默认的垃圾回收:PS+ParallelOld

2.3 垃圾收集算法*

  • **标记-清除算法:**首先标记出不需要回收的对象,然后回收掉所有没有被标记的对象。

    –>会产生大量不连续的内存碎片

  • **标记-复制算法:**将内存分为大小相等的两块区域,每次使用其中一块。当一块内存使用完之后,把还存活的对象复制到另一块内存去,再把使用过的空间一次清理掉。

    –>浪费内存空间

  • **标记-整理算法:**在标记-清除的基础上,让所有存活的对象向一端移动,然后清理掉端边界以外的内存。

  • **分代收集算法:**java堆一般分为新生代和老年代,在新生代中,对象死亡率高,可以使用“标记-复制算法”;在老年代中对象死亡率低,可以使用“标记-清除”或“标记-整理”算法。

3、执行引擎

4、类加载子系统

4.1 加载

将.class文件加载进内存,存放在方法区,然后创建一个class对象,用来封装类的数据结构。

–>类加载器:非数组类由java虚拟机直接创建,数组类由类加载器创建,创建完成后均由类加载器加载。

1、 BootstrapClassLoader(启动类加载器) :最顶层的加载类, 负责加载 %JAVA_HOME%/lib目录下的jar包和类 。

2、 ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类

3、 AppClassLoader(应用程序类加载器) : 面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

–>双亲委派模型

java应用程序由若干个.class文件组成,文件之间经常发生调用关系。而在程序启动时,class文件并不会一次性加载进内存,而是根据程序需要,使用类加载机制(classloader)来动态的加载某个class文件到内存当中,只有class文件被加载进内存以后才能被其他class文件调用。

**classloader加载类的原理:**使用双亲委派模型来搜索类。 每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

**jvm如何判定两个class文件相同:**不仅要判定两个类名是否相同,而且要判定是否由同一个classloader加载。

**优点:**避免重复加载,如果父类已经加载了该类的时候,子类就没有必要再加载一次。也保证了 Java 的核心 API 不被篡改。

**不想用双亲委派模型怎么办:**使用自定义类加载器的话需要继承ClassLoader类。如果不想打破双亲委派模型就重写ClassLoader类中的findClass方法,当父类加载器无法加载时,就会通过这个方法加载。如果想打破就需要重写loadClass方法。

4.2 验证

确保class文件的合法性,不会危害虚拟机自身安全。

验证阶段示意图

4.3 准备

为类变量分配内存并且设置初始值。

4.4 解析

将常量池内的符号引用转换为直接引用的过程。

4.5 初始化

img

4.6 常见面试题

4.6.1 什么是类加载机制?

虚拟机把描述类的数据从class文件中加载到内存中,并对数据进行检验、转换解析和初始化,最终形成能够被虚拟机直接使用的java类型。

4.6.2 Java类加载过程

加载–>验证–>准备–>解析–>初始化–>使用–>卸载

4.6.3 什么时候会发生类加载

  1. 使用new关键字实例化对象、调用类的静态方法、读取类的静态字段;
  2. 触发反射机制时;
  3. 初始化一个类的时候,如果发现其父类还没有进行初始化;
  4. 虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。

4.6.4 jvm加载class文件的原理机制

4.6.5 类加载器的双亲委派模型是什么?可以被打破吗?为什么?

初始化,最终形成能够被虚拟机直接使用的java类型。

4.6.2 Java类加载过程

加载–>验证–>准备–>解析–>初始化–>使用–>卸载

4.6.3 什么时候会发生类加载

  1. 使用new关键字实例化对象、调用类的静态方法、读取类的静态字段;
  2. 触发反射机制时;
  3. 初始化一个类的时候,如果发现其父类还没有进行初始化;
  4. 虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。

4.6.4 jvm加载class文件的原理机制

4.6.5 类加载器的双亲委派模型是什么?可以被打破吗?为什么?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值