jvm
2020-09-07 10:53:29 6 举报
AI智能生成
jvm面试总结
作者其他创作
大纲/内容
jvm
概念
jvm是什么
子主题
内存结构
程序计数器
线程私有
当前线程执行的字节码的行号指示器
控制分支、循环、跳转、异常处理、线程恢复
虚拟机栈
每个方法执行都会创建一个栈帧入栈,执行完毕出栈
栈帧
局部变量表
存储内容
存储基本类型(8种)
对象引用,指向堆内存中对象
returnAddress 类型,指向一个指令地址
64位long和double占用两个槽,其他类型占用一个槽
操作数栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种 字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作
动态链接
指向运行时常量池 [1] 中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
方法出口
如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址
2.方法执行的过程中遇到了异常
本地方法栈
调用本地native方法时使用的栈
堆
线程共享
存放对象实例
TLAB
堆中线程私有的内存
方法区
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
直接内存
可能导致OutOfMemoryError,但不受java堆大小控制
NIO它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。
对象内存布局
对象头布局
运行时数据
哈希码
GC分代年龄
锁状态标志
偏向线程ID
锁记录引用或者monitor引用
class类型指针
数组长度
实例数据
对齐填充
自动内存管理系统要求对象起始地址必须是8字节的整数倍
必须是8字节,是为了找对象内存结束位置得时候,可以跳着8字节找
对象头是8的整数倍,但是实例数据不一定,所有就用到对齐填充
考虑gc的角度
为什么要gc
业务运行过程中不断创建对象,需要占用内存
内存有限,无用的对象需要清理释放内存
内存不足是会发生outofmemory异常
java何时自动gc
gc会带来什么问题
gc可能需要STW,系统不可用
为什么要STW,因为业务过程中可能不断修改引用关系,而且GC需要移动内存对象
gc频繁占用线程影响业务线程
gc内存释放率低
gc调优要干什么
提高gc内存释放率
提高gc的回收速度
并发
尽量避免STW,或者降低时间,降低系统延时
降低gc发生的频率
故障处理
尽一切条件保证系统正常运行
保存下堆栈信息,重启
回滚
内存溢出
dump内存文件
jmap
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath
使用mat分析
导致OMM的对象是否是必须的
否:内存泄露,分析GCROOT链
是:调整内存比例或调大内存
调优
目的
提高吞吐量
降低RT,降低停顿
提高内存利用率
基准
full gc一般 1s
full gc执行频率大约 10分钟一次
minor gc一般 50ms
minor gc执行频率大约10s执行一次
优化思路
如果 YGC 超过5秒一次,甚至更长,说明系统内存过大,应该缩小容量;如果频率很高,说明 Eden 区过小,可以将 Eden 区增大,但整个新生代的容量应该在堆的 30% - 40%之间,eden,from 和 to 的比例应该在 8:1:1左右,这个比例可根据对象晋升的大小进行调整
FGC 的原因有几个,1 Old 区内存不够,2 元数据区内存不够,3 System.gc(), 4 jmap 或者 jcmd,5 CMS Promotion failed 或者 concurrent mode failure,6 JVM 基于悲观策略认为这次 YGC 后 Old 区无法容纳晋升的对象,因此取消 YGC,提前 FGC。
通常优化的点是 Old 区内存不够导致 FGC。如果 FGC 后还有大量对象,说明 Old 区过小,应该扩大 Old 区,如果 FGC 后效果很好,说明 Old 区存在了大量短命的对象,优化的点应该是让这些对象在新生代就被 YGC 掉,通常的做法是增大新生代,如果有大而短命的对象,通过参数设置对象的大小,不要让这些对象进入 Old 区,还需要检查晋升年龄是否过小。如果 YGC 后,有大量对象因为无法进入 Survivor 区从而提前晋升,这时应该增大 Survivor 区,但不宜太大。
内存控制
大内存空间
减少GC的次数
一旦GC,GC时间长
小内存
GC会频繁触发
每次GC会很快
内存调优
堆调整-Xmx -Xms -Xmn
调整年轻代和老年代比例
尽可能让对象呆在新生代,然后被回收
但同时避免存活对象长时间在新生代进行对象拷贝
高并发型
高并发型大量对象朝生夕死 将新生代尽量的大
任务型
任务型大量对象在老年代,需要将老年代尽量大减少fullgc
栈调整 -xss
metaspace区调整 -XX:MaxPermSize
根据实际情况调整metaspace区的大小
栈调整 -Xss
根据情况选择合适大小
参数调优
大对象直接进入老年代
-XX:PretenureSizeThreshold
设置为 1 M
长期存活的对象将进入老年代
-XX: MaxTenuringThreshold 默认 15
CMS调整 UseCMSInitiatingOccupancyOnly
动态检查机制
JVM会根据最近的回收历史,估算下一次老年代被耗尽的时间,快到这个时间的时候就启动一个并发周期,可以设置false关闭
CMS调整-XX:CMSInitiatingOccupancyFraction
如果内存使用率增长很快,降低该阈值,避免并发失败
如果内存使用增长缓慢,调大该值,降低Full GC的频率
CMS调整 -XX:CMSFullGCsBeforeCompaction=n
在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩
该值越大,压缩频率越低,碎片率越高,gc越快该值越小,压缩频率越高,碎片率越低,gc越慢
空间分配担保-XX:HandlePromotionFailure
调优工具
arthas
stack
输出调用路径
trace
跟踪每行代码的执行时间
watch
查看方法的出入参、异常情况
jstat -gcutil观察gc情况
调整survive和edon比例
调优案例
CPU过高
1.通过jps查看进程号
2. 通过 top -Hp pid 找到占用cpu高的线程号
3. 通过 printf \"%x\\" Tid 得到16进制
4. 通过jstack 过滤16进制线程栈信息
jstack 10765 | grep '0x2a34' -C5 --color
排查是否有死锁存在
jstack 查询是否有deadlock
接口耗时不定期的会很长
通过接口压测工具
大多数线程都会阻塞于某个地方,分析日志
分析线程堆栈信息
jvm内存溢出
可能原因
内存分配确实小了
内存泄露,某个对象频繁申请,却没释放
资源被频繁申请,系统资源耗尽,如不断创建线程,不断发起网络连接
dump
jstat监控内存情况
jmap查看堆信息
jmap -head pid
查看堆内存分配情况和使用情况
jmap -histo:live 10765 | more
找到最耗内存的对象
达到比较大的情况进行jmap -dump -format:b -file...导出dump文件
使用mat分析dump文件内存泄漏对象信息
然后根据内存对象反查代码
先atach进程
然后查看dashboard
监控内存使用
对象创建过程
1.当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到 一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那 必须先执行相应的类加载过程,
2.在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来
指针碰撞
内存规则,未使用和已使用在两边
空闲列表
内存不规则,标记清理垃圾算法导致
在TLAB上分配保证线程安全
3.内存分配完成之后,为对象中的字段都初始化为零值(但不包括对象头)
4.对象头初始化,初始化对象头的类型引用、hashcode(惰性计算,调用object.hashCode才计算)、gc年龄、锁状态(00)
5.从虚拟机的视角来看,一个新的对象已经产生了。执行对象的构造函数,即Class文件中的<init>()方法
引用reference
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表 某块内存、某个对象的引用
作用
一是从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引
二是根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
分类
强引用
存在强引用,GC永不回收
软引用 SoftReference
内存足够时不会回收,内存不足时回收
弱引用 WeakReference
无论内存是否足够,每次GC都会回收
虚引用 了PhantomReference
一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
垃圾回收
问题
何时触发垃圾回收
对象创建时首先在新生代创建,当新生代的eden没有足够大的连续空间创建对象时,触发新生代GC
当对象gc年龄达到时晋升老年代,老年代内存不足导致old gc
当创建的对象过大超过配置的阈值,直接在老年代创建,老年代内存不足导致old gc
当新生代的survivor不够用,无法存放gc后的存活对象,年轻代GC前判断内存分配担保检查老年代是否够晋升的平均值,不够触发fullGC
CMS达到内存使用比例后触发old GC
Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC
主动调用System.gc()
每次调整内存参数时会触发full gc
如何认定垃圾(哪块内存需要回收)
引用计数法
可达性分析
GCROOT
栈中的引用
在方法区中类静态属性引用
方法区中常量引用
Native方法引用
怎样找到GCROOT
准确式垃圾收集,HotSpot使用一组称为OopMap的数据结构。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在栈和寄存器中哪些位置是引用。这样子,在GC扫描的时候,就可以直接知道哪些是可达对象了。
HotSpot只在特定的位置生成OopMap,这些位置称为安全点
记忆卡集(卡表)
一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,避免回收新生代的时候对老年代的全内存扫描
注意:所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的
finalize方法自我拯救
这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临 下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了
方法区中类被回收
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用
何时开始回收动作
安全点
安全点的选定基本上以“是否具有让程序长时间执行“的特征选定的
位置
循环的末尾
方法返回前
调用方法的 call 之后
抛出异常的位置
安全区域
一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何地方开始GC都是安全的。在线程进入安全区域时,它首先标志自己已经进入安全区域,在这段时间里,当JVM发起GC时,就不用管进入安全区域的线程了。在线程将要离开安全区域时,它检查系统是否完成了GC过程,如果完成了,它就继续前行。否则,它就必须等待直到收到可以离开安全区域的信号
如何实现STW
在HotSpot中采取了一种称为主动式中断的方式让线程进入安全点,在JVM中有一个内存页面,线程在工作的平时会时不时的检查这个页面的值,在执行GC之前,JVM中的VMthread会提前将这个内存页面的访问属性为不可读,这时,其他工作线程再去读这个页面,将触发内存访问异常,JVM提前安装好的异常捕获器这时就能接管各线程的执行流程,做一些GC前的准备后,接着block,将线程挂起。
垃圾回收算法
理论
1.绝大多数对象都是朝生夕灭的
2.熬过越多次垃圾收集过程的对象就越难以消亡
3.跨代引用相对于同代引用来说仅占极少数
存在互相引用关系的两个对象,是应该倾 向于同时生存或者同时消亡的
minorGC时不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构记忆集,这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。需要在对象改变引用关系时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
分治:基于这两种对象类型,将内存分为年轻代和老年代,分别存储这两种类型的对象,采用不同的垃圾回收机制,提高效率
粒度
部分收集(Partial GC)
新生代收集(Minor GC/Young GC)
老年代收集(Major GC/Old GC)
混合收集(Mixed GC)
收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为
整堆收集(Full GC)
收集整个Java堆和方法区的垃圾收集
算法
标记清除
优点
只需要标记后直接清除,简单,不涉及移动对象,可以并发
缺点
执行效率不稳定,垃圾多的时候需要大量标记和清除
内存利用率低,导致空间碎片,遇到大对象分配时触发垃圾回收
内存分配的时候使用 空闲列表法
标记复制
浪费一半的内存
移动对象效率低
不会产生内存碎片,分配对象的时候适用指针碰撞法
适用回收对象存活率低的内存
如果Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代
标记整理
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用
虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经 大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间CMS
适用回收对象存活率高的内存
垃圾回收器
年轻代
Serial收集器
单线程
标记复制算法
parNew
多线程并行收集
Parallel Scavenge收集器
目标则是达到一个可控制的吞吐量
-XX:MaxGCPauseMillis
-XX:GCTimeRatio
-XX:UseAdaptiveSizePolicy
自动调节新生代 survivor和edon比例、年轻代大小
老年代
Serial Old
作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用
Parallel Old
支持多线程并发收集
CMS
一种以获取最短回收停顿时间为目标的收集器
步骤
初始标记
仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
并发标记
从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程
三色球
重新标记
修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录
并发清除
CMS默认启动的回收线程数是(处理器核心数量 +3)/4
CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用
CMS运行期间预留的内存无法满足程序分配新对象的需要,就导致并发失败,STW,临时启用Serial Old收集器来重新进行老年代的垃圾收集
对CPU资源占用比较多。可能因为占用一部分CPU资源导致应用程序响应变慢。
(处理器核心数量 +3)/4
CMS无法处理浮动垃圾。在并发清除阶段,用户程序继续运行,可能产生新的内存垃圾
需要预留一部分内存,在垃圾回收时,给用户程序使用,否则发生并发失败,将STW启用Serial Old收集器
基于标记-清除算法,容易产生大量内存碎片,导致full GC
G1
标记整理算法
收集器能够对扮演不同角色的 Region采用不同的策略去处理
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象
每个Region中都维护了记忆集,用于记录跨Region引用关系,避免回收一个Region时扫描其他Region
G1以Region作为gc的最小单元,降低了粒度,所以G1能建立可预测的停顿时间模型
需要 停顿线程,但耗时很短
最终标记
筛选回收
可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。
参数
-XX:MaxGCPauseMillis=200
最重要的参数,指定停顿时间
衡量指标
内存占用,是否清理全部垃圾
吞吐量
延迟
GC参数
-XX:+UseConcMarkSweepGC
parNew+CMS
-XX:ParallelCMSThreads
设置CMS的线程数量
-XX:CMSInitiatingOccupancyFraction
设置老年代内存使用率触发垃圾回收的阈值,默认68%
该值越高,发生并发失败的概率越大该值越低,老年代内存利用率低,发生gc的概率越大
-XX:+UseCMSCompactAtFullCollection=true
设置CMS是否在垃圾回收后进行内存碎片整理,配合-XX:CMSFullGCsBeforeCompaction=n使用
-XX:CMSFullGCsBeforeCompaction=n
-XX:+UseG1GC
-XX:G1HeapRegionSize
指定region大小,1MB~32MB,且必须是2的幂
有大对象时,增大该值,使大对象不再是大对象
-XX:MaxGCPauseMillis
设置最大垃圾收集停顿时间,默认200ms
-XX:GCPauseIntervalMillis
设置停顿间隔时间
-XX:ParallelGCThreads
垃圾回收线程数
内存监控
jstat观察垃圾回收频率
类加载
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
类加载时机
遇到new、getstatic、putstatic、invokestatic字节码指令时
使用new关键字实例化对象的时候
读取一个类型的静态字段
设置一个类型的静态字段
调用一个类型的静态方法的时候
使用java.lang.reflect包的方法对类型进行反射调用的时候
main方法所在的类要初始化
父类还没有进行过初始化,则需要先触发其父类的初始化
接口初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的静态变量等时才会初始化父接口。
对于静态字段, 只有直接定义这个字段的类才会被初始化
通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
包含default方法的接口实现类发生了初始化,那该接口要在其之前被初始化
类加载过程
加载
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象
验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存(方法区)并设置类变量初始值的阶段
进行内存分配的 仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
解析
是Java虚拟机将常量池内的符号引用替换为直接引用的过程
比如方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置
初始化
初始化阶段就是执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的 语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的
<clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显 式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行 完毕
Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步
类加载机制(双亲委派)
启动类加载器
扩展类加载器
应用程序类加载器
一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
为什么要双亲委派:任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
安全性,避免用户自己编写的类动态替换JAVA的一些核心类,比如String
避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。
上层的类加载器无法访问底层类加载器加载的类
类卸载
有JVM自带的三种类加载器(根、扩展、系统)加载的类始终不会卸载。因为JVM始终引用这些类加载器,这些类加载器使用引用他们所加载的类,因此这些Class类对象始终是可到达的
由用户自定义类加载器加载的类,是可以被卸载的
字节码执行
栈帧结构
方法调用
直接引用和动态链接
重载方法是静态分派
覆盖方法是动态分派
JIT
https://zhuanlan.zhihu.com/p/81941373
方法调用次数阈值
循环次数
内存模型
问题背景
编译器优化
为了提升性能,不存在依赖关系指令可能会被重排序
处理器优化
指令重排序
性能优化,添加高速缓存,内存不一致导致的可见性
并发编程原子性
单个变量的读写是原子操作
如何将一个代码块变为原子的呢
解决什么问题
并发编程下原子性、有序性、内存可见性
一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松,尽可能的让其优化程序
如何解决
定义工作内存和主内存
JMM屏蔽了不同处理器内存模型的差异,为Java程序员呈现了一个一致的内存模型
JMM规定线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了共享变量的副本,各个线程线程只能操作自己的工作内存,不可以访问其它线程的工作内存。
内存交互细节
lock
read
load
use
assign
store
write
as-serail-if规则定义单线程有序性
定义happens-before规则方便编程人员理解有序性
内存原语
synchronized
volatile
内存屏障
保证不会发生重排序
强制将对缓存的修改操作立即写入主存
强制去主存中读取
final
0 条评论
下一页