JVM(上) — 类加载与调优
📋 本文节选自《JVM 面试题(上)》,返回 查看全部章节
28、说一下垃圾分代收集的过程
分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。 新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。 当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下:
在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代
Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%
Survivor 区内存不足会发生担保分配
超过指定大小的对象可以直接进入老年代
Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC
FullGC,整个堆的垃圾收集,触发条件: 1.每次晋升到老年代的对象平均大小>老年代剩余空间 2.MinorGC后存活的对象超过了老年代剩余空间 3.元空间不足 4.System.gc() 可能会引起 5.CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成 6.堆内存分配很大的对象
29、如何找到死锁的线程?
死锁的线程可以使用 jstack 指令 dump 出 JVM 的线程信息。 jstack -l <pid> > threads.txt 有时候需要dump出现异常,可以加上 -F 指令,强制导出 jstack -F -l <pid> > threads.txt
如果存在死锁,一般在文件最后会提示找到 deadlock 的数量与线程信息
30、invokedynamic 指令是干什么的?
Java 7 开始,新引入的字节码指令,可以实现一些动态类型语言的功能。Java 8 的 Lambda 表达式就是通过 invokedynamic 指令实现,使用方法句柄实现。
31、什么是方法内联?
为了减少方法调用的开销,可以把一些短小的方法,纳入到目标方法的调用范围之内,这样就少了一次方法调用,提升速度
32、什么是逃逸分析?
分析对象动态作用域
当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;
被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
从不逃逸
如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化,如栈上分配、标量替换、同步消除。
33、描述一下什么情况下,对象会从年轻代进入老年代
对象的年龄超过一定阀值,-XX:MaxTenuringThreshold 可以指定该阀值
动态对象年龄判定,有的垃圾回收算法,比如 G1,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法
大小超出某个阀值的对象将直接在老年代上分配,值默认为 0,意思是全部首选 Eden 区进行分配,-XX:PretenureSizeThreshold 可以指定该阀值,部分收集器不支持
分配担保,当 Survivor 空间不够的时候,则需要依赖其他内存(指老年代)进行分配担保,这个时候,对象也会直接在老年代上分配
34、safepoint 是什么?
为了减少对象引用的扫描,使用 OopMap 的数据结构在特定的位置记录下栈里和寄存器里哪些位置是引用; 但为了避免给每条指令都生成 OopMap 记录占用大量内存的问题,只在特定位置记录这些信息。 安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,如方法调用、循环跳转、异常跳转等都属于指令序列复用。
35、MinorGC、MajorGC、FullGC 什么时候发生?
MinorGC 在年轻代空间不足的时候发生
MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC
FullGC 老年代无法再分配内存;元空间不足;显示调用 System.gc;像 CMS 一类的垃圾回收器,在 MinorGC 出现 promotion failure 时也会发生 FullGC
36、说说类加载的过程
加载(Loading),通过一个类的全限定名来获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证(Verification),确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
准备(Preparation),正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值。
解析(Resolution),是 JVM 将常量池内的符号引用替换为直接引用的过程。
初始化(Initialization),执行类构造器 <clinit> 方法的过程,执行所有类变量的赋值动作和静态语句块(static{}块)。
其中验证、准备、解析统称为称为连接(Linking)
37、可以描述一下 class 文件的结构吗?
Class 文件包含了 Java 虚拟机的指令集、符号表、辅助信息的字节码(Byte Code),是实现跨操作系统和语言无关性的基石之一。
一个 Class 文件定义了一个类或接口的信息,是以 8 个字节为单位,没有分隔符,按顺序紧凑排在一起的二进制流。
用 "无符号数" 和 "表" 组成的伪结构来存储数据。
无符号数:基本数据类型,用来描述数字、索引引用、数量值、字符串值,如u1、u2 分别表示 1 个字节、2 个字节
表:无符号数和其他表组成,命名一般以 "_info" 结尾
组成部分 1、魔数 Magic Number
Class 文件头 4 个字节,0xCAFEBABE
作用是确定该文件是 Class 文件
2、版本号
4 个字节,前 2 个是次版本号 Minor Version,后 2 个主版本号 Major Version
从 45 (JDK1.0) 开始,如 0x00000032 转十进制就是 50,代表 JDK 6
低版本的虚拟机跑不了高版本的 Class 文件
3、常量池
常量容量计数值(constant_pool_count),u2,从 1 开始。如 0x0016 十进制 22 代表有 21 项常量
每项常量都是一个表,目前 17 种
特点:Class 文件中最大数据项目之一、第一个出现表数据结构
4、访问标志
2 个字节,表示类或接口的访问标志
5、类索引、父类索引、接口索引集合
类索引(this_class)、父类索引(super_class),u2
接口索引集合(interfaces),u2 集合
类索引确定类的全限定名、父类索引确定父类的全限定名、接口索引集合确定实现接口
索引值在常量池中查找对应的常量
6、字段表(field_info)集合
描述接口或类申明的变量
fields_count,u2,表示字段表数量;后面接着相应数量的字段表
9 种字段访问标志
7、方法表(method_info)集合
描述接口或类申明的方法
methods_count,u2,表示方法表数量;后面接着相应数量的方法表
12 种方法访问标志
方法表结构与字段表结构一致
8、属性表(attribute_info)集合
class 文件、字段表、方法表可携带属性集合,描述特有信息
预定义 29 项属性,可自定义写入不重名属性
38、说说 JVM 如何执行 class 中的字节码。
JVM 先加载包含字节码的 class 文件,存放在方法区,实际运行时,虚拟机会执行方法区内的代码。Java 虚拟机在内存中划分出栈和堆来存储运行时的数据。
运行过程中,每当调用进入 Java 方法,都会在 Java 方法栈中生成一个栈帧,用来支持虚拟机进行方法的调用与执行,包含了局部变量表、操作数栈、动态链接、方法返回地址等信息。
当退出当前执行的方法时,不管正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。
方法的调用,需要通过解析完成符号引用到直接引用;通过分派完成动态找到被调用的方法。
从硬件角度来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。翻译过程由两种形式:第一种是解释执行,即将遇到的字节一边码翻译成机器码一边执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。在 HotSpot 里两者都有,解释执行在启动时节约编译时间执行速度较快;随着时间的推移,编译器逐渐会返回作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。
39、生产环境 CPU 占用过高,你如何解决?
top + H 指令找出占用 CPU 最高的进程的 pid
top -H -p
在该进程中找到,哪些线程占用的 CPU 最高的线程,记录下 tid jstack -l
threads.txt,导出进程的线程栈信息到文本,导出出现异常的话,加上 -F 参数 将 tid 转换为十六进制,在 threads.txt 中搜索,查到对应的线程代码执行栈,在代码中查找占 CPU 比较高的原因。其中 tid 转十六进制,可以借助 Linux 的 printf "%x" tid 指令
我用上述方法查到过,jvm 多条线程疯狂 full gc 导致的CPU 100% 的问题和 JDK1.6 HashMap 并发 put 导致线程 CPU 100% 的问题
40、生产环境服务器变慢,如何诊断处理?
使用 top 指令,服务器中 CPU 和 内存的使用情况,-H 可以按 CPU 使用率降序,-M 内存使用率降序。排除其他进程占用过高的硬件资源,对 Java 服务造成影响。
如果发现 CPU 使用过高,可以使用 top 指令查出 JVM 中占用 CPU 过高的线程,通过 jstack 找到对应的线程代码调用,排查出问题代码。
如果发现内存使用率比较高,可以 dump 出 JVM 堆内存,然后借助 MAT 进行分析,查出大对象或者占用最多的对象来自哪里,为什么会长时间占用这么多;如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令 pmap 查出进程的内存分配情况、gdb dump 出具体内存信息、perf 查看本地函数调用等。
如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂停的时间、各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等,如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx,使用 visualmv 等可视化工具远程监控与分析。
为了帮助更多的粉丝朋友,找到个好工作,多拿几个offer。苏三最近建一些工作内推群,群里目前已经收集了很多内推岗位,大厂、中厂、小厂、外包都有。欢迎HR、开发、测试、运维和产品加入。

扫描下方微信,备注:网站+所在城市,即可拉你进工作内推群。
