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

内存区域

虚拟机栈
生命周期与线程相同,描述的是Java 方法执行的内存模型,每一个方法在执行的时候都会建立一个栈帧,用于存取局部变量表、操做数栈、动态连接、方法出口等信息
本地方法栈
与虚拟机栈做用类似,只不过本地方法栈是为虚拟机使用到的Native方法服务
程序计数器
内存空间较小,能够看作是当前线程所执行的字节码的行号指示器。此内存区域是惟一一个在Java虚拟机规范中没有规定任何OutOfMemoryError状况的区域
若是线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是Native方法,这个计数器值为空(Undefined)

内存区域最大的一块,此内存区域的惟一目的就是存放对象实例,基本上全部的对象实例分配都是由其分配内存。Java堆是垃圾收集器管理的区主要区域,所以有时也成为GC堆
方法区
也称为非堆,主要用来存取已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据java

对象内存布局

对象头(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
双亲委派模型
若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,所以全部的加载请求最终都是应该传送到顶层的启动类加载器中,只有当父类加载器反馈本身没法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试本身去加载

下面附上我本身整理的路线图:


最后:

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