写点什么

10 个经典又容易被人疏忽的 JVM 面试题

  • 2022 年 5 月 12 日
  • 本文字数:3665 字

    阅读完需:约 12 分钟

//公众号:捡田螺的小男孩


StringBuilder?str?=?new?StringBuilder();


str.append(a);


str.append(b);


return?str;


}


//不直接返回 StringBuffer,不发生逃逸


public?String?notEscape(String?a,?String?b)?{


//公众号:捡田螺的小男孩


StringBuilder?str?=?new?StringBuilder();


str.append(a);


str.append(b);


return?str.toString();


}


//外部线程可见 object,发生逃逸


public?void?objectEscape(){


object?=?new?Object();


}


//仅方法内部可见,不发生逃逸


public?void?objectNotEscape(){


Object?object?=?new?Object();


}


}


「逃逸分析的好处」


**?**栈上分配,可以降低垃圾收集器运行的频率。同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且 GC 频率也会减少。?


2.虚拟机为什么使用元空间替换了永久代?


====================


「什么是元空间?什么是永久代?为什么用元空间代替永久代?」?我们先回顾一下**「方法区」**吧,看看虚拟机运行时数据内存图,如下:



?



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



?


「什么是永久代?它和方法区有什么关系呢?」


?



如果在 HotSpot 虚拟机上开发、部署,很多程序员都把方法区称作永久代。可以说方法区是规范,永久代是 Hotspot 针对该规范进行的实现。在 Java7 及以前的版本,方法区都是永久代实现的。



?


「什么是元空间?它和方法区有什么关系呢?」


?



对于 Java8,HotSpots 取消了永久代,取而代之的是元空间(Metaspace)。换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。



?


「为什么使用元空间替换了永久代?」


  • 永久代的方法区,和堆使用的物理内存是连续的。



**「永久代」**是通过以下这两个参数配置大小的~


  • -XX:PremSize:设置永久代的初始大小

  • -XX:MaxPermSize: 设置永久代的最大值,默认是 64M


对于**「永久代」,如果动态生成很多 class 的话,就很可能出现「java.lang.OutOfMemoryError: PermGen space 错误」**,因为永久代空间配置有限嘛。最典型的场景是,在 web 开发比较多 jsp 页面的时候。


  • JDK8 之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器**「内存有多大,元空间就有多大」**。



可以通过以下的参数来设置元空间的大小:


?-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时 GC 会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 MaxMetaspaceSize 时,适当提高该值。-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。-XX:MinMetaspaceFreeRatio,在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集-XX:MaxMetaspaceFreeRatio,在 GC 之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集?


「所以,为什么使用元空间替换永久代?」


?



表面上看是为了避免 OOM 异常。因为通常使用 PermSize 和 MaxPermSize 设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到 OOM 错误。当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制啦。



?


3.什么是 Stop The World ? 什么是 OopMap?什么是安全点?


=======================================


进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为**「Stop The World」**。


在 HotSpot 中,有个数据结构(映射表)称为**「OopMap」。一旦类加载动作完成的时候,HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来,记录到 OopMap。在即时编译过程中,也会在「特定的位置」**生成 OopMap,记录下栈上和寄存器里哪些位置是引用。


这些特定的位置主要在:


  • 1.循环的末尾(非 counted 循环)

  • 2.方法临返回前 / 调用方法的 call 指令后

  • 3.可能抛异常的位置


这些位置就叫作**「安全点(safepoint)。」**?用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。


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


=====================



JVM 包含两个子系统和两个组件,分别为


**?**Class loader(类装载子系统)Execution engine(执行引擎子系统);Runtime data area(运行时数据区组件)Native Interface(本地接口组件)。?


  • 「Class loader(类装载):」?根据给定的全限定名类名(如:java.lang.Object)来装载 class 文件到运行时数据区的方法区中。

  • 「Execution engine(执行引擎)」:执行 class 的指令。

  • 「Native Interface(本地接口):」?与 native lib 交互,是其它编程语言交互的接口。

  • 「Runtime data area(运行时数据区域)」:即我们常说的 JVM 的内存。


?



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



?


5. 守护线程是什么?守护线程和非守护线程的区别是?守护线程的作用是?


====================================


「守护线程」是区别于用户线程哈,「用户线程」即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种「通用服务的线程」。垃圾回收线程就是典型的守护线程。


「守护线程和非守护线程的区别是?」?我们通过例子来看吧~


/**


*?关注公众号:捡田螺的小男孩


*/


public?static?void?main(String[]?args)?throws?InterruptedException?{


Thread?t1?=?new?Thread(()->?{


while?(true)?{


try?{


Thread.sleep(1000);


System.out.println("我是子线程(用户线程.I?am?running");


}?catch?(Exception?e)?{


}


}


});


//标记为守护线程


t1.setDaemon(true);


//启动线程


t1.start();


Thread.sleep(3000);


System.out.println("主线程执行完毕...");


}


运行结果:



可以发现标记为守护线程后,「主线程销毁停止,守护线程一起销毁」。我们再看下,去掉 t1.setDaemon(true)守护标记的效果:


public?static?void?main(String[]?args)?throws?InterruptedException?{


Thread?t1?=?new?Thread(()->?{


while?(true)?{


try?{


Thread.sleep(1000);


System.out.println("我是子线程(用户线程.I?am?running");


}?catch?(Exception?e)?{


}


}


});


//启动线程


t1.start();


Thread.sleep(3000);


System.out.println("主线程执行完毕...");


}



所以,当主线程退出时,JVM 也跟着退出运行,守护线程同时也会被回收,即使是死循环。如果是用户线程,它会一直停在死循环跑。这就是**「守护线程和非守护线程的区别」**啦。


守护线程拥有**「自动结束自己生命周期的特性」,非守护线程却没有。如果垃圾回收线程是非守护线程,当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是「为什么垃圾回收线程需要是守护线程啦」**。


6.WeakHashMap 了解过嘛?它是怎么工作的?


==========================


「WeakHashMap」?类似 HashMap ,不同点在 WeakHashMap 的 key 是**「弱引用」**的 key。


谈到**「弱引用」**,在这里回顾下四种引用吧


**?**强引用:Object obj=new Object()这种,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。软引用: 一般情况不会回收,如果内存不够要溢出时才会进行回收弱引用:当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。虚引用:为一个对象设置虚引用的唯一目的只是为了能在这个对象被回收时收到一个系统的通知。?


正是因为 WeakHashMap 使用的是弱引用,「它的对象可能随时被回收」。WeakHashMap 类的行为部分**「取决于垃圾回收器的动作」,调用两次 size()方法返回不同值,调用两次 isEmpty(),一次返回 true,一次返回 false 都是「可能的」**。


WeakHashMap**「工作原理」**回答这两点:


**?**WeakHashMap 具有弱引用的特点:随时被回收对象。


发生 GC 时,WeakHashMap 是如何将 Entry 移除的呢??


WeakHashMap 内部的 Entry 继承了 WeakReference,即弱引用,所以就具有了弱引用的特点,「随时可能被回收」。看下源码哈:


private?static?class?Entry<K,V>?extends?WeakReference<Object>?implements?Map.Entry<K,V>?{


V?value;


final?int?hash;


Entry<K,V>?next;


/**


*?Creates?new?entry.


*/


Entry(Object?key,?V?value,


ReferenceQueue<Object>?queue,


int?hash,?Entry<K,V>?next)?{


super(key,?queue);


this.value?=?value;


this.hash??=?hash;


this.next??=?next;


}


......

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
10个经典又容易被人疏忽的JVM面试题_Java_爱好编程进阶_InfoQ写作社区