JVM相关面试题

136 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情 >>

JVM相关面试题

1,内存模型

程序计数器:线程私有,一块较小的内存空间,是jvm执行程序的流水线,存放一些跳转指令,维护下一个将要执行指令的地址。(仅限于Java方法, Native方法该计数器值为undefined).

java虚拟机栈:线程私有,每个方法执行都会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法被调用至返回的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程(VM提供了-Xss来指定线程的最大栈空间, 该参数也直接决定了函数调用的最大深度).

本地方法栈:虚拟机使用到的Native方法服务

java堆:-Xmx最大堆内存 -Xms初始堆内存。所有线程共享,GC收集器管理的主要区域,jvm创建的对象存放于此,主要分为新生代和旧生代,而新生代又分为eden区(80%),from survivor(10%)和to survivor区,之所以这么分是因为新生代中98%的对象是朝生夕死, ##所以采用复制算法的GC策略,将eden和其中一块survivor中存活的对象复制到另一块survivor区,然后清理eden和用过的survivor区

方法区:所有线程共享,方法区是jvm规范中定义的一个概念,存放加载的类,常量,静态变量,JIT编译后的代码等数据。hotspot中用永久代来实现,别的jvm没有永久代的概念

在Java 6中,方法区中包含的数据,除了JIT编译生成的代码存放在native memory的CodeCache区域,其他都存放在永久代;

在Java 7中,Symbol的存储从PermGen移动到了native memory,并且把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内);

在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),-XX:MaxPermSize 参数失去了意义,取而代之的是-XX:MaxMetaspaceSize。

元空间:jdk1.8中移除永久代,而使用元空间,两者都是对jvm方法区的实现,不过元空间并不在虚拟机中,而是使用本地内存。

使用元空间代替永久代的原因:

1,字符串存在永久代中,容易出现性能问题和内存溢出
2,类及方法信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
3,永久代会为GC带来不必要的复杂度,并且回收效率偏低
4,oracle可能会将HotSpot与JRockit合二为一

内存分配原则:

对象优先在Eden分配,当没有足够空间触发Minor GC
大对象直接进入老年代,很长的字符串及数组都是大对象
长期存活的对象进入老年代,新生代中经历一次Minor GC年龄就增加一岁,当达到默认的15岁,就进入老年代,这个默认值可以设置

注:直接内存:直接内存并不是JVM运行时数据区的一部分, 但也会被频繁的使用: 在JDK 1.4引入的NIO提供了基于Channel与Buffer的IO方式, 它可以使用Native函数库直接分配堆外内存(java.nio.ByteBuffer.allocateDirect()), 然后使用DirectByteBuffer对象作为这块内存的引用进行操作(详见: Java I/O 扩展),这样就避免了在Java堆和Native堆中来回复制数据, 因此在一些场景中可以显著提高性能. 显然, 本机直接内存的分配不会受到Java堆大小的限制(即不会遵守-Xms、-Xmx等设置), 但既然是内存, 则肯定还是会受到本机总内存大小及处理器寻址空间的限制, 因此动态扩展时也会出现OutOfMemoryError异常.

2,GC机制

判断对象死亡算法

    • 引用计数法:给对象添加一个引用计算器,每当有地方引用它就加1,引用失效就减1,为0时就表示对象不再被使用,简单效率高,但是存在一个对象之间相互循环引用问题。
    • 可达性分析算法:主流语言都采用此算法判断对象存活,原理是对象到GC Roots是不可达的,就认定为此对象可回收。
    • 在java语言中,可作为GC Roots的对象包括:虚拟机栈中引用的对象、java堆类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象

垃圾收集算法

  1. 标记-清除算法:最基础的收集算法,后续算法都是基于这种思路改进。分为标记和清楚两个过程,但是效率不高,且产生大量不连续内存碎片。再分配大对象时,可能无法找到足够的连续内存而导致提前出发一次gc

  2. 复制算法:实现简单,效率高,现在商用虚拟机都采用这种算法来回收新生代。将eden和其中一块survivor中存活的对象复制到另一块survivor区,然后清理eden和用过的survivor区

  3. 标记整理算法:复制算法当对象存活率较高时效率会降低,所以根据老年代特点设计出这种算法,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  4. 分代收集:根据对象存活周期不同选择以上三种不同的算法去收集,java堆新生代使用 复制算法,旧生代使用 标记-整理算法

垃圾回收器

  1. 串行垃圾回收器Serial:最基本,最古老的收集器,只用一个单独的线程进行垃圾回收,冻结所有应用程序进行工作,客户端可能会用

  2. 并行垃圾回收器Parallel:使用多线程进行垃圾回收,停止其他所有应用程序线程.

  3. 并发标记扫描垃圾回收器CMS:基于标记-清除算法实现,主要有四步:初始标记-并发标记-重新标记-并发清除,初始标记,重新标记任要停止其他应用线程。并发收集,低停顿。缺点使用标记-清除算法

  4. G1垃圾回收器:目前最先进的收集器,替换CMS,将整个java堆划分为多个大小相等的独立区。特点:并行与并发、分代收集、空间整合(标记-整理)、 可预测停顿