jvm面试知识大全(别被面试官问住了)
jvm面试知识大全(别被面试官问住了)Spring和tomcat都有自己的ClassLoader,热部署原理也是一样。而打破双亲委派继承ClassLoader类,需要重写loadClass,findClass就可以了。重写的逻辑不应该有双亲委派的逻辑。双亲委托机制:避免类的重复加载,防止核心API库被随意篡改。违反了双亲委托:JDBC,JNDI,tomcat 通过线程上下文,让父类加载器请求子类加载器去加载双亲委派的逻辑就在loadClass这个方法里,那么自定义自己的classloader的关键就是重写这个findClass方法。
- 1 类加载过程
过程:加载->验证->准备->解析->初始化->卸载
类加载过程
- 加载:通过类的权限定名获取此类的二进制字节流;将字节流代表的静态存储结构转化为运行时数据结构;在运行中生成该类的Class对象,作为访问入口。
- 验证:文件格式验证、元数据验证、字节码验证、符号引用验证等,确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
- 准备:为类变分配内存并设置类变量的初始值。
- 解析:虚拟机将常量池符号引用替换为直接引用的过程。符号引用:一组符号描述所引用的目标,与虚拟机实现的布局无关,引用的目标不一定要已经加载到内存中。直接引用:直接指向目标的指针、相对偏移量或句柄。
- 初始化:执行构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
- 以下几种情况将会对类进行初始化遇到new、getstatic、putStatic、imokeStatic等字节码指令。即使用new、读取静态字段、调用静态字段。使用reflect包的方法对类的反射调用
加载器:启动类加载器,扩展类加载器,系统类加载器
类加载器
双亲委托机制:避免类的重复加载,防止核心API库被随意篡改。
违反了双亲委托:JDBC,JNDI,tomcat 通过线程上下文,让父类加载器请求子类加载器去加载
双亲委派的逻辑就在loadClass这个方法里,那么自定义自己的classloader的关键就是重写这个findClass方法。
而打破双亲委派继承ClassLoader类,需要重写loadClass,findClass就可以了。重写的逻辑不应该有双亲委派的逻辑。
Spring和tomcat都有自己的ClassLoader,热部署原理也是一样。
- 2 对象引用(强软弱虚)
强引用:即使内存不足,也不会被垃圾回收
软引用:内存充足不回收,内存不够就回收
弱引用:不管内存是否充足,都会回收
虚引用:不会决定对象的生命周期,必须和引用队列 (ReferenceQueue)联合使用
- 3 JMM Java内存模型
3.1)什么是Java内存模型
Java虚拟机规范所定义的一种模型,屏蔽掉底层不同计算机的区别。以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量
JMM定义了线程和主内存之间的抽象关系
- 线程之间的共享变量存储在主内存(Main Memory)中
- 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
- 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
- java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(jvm内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。
如果线程A与线程B之间要通信的话,必须要经历下面2个步骤:
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
3.2)内存模型背景:https://www.jianshu.com/p/a2fc62fcbbe8
1)硬件的效率问题 cpu<-->高速缓存<-->内存 嗅探机制
2)多级缓存,多核处理器会产生缓存不一致性
3)为了提升处理器效率,会对输入的代码乱序执行(会产生多核依赖)
4)总线风暴:由于volatile的mesi缓存一致性协议需要不断的从主内存嗅探和cas不断循环无效交互导致总线带宽达到峰值,所以部分volatile和cas使用synchronized
3.3)内存模型组成
主内存 工作内存
3.4)JVM内存操作的并发
1)工作内存数据一致性
2)优化程序性能对指令重排序
3.5)内存交互的基本操作
3.5.1)8种操作:lock unlock read load use assign store write
3.5.2)同步规则:
内存模型运行规则
1)三个特性:
原子性(8个指令控制)
可见性:通过volatile、synchronized、final来实现
有序性(as-if-serial):Java通过volatile、synchronized来保证。
2)内存屏障:
写屏障:StoreStore StoreLoad
读屏障:LoadLoad LoadStore
volatile
1)可见性
2)禁止指令重排序
每个volatile写操作之前插入StoreSore屏障,写操作之后插入StoreLoad屏障
每个volatile读操作之前插入LoadLoad屏障,读操作之后插入LoadStore屏障
final
被final修饰的字段 一旦初始化完成,final变量的值立刻回写到主内存。
3.6)happen before原则
3.6.1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
3.6.2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3.6.3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
3.6.4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
3.6.5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
3.6.6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
3.6.7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
3.6.8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
- 4 jvm GC
4.1)内存结构
4.1.1)程序计数器
标记虚拟机指令执行的地址
4.1.2)java虚拟机栈
每个线程都有一个私有的栈帧,生命周期和线程一样。存储信息包含,局部变量,操作数栈,动态链接,方法出口
4.1.3)本地方法栈
虚拟机执行native方法服务
4.1.4)堆
内存回收的地方
4.1.5)元数据
主要存储类的相关信息,常量,静态变量放在了堆中
在java语言中,什么对象可作为GCRoot的对象? a. java虚拟机栈中的引用的对象。 b.方法区中的类静态属性引用的对象。 (一般指被static修饰的对象,加载类的时候就加载到内存中。) c.方法区中的常量引用的对象。 d.本地方法栈中的JNI(native方法)引用的对象
元空间与永久代区别是其内存空间直接使用的是本地内存,而metaspace没有了字符串常量池,而在jdk7的时候已经被移动到了堆中, MetaSpace其他存储的东西,包括类文件,在JAVA虚拟机运行时的数据结构,以及class相关的内容,
4.2)GC 判断哪些对象需要回收,什么时候回收,怎么回收 哪些对象需要回收:
4.2.1)引用计数法(循环引用),
4.2.2)根搜索法(虚拟机栈中引用的对象,方法区类静态属性引用的对象,方法区常量引用的对象,本地方法栈中引用的对象)
4.4)GC回收算法:
4.4.1)标记清除:效率不高,会产生碎片
4.4.2)复制:解决标记清除的问题,但是空间利用率会低
4.4.3)标记整理:针对于老年代
4.4.4)分代回收:新生代(复制)和老年代(标记清除,标记整理)
4.5)垃圾回收器:
4.5.1)新生代
serial 复制 单线程,垃圾回收时stop the world
适用场景:桌面应用,单核服务器
ParNew serial多线程版本,垃圾回收时仍需要停顿
适用场景:多核服务器;与 CMS 收集器搭配使用
Parallel Scavenge 注重吞吐量,可以通过收集频率和停顿时间来调节
吞吐量=用户代码时间/(用户代码时间 GC时间)
适用场景:高吞吐量,高效利用CPU,
4.5.2)老年代
serial old 标记整理
适用场景:桌面应用,单核服务器,与 Parallel Scavenge 收集器搭配;作为 CMS 收集器的后备预案。
Parallel Old 标记整理
适用场景:与Parallel Scavenge搭配试用,注重吞吐量
CMS 标记清除(初始标记(STW),并发标记,重新标记(STW),并发清除),以最短停顿回收时间为目标
CMS缺点:
1)对cpu敏感,默认垃圾回收线程数是(CPU数 3)/4
2)浮动垃圾不好处理
3)容易产生空间碎片
CMS解决内存碎片
CMS收集器提供了-XX:UseCMSCompactAtFullCollection开关参数,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过 程。 有参数可以配置有多少次Full GC会堆内存碎片进行整理(-XX:CMSFullGCsBeforeCompaction)
G1 建立可预测的停顿时间,避免在整个堆上垃圾回收(初始标记(STW),并发标记,最终标记(STW),筛选回收(STW))
G1的目标是在停顿和延迟可控的情况下尽可能提高吞吐量。
回收标准:不再是它属于哪个代,而是哪块区域回收的多,回收的收益最大
空间分成多个大小相等的独立区域,每个区域可能是E或S或O,回收的时候只是部分回收,而不是全堆扫描
并发标记的时候,如果老年代引用新生代的对象,则标记为脏卡,Minor GC的时候通过卡表来防止全堆扫描
4.5.3)new->old
对象分配在Eden,每次Eden满了,minorGC 将存活的对象放在S0区,如果S0满了,将存活的对象复制到S1,Eden S0清空,
以后Eden区执行Minor GC后,就将剩余的对象添加S1 当两个S区切换一定次数(默认15,-XX:MaxTenuringThreshold)后,
仍然存活的对象复制到老年代 老年代满了触发majorGC(full gc)
大对象直接分配到老年代
如果full gc完后,仍然内存不够,则会有OOM:heap space
system.gc()也可以触发full gc
serial<----> serial old
parNew<----> CMS
parallel scavenge<---->serial old parallel old
G1
分代年龄为啥是15:markword部分存分代年龄的二进制长度是4,最大是15
4.6)异常排查
4.6.1)cpu 100%排查步骤
1、top -c 找到cpu高的pid
2、top -Hp pid 找到对应的线程pid2
3、prinf "%x\n" pid2 转换成16进制为 aaa
4、jstack aaa >aaa.log
4.6.2)OOM排查思路
1、找到对应的dump文件
2、然后使用MAT进行打开dump文件,查看堆栈信息
4.7)OOM
4.7.1)内存溢出
4.7.1.1)heapspace:java堆内存不够,可以调节jvm初始内存,最大内存
4.7.1.2)PermGenspace(java 1.6):永久代内存不够,可以调节-XX:MaxPermSize=128m,-XXermSize=128m
4.7.1.3)metaSpace:调整 MetaSpaceSize,MetaSpaceSize ,元数据不依赖jvm内存,而是直接使用本地内存
元空间的出现就是为了解决突出的类和类加载器元数据过多导致的OOM问题
4.7.2)内存泄露
4.8)JVM调优监控工具
4.8.1)jstack:观察当前线程的运行情况及运行状态
4.8.2)jstat:对java应用程序的资源和性能进行实时的命令行的监控,包含classloader compiler gc情况
4.8.3)jmap:监视jvm的物理内存
jmap -dump:format=b file=dump.bin pid:可以从core文件或进程中获得内存的具体匹配情况
4.8.4)jinfo:可以修改运行时的java进程的opts
jinfo -opt pid 如:查看2788的MaxPerm大小可以用
jinfo -flag MaxPermSize 2788。
4.9)OOM
4.9.1)heap space
4.9.1.1)产生原因
死锁 需要排查代码
程序占用太多内存,超过jvm的参数配置,调整jvm配置,-Xms -Xmx
4.9.2)meta space
调整-XX:metaspaceSize
4.9.3)Direct Buffer Memory
通常在NIO下,使用bytebuffer写入缓存写出缓存
调整大小-XX:MaxDirectMemorySize
4.9.4)unable to create new native thread
系统内存耗尽,无法为新线程
解决方案:
1、通过jstack确定应用创建了多少线程分配内存,超量创建的线程的堆栈信息是怎样的?谁创建了这些线程?一旦明确了这些问题,便很容易解决
2、调整操作系统线程数阈值
3、增加机器内存
4、减小堆内存
5、减小线程栈大小
6、减小进程数量
4.9.5)GC overhead limit exceeded
内存多次回收,但是可用内存还是很少
解决方案:
1、增加堆内存
2、MAT工具分析内存heap快照,找出内存泄露原因
4.9.6)StackOverflowError
线程请求的栈深度大于虚拟机所允许的请求深度
4.10)JVM 调优思路
4.10.1)新对象留在年轻代,因为minor gc成本远小于full gc成本
如果程序有更多的临时对象,则需要选择更大的年轻代,如果程序有更多才持久对象,则需要选择更大的老年代内存
-XX:newSize -XX:MaxNewSize设置为同样大小并且 -XX:NewRatio=2,年轻代与老年代比例是1:2
4.10.2)大对象进入老年代
-XX:PetenureSizeThreshold=1000000
4.10.3)禁止堆的压缩和扩展
-Xmx=-Xms
4.10.4)降低程序GC的停顿
使用CMS
4.10.5)增大吞吐量,选择并行垃圾回收器
-XX: UseParallelGC:选择垃圾收集器为并行收集器
4.10.6)开启GC日志
为了打印日志方便排查问题最好开启GC日志,开启GC日志对性能影响微乎其微
-XX: PrintGCTimeStamps -XX: PrintGCDetails -Xloggc:gc.log
4.10.7)OOM的时候生成内存快照
-XX: HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,方便排查问题
4.10.8)禁止system.gc(),设置-XX: DisableExplicitGC
4.11)内存分配担保
新生代内存不足的时候,把新生代存活的对象搬到老年代,然后新生代分配的空间用于分配给新的对象。这里老年代是担保人,在不同的GC下,担保策略也不一样。
在Serial Serial Old的情况下,发现放不下就直接启动担保机制;
在Parallel Scavenge Serial Old的情况下,却是先要去判断一下要分配的内存是不是>=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制。