转载

面试半年,凭借这份JVM面试题,我终于拿到了字节跳动的offer!

内存区域

虚拟机栈

生命周期与线程相同,描述的是Java 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存取局部变量表、操作数栈、动态链接、方法出口等信息

本地方法栈

与虚拟机栈作用相似,只不过本地方法栈是为虚拟机使用到的Native方法服务

程序计数器

内存空间较小,可以看做是当前线程所执行的字节码的行号指示器。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空(Undefined)

内存区域最大的一块,此内存区域的唯一目的就是存放对象实例,基本上所有的对象实例分配都是由其分配内存。Java堆是垃圾收集器管理的区主要区域,因此有时也成为GC堆

方法区

也称为非堆,主要用来存取已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

对象内存布局

对象头(Header)

用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

类型指针

实例数据(Instance Data)

对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容

对齐补充(Padding)

仅仅起到占位符的作用

对象访问定位

句柄访问

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

直接指针访问

Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址

虚拟机栈和本地方法栈异常

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常

如果虚拟机在拓展时无法申请到足够的内存空间,则抛出OutOfMemoryError(OOM)异常

对象已死

引用计数法

对象每引用一次就加1,引用失效则减1,当引用次数为0的时候将进行回收,会出现循环依赖问题,因此虚拟机没有使用此算法

可达性分析

使用GC ROOTS来判断一个对象是否可达,不可达将其判断为不可达的对象

回收算法

标记-清除算法

将要回收的对象进行标记,回收的时候直接将已标记的对象进行回收,但是很容易产生内存碎片

标记-整理算法

将要回收的对象进行标记并移动到内存区域的一端,减少内存碎片的产生,但是这很影响效率

复制算法

新生代的对象大部分都是朝夕生死的,使用复制算法将不需要回收的对象移动到Survivor区,为Eden区腾出空间,因为对象优先在Eden分配,年轻代中默认为Eden:Survivor为8:1,其中Survivor有两个

分代收集算法

只是根据对象存活周期的不同将内存划分为几块,一般是将java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法

在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,那就选中复制算法,只需要少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记–清理或标记–整理算法

HotSpot算法实现

枚举根节点

安全点(Safepoint)

安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都是非常短暂,程序不太可能因为指令长度太长这个原因而过长时间运行,“长时间运行”的最明显的特征就是指令序列复用,如方法调用、循环跳转、异常跳转

产生安全点

方法调用

循环跳转

异常跳转

安全区域(Safe Region)

Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,需要安全区域来解决

在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的,可以把Safe Region看成是被拓展了的Safepoint

垃圾收集器

并行与并发的概念

并行(Parallel):指多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待状态

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾回收器程序运行于另一个CPU上

新生代

Serial(JDK1.3.1之前)

单线程收集器,进行垃圾回收时必须暂停其他所有的工作线程,知道它收集结束。优点是简单高效(与其他收集器的单线程相比),没有线程交互开销

ParNew(JDK1.3)

Serial的多线程版本,除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处

Parallel Scavenge(JDK1.4)

使用复制算法的收集器,使用并行的多线程收集器,为了达到一个可控制的吞吐量,即CPU用于执行用户代码的时间与CPU总消耗时间的比值(吞吐量=用户代码执行时间/(用户代码执行时间+垃圾收集时间)),也称为吞吐量优先收集器

老年代

Serial Old

Serial收集器的老年代版本,单线程收集器,使用标记——整理算法 。主要意义也是在于给Client模式下的虚拟机使用,GC时需要STW

Parallel Old(JDK1.6)

Parallel Scavenge收集器的老年代版本,使用多线程和标记——整理算法

CMS(JDK1.5,Concurrent Mark Sweep)

使用标记——清除算法,以获取最短回收停顿时间为目标的收集器。优点:并发收集,低停顿,Sun公司也称之为并发低停顿收集器(Concurrent Low Pause Collector)

运行步骤

初始标记(需要STW)

标记一下GC Roots能直接关联到的对象,速度很快

并发标记

进行GC Roots Tracing的过程

重新标记(需要STW)

为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一点,但远比并发标记的事件短

并发清除

缺点

对CPU资源非常敏感

无法处理浮动垃圾

由于采用了标记——清除算法,所以这很容易导致产生大量空间碎片

G1(JDK1.7,Garbage First)

运行步骤

初始标记

并发标记

最终标记

筛选回收

面向服务端应用

特点

并行与并发

充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW(Stop The World,GC进行时需停顿所有的Java执行进程)停顿的时间

分代收集

空间整合

可预测的停顿

内存分配与回收策略

新生代GC与老年代GC

新生代GC(Minor GC)

指发生在新生代的垃圾回收动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快

老年代(Major GC / Full GC)

指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行进行Major GC的策略选择过程)。Major GG的速度一般会比Minor GC慢10倍以上

对象优先在Eden分配

对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB(Thread Local Allocation Buffer 本地线程分配缓冲区)上分配

大对象直接放入老年代

大对象指的是需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串和数组,尽量避免出现朝生夕灭的大对象

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

虚拟机为每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当其年龄增加到一定程度(默认为15岁),就会晋升到老年代。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlerPromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;如果大于,将尝试进行一次Minor GC,尽管这个Minor GC是有风险的;如果小于,或者HandlerPromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC

JVM常用命令

jps

类似于Linux中的ps命令,列出正在执行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID),是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程

jps [options] [hostid]

options

-q

只输出LVMID,省略主类的名称

-m

输出虚拟机进程启动时传递给主类main()函数的参数

-l

输入主类的全名,如果进程执行的是Jar包,输出Jar包路径

-v

输出虚拟机进程启动时的JVM参数

hostid

RMI注册表中注册的主机名

jstat

虚拟机统计信息监视工具,可以显示本地或远程虚拟机中的类加载、内存、垃圾收集、JIT编译等运行数据

jstat [option vmid [interval[s | ms] [count]] ]

options 列举2个

-class

监视类装载、卸载数量、总空间以及类装载所耗费的时间

-gc

监视Java状况,包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息

interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次;eg:

jstat -gc 2764 250 20

需要每250ms查询一次进程2764垃圾收集情况,一次查询20次

jinfo

Java配置信息工具,实时查看和调整虚拟机各项参数

jinfo [options] pid

options

jinfo对于Windows平台功能仍然有较大限制,只提供了最基本的-flag选项

eg:查询CMSInitiatingOccupancyFraction参数值

jinfo -flag CMSInitiatingOccupancyFraction 1444

jmap

Java内存影像工具(Memory Map for Java),jmap命令用于生成堆转储快照(一般称为headdump或dump文件)。和jinfo命令一样,jmap有不少功能在Windows平台都是受限的

jmap [ option ] vmid

options列举4个

-dump

用于生成Java堆转储快照

-finalizerinfo

显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在Linux/Solaris平台才有效

-head

显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在Linux/Solaris平台下有效

-histo

显示堆中对象统计信息,包括类、实例数量、合计容量

jhat

虚拟机堆转储快照分析工具,Sun JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照,jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。

jstack

Java堆栈跟踪工具,jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因。如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待些什么资源

jstack [option] vmid

-F

当正常输出的请求不被响应时,强制输出线程堆栈

-l

除堆栈外,显示关于锁的附加信息

-m

如果调用到本地方法的话,可以显示C/C++的堆栈

类加载

加载

通过一个类的全限定名来获取定义此类的二进制字节流

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

(Class对象比较特殊,存放在方法区里)

二进制流获取路径

从ZIP包中获取,这很常见,最终成为JAR、EAR、WAR格式的基础

从网络中获取,如从Applet应用中获取

运行时计算生成,如动态代理技术(JDK动态代理或cglib),在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为”*$Proxy“的代理类的二进制流

由其他文件生成,如JSP应用,由JSP文件生成对应的Class类

从数据库中读取,比较少见,有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发

验证

文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处

是否以魔数0xCAFEBABE开头

主、次版本号是否在当前虚拟机处理范围之内

常量池的常量中是否有不被支持的常量类型(检查常量tag标志)

指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据

Class文件中各个部分及文件本身是否有被删除的或附加的其他信息

元数据验证

对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求

这个类是否有父类(除了java.lang.Object之外,所有的类应当有父类)

这个类的父类是否继承了不允许继承的类,如被final修饰的类

如果这个类是抽象类,是否实现了其父类或接口之中要求实现的所有方法

类中的字段、方法是否与父类产生矛盾(如覆盖了父类的final字段、或者出现不符合规则的方法重载、重写)

字节码验证

验证过程最为复杂,主要目的是通过数据流和控制流分析,以确定程序语义是合法的、符合逻辑的

保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中

保证跳转指令不会调转到方法体以外的字节码指令上

保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的

符号引用验证

符号引用中通过字符串描述的全限定名是否能找到对应的类

在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

符号引用中的类、字段、方法的访问性

(private、protected、public、default)是否可以被当前类访问

准备

为类变量(被static修饰的变量,不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配

初始值通常情况下是数据类型的零值

如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量value就会被初始化为ConstantValue属性所指定的值(被final修饰的类变量在编译时会生成ConstantValue属性)

eg:public static final int value = 123;

在准备阶段虚拟机就会根据ConstantValue的设置将value赋值给123

解析

解析阶段是虚拟机将常量池内的符号引用(在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量)替换成直接引用的过程

符号引用(Symbolic References)

符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中

直接引用(Direct References)

直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在

类或接口的解析

字段解析

类方法解析

接口方法解析

初始化

必须立即对类进行初始化的5种情况

遇到new(实例化一个对象)、getstatic(读取一个类的静态字段【被final修饰、已在编译期把结果放入常量池的静态字段除外】)、putstatic(设置一个类的静态字段【被final修饰、已在编译期把结果放入常量池的静态字段除外】)或invokestatic(调用一个类的静态方法)字节码指令时

使用java.lang.reflect包的方法对类进行反射调用的时候

当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化

包含main方法的类(执行的主类)

当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的的类没有进行过初始化,则需先触发其初始化

主动引用

上述五种情况均为主动引用

被动引用

所有引用类的方法都不会触发初始化

通过子类引用父类的静态字段,不会导致子类初始化

通过数组定义来引用类,不会触发此类的初始化

常量在译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

类构造器()

可由类中的static{}语句块产生

也可由接口中的定义的常量产生,接口中不能定义static{}语句块,需要注意的是,执行接口的()方法不需要先执行父接口的()方法,只有当父接口的常量使用时,父接口才会初始化

它不要显示调用地父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。可以得出java.lang.Object是第一个先执行()方法的类

虚拟机会保证一个类的()方法在多线程环境中被正确加锁、同步,如若多个线程同时去初始化一个类,那么只有一个线程执行这个类的()方法,其他线程阻塞等待,直至这个线程执行完()方法。其他线程唤醒之后不会再次进入()方法

()方法不是必须的,如果类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法

实例构造器()

使用

卸载

类加载器

类加载器分类

从JVM的角度上看

启动类加载器 Bootstrap ClassLoader

由C++实现,是虚拟机的一部分

所有其他的类加载器

由Java实现,独立于虚拟机外部,并且全部继承自抽象类ClassLoader

从Java开发人员角度上看

启动类加载器 Boostrap ClassLoader

拓展类加载器 Extension ClassLoader

应用程序类加载器 Application ClassLoader

自定义类加载器 User ClassLoader

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都是应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

下面附上我自己整理的路线图:

面试半年,凭借这份JVM面试题,我终于拿到了字节跳动的offer!

面试半年,凭借这份JVM面试题,我终于拿到了字节跳动的offer!

最后:

上面的路线图只是一部分,欢迎大家关注我的公众号:前程有光,路线图都放在我的公众号里面了,另外整理了1000多道将近500多页pdf文档的Java面试题资料关注后回复领取资料即可领取到,文章都会在里面更新,整理的资料也会放在里面。

原文  https://segmentfault.com/a/1190000023088920
正文到此结束
Loading...