高效面试-JVM篇
2022-09-06 23:46:51 0 举报
专注于Java面试知识、技能以及技巧相关的培训与辅导,帮Java程序员们在面试过程中能掌握核心知识技能。
作者其他创作
大纲/内容
弄清楚自己的情况和目标岗位的情况
总结自己(技能、工作经验、工作经历、特点、优势、劣势(因为不规范导致的问题,团队节奏不一致导致的,说完劣势后要说出解决方案和解决步骤,证明是个做事的人))
S:项目发生时的背景是什么?
T:项目的任务是什么,要达到什么样的目标?
A:面对这个任务采取了什么样的解决方案?
R:(量化)描述最终取得了什么样的结果?
STAR法则
准备话术(自我介绍、知识点、面试话术、业务场景题、工作的业绩指标(接口QPS/用户量级/服务器规模/系统架构图)
准备工作
才可以开始投简历
每天尽量不要约超过两家面试,要留出来时间做复盘,做总结,总结面试结果
面试要记得录音!!!
面试要把被问到的所有问题写下来,然后去逐个攻克(目的是,如果再给你一次去他家面试的机会时,你能够面试成功)
开始约面试
...反复面试...
简历优化1.0
数据-看结果导向性——服务器规模、用户量级、接口QPS
产出物-看落地实干性——系统架构图、功能设计文档、接口业务流程图
工具/技能-常用的开发工具和常用的语言/中间件/调优手段等
荣誉-明星员工/Owner意识/核心的项目上线/解决了某些关键的疑难杂症
软实力-沟通能力,解决问题能力,协调能力,统筹上线的能力,复盘能力,风险评估/规避的能力,planA/planB/.../planN
有哪些特质更容易打动招聘者?
简历优化2.0
找这个人做什么?招聘信息/招聘要求/能力范围/薪资范围
1.快速的通览一遍,看主要经历、能力是否匹配
2.重点去看“经历的变化过程”,看能力成长,连续性的工作和业务复杂度,依此去评估这个候选者的学习能力和成长速度
3.看“简历里对重点经历的描述内容”。复杂度/规范/有没有深度思考/能不能形成沉淀的知识内容/有没有复盘/总结
怎么看这份简历?
换位思考,以面试官角度去想问题
简历优化3.0
就是“落地风险”
1、“聪明”,看的就是一个人能力的可迁移性和迁移速度,把知识/工作方法/解决方案形成文档沉淀。
2、“皮实”和“乐观”,看的是当工作中我们面对的人、事的复杂度增加后,应该如何应对。
3、“自省”,看的是对过往做的事情是否有反思,是否有更新和迭代自己的工作方法(复盘 -> 提升)。
面试官最担心的是什么?
简历优化4.0
需要的是连续的内存空间,可能堆上有200M空间,但是不连续。
背景:数组是一段连续的内存空间,存储的是相同类型的数据。
注意到了一个细节:JVM在OOM前会有一个GC(垃圾回收)的操作。
OOM是什么?OutOfMemoryError。内存溢出,产生的原因:可能是内存泄漏了,可能是堆的大小分配不合理,可能是因为Java引用处理不及时,内存无法释放等等。但是:JVM在OOM前会有一个GC(垃圾回收)的操作,如果垃圾回收后仍空间不足,则才会抛出OOM异常。
堆:所有的Java对象实例都存储在堆上。GC垃圾回收的主要空间就是堆。
栈:对于每个线程的操作栈,有入栈和出栈,生命周期是当前线程执行结束,作用域是当前线程。
本地方法栈
运行时常量池
程序计数器
方法区
其他内存如Code Cache:JIT即时编译器优化后的代码存储在这里/GC执行的时候
直接内存:dirct buffer所需要的空间,就是由直接内存分配的。
-XX:SurvivorRatio=8(默认)年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:8,表示Eden:Survivor=8:1,一个Survivor区占整个年轻代的1/10-XX:NewRatio=3设置年轻代(EC+S0C+S1C)和年老代(OC)的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4(jdk1.8,默认2)
JVM的内存结构有哪些?
数组的分配是需要连续的内存空间,对于OOM异常前,JVM会进行垃圾回收操作,主要是回收堆内存空间,回收后堆内存空间大于100M,但还是抛出了OOM异常,可以证明,当前堆内存里没有连续的100M内存空间。
CMS:对于使用年轻代和老年代的内存管理时,堆大于100M,表示的是新生代和老年代加起来总和大于100M,而新生代和老年代各自并没有大于100M的连续内存空间。(大数组创建时,会在老年代创建,可以得出老年代没有连续的100M内存空间,但是年轻代有没有不确定)
G1:对于这种按region来管理的内存垃圾收集器,可能的情况是没有多个连续的region,它们的内存总和大于100M。
但是不同的垃圾收集器对于这种情况还会有不一样的区别:
结论:
内容:
话术:
面试题:我在试图分配一个 100M bytes 大数组的时候发生了 OOM,但是 GC 日志显示,明明堆上还有远不止 100M 的空间,你觉得可能问题的原因是什么?
1.对象头:包括两个部分,第一部分存储对象自身的运行时数据,如:HashCode\\GC分代年龄\\锁状态标志位\\线程持有的锁\\偏向锁ID等。也叫Mark Word。另一部分是类型指针,也就是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2.实例数据:存储真正的有效信息。
3.对齐填充:对象大小必须是8字节的整数倍。它仅仅起到占位符的作用。
常用的JVM命令:jconsole\\jmap
内容
Java对象主要有三部分组成,对象头、实例数据和对齐填充。对象头就是Mark Word里面包含了哈希值、锁标志位、GC分代年龄、线程持有的锁、偏向锁ID等,对象头中还会有一个指向它的类元数据的指针,用来确定对象的实例,这里有个区别就是如果对象是数组类型,那么在对象头中还必须持有一块用于记录数组长度的数据,虚拟机用3个字宽存储;如果对象是非数组,则用2个字宽存储。实例数据存储真正的有效信息,就是所有的成员变量。对齐填充是指对象的大小必须是8字节的整数倍,仅起到占位符的作用。平时常用的工具就是jconsole、jmap、jol等。
面试题:你知道Java对象的内存结构是什么样的吗?如何计算一个Java对象的大小?
synchronzied:是实现同步的基础,Java中每一个对象都可以作为锁。
对象头Mark Word里,区别:如果对象是数组类型,则虚拟机用3个字宽存储对象头;如果对象是非数组类型,则用2个字宽存储对象头。
Java对象头的长度
锁存放的位置和信息都有什么?
无锁
偏向锁:CAS实现(无锁状态,实际上未上锁)
轻量级锁:自旋
重量级锁:停止自旋,阻塞
死锁:两个线程互相持有对方需要的资源,同时又想要获得对方持有的资源。于是就发生了互相进行等待的情况,最终发生死锁。
锁升级的一个场景案例:获取偏向锁:小明去上厕所,小明问了一句里面,有人嘛?没人回应(CAS),于是小明进去上了厕所,并在厕所门上标注:“小明”;拥有偏向锁: 小明过了一会又来了,发现厕所门上的纸还在,还是“小明”(CAS),于是小明这次没有问,直接去上厕所了;升级轻量级锁: 小明在上厕所的时候小华来了,小华看到门上有“小明”的字样,知道小明在里面上厕所,于是催促小明说你快点,我在外面等你呢,每隔一段时间小华就要问一次(自旋锁)。等到小明上完厕所出来,小华进去上了厕所,临上厕所之前还不忘记把厕所上的字改成了“小华”升级重量级锁:小华在里面上厕所的时候,这时候来了很多的人,这些人就在门口每隔一段时间问小华什么时候好,然而经过了很多次以后小华说你们等吧,我出来了叫你们。众人说那就算了,在厕所外面安静的等吧(停止自旋,进入阻塞状态)。当小华上完厕所出来以后告诉大家,我上完了,然后众人靠自己的手速抢位置,并不是第一个人就能抢到,完全是随机性的(非公平锁)(公平锁的实现就是加一个队列,具有先进先出的特性)。
非公平锁会有什么问题?会存在线程一直抢不到锁的情况,(请求超时,请求失败,线程一直占有导致cpu标高...)
为什么锁要升级?
锁的优缺点对比
锁的几种状态和锁升级过程(由上到下)
synchronized是实现同步的基础,Java中每个对象都可以做为锁。锁的维度可以是方法、类和代码块。锁的信息存放在了对象头mark word里面。锁的状态是从偏向锁到轻量级锁再到重量级锁的一个升级状态,偏向锁就是一个CAS实现,优点是加锁和解锁不需要额外的性能消耗,缺点是如果线程之间存在锁竞争会有额外的锁撤销的性能损耗,适用于尽量不发生锁竞争的场景;轻量级锁是自旋实现,优点是竞争的线程不会阻塞,提高了程序的响应速度,缺点是始终得不到锁的线程会自旋消耗CPU,适用于追求响应速度、同步块执行非常快的场景;重量级锁是阻塞,好处是阻塞不会消耗CPU,缺点是阻塞响应时间慢,适用于追求吞吐量、同步块执行比较慢的场景。还有一个比较有趣的锁升级的例子...
面试题:说一下synchronized的锁升级过程吧?
Java代码的执行过程
JVM对代码的优化可以分为 运行时优化 和JIT即时编译器优化
逃逸分析:分析对象的作用域,当对象在一个方法中被定义后,它可能会被外部方法引用,这时就叫方法逃逸。是JVM性能优化的基础依据。
锁优化:Synchronized锁升级。
同步锁消除(判断一个加了锁的对象/方法没有发生锁竞争时,会将锁进行消除提升性能)
方法内联
解释器/编译器的运行时优化:主要是解释执行和动态编译通用的一些机制。
依据是:server模式的热点阈值是10000次,client模式的阈值是1500次,阈值也可以进行调整,-XX:CompileThreshold=N。
怎么判断方法是热点代码?
实现:方法计数器和回边计数器。(调用次数不是一直累加,而是随着时间会逐渐衰减)也可以关闭计数器衰减。 -XX:-UseCounterDecay (慎用)
怎么统计调用方法的次数?
不一定。因为JIT即使编译器的发展,有可能会发生对象没有逃逸出去,就可能会进行栈上内存分配。(但是:不同的JDK实现有可能会不同)
是不是所有的对象都会存储在堆上?
栈上内存分配(OSR)
...
即时编译器优化:指的是将热点代码以方法为单位转换成机器码,直接运行在底层硬件上。(性能更高)
Java代码的生命周期是先通过javac编译为字节码,然后由解释器去解释执行和jit即时编译器转换为机器码去执行。jvm有server模式和client模式,区别就是对热点代码的阈值不一样,server模式是10000,client是1500次。也可以通过-XX:CompileThreshold=N设置。jit即时编译器承担了更多的优化工作,它是把热点代码以方法为单位转换成机器码,直接运行到底层硬件上,所以性能比在jvm解释执行更高一些。jvm做的优化有逃逸分析,就是分析对象的作用域有没有被外部方法引用,是jvm做优化的一个基础。同步锁消除,就是当方法上有锁的但是没有发生锁竞争,jvm会替我们把锁消除来提高性能。synchronized锁升级。方法内联就是代码上的优化,减少压栈出栈的次数栈上内存分配就是逃逸分析发现对象没有被其他方法引用,也就是没有逃逸出去的时候会进行在栈上分配内存,但是由于jdk的实现不同,也不一定会发生栈上内存分配,有的jdk实现是所有的对象必须在堆上。
面试题:JVM在优化代码时它都做了些什么?
用最小的内存获得最大的性能
及时释放掉垃圾对象(因为垃圾越多,回收时间越长)
目标:
不能,因为成本问题;因为一旦内存满了触发GC后需要的时间非常长;会有资源浪费的情况;对象的寻址效率问题。
能不能把内存设置成无限大?
gc的时间要足够小(堆内存空间设置的要足够小)
gc的次数要足够少(堆内存空间设置的要足够大)
元数据空间Metaspace设置要合理,(如果元空间资源不足了,就会发生OOM(之前就会触发Fullgc))
老年代空间要设置的合理(尽量防止fullgc和OOM)
尽量让垃圾对象在年轻代的时候就被回收
尽量防止大对象的产生。
尽量减少fullgc(最好不要发生)
JVM调优的原则
年轻代属于并行的垃圾回收器;
老年代属于并发的垃圾回收器;
优点:响应时间优先。(并发收集、低停顿)
缺点:存在内存碎片(缺点的解决方案是开启压缩:-XX:CMSFullGCsBeforeCompaction=10)
特点:
parNew(年轻代)+CMS(老年代)【标记-清除算法】
将整个Java堆给划分成了2048个大小相同的region块,region大小2的n次幂。
【标记-清除算法】
优点:堆的结构上来看不会产生垃圾碎片;并发收集、低停顿(相同配置下和cms相比,cms在最好的情况下更好,但是G1在最差的情况下也会比cms最差的情况要好很多。)可以预测停顿时间,能获得尽可能高的收集效率。
缺点:G1在运行时垃圾收集产生的内存和负载都比CMS要高。
G1
常见的垃圾回收器
从经验来说,小内存应用堆大小(4-8g)时,选择CMS。大内存应用(16G)时,选择G1。
实际工作要怎么去做:测试环境的应用堆大小设置都不大,应该使用CMS。(突出的是你的服务部署,资源规划,性能优化,技术选型,资源成本,责任心,形成文档、规范、全部门推)
将最小堆和最大堆设置成一致的,可以减少内存抖动带来的性能损耗。
实际使用经验
垃圾回收
做JVM优化的目标其实就是要用最小的内存获得最大的性能,所以我们在选择服务器配置就要以这个为依据,我们公司里有一个规范的标准就是4C8G的机器使用CMS垃圾回收,8C16G的机器使用G1垃圾回收,8G内存的机器使用cms的优点是并发收集,低停顿,但是cms也有个问题就是会产生内存碎片,所以我们会开启压缩命令,-XX:CMSFullGCsBeforeCompaction=10,意思就是发生10次fullgc后进行压缩,清理内存碎片。16G内存机器使用G1的优点是划分了Region块,不会产生垃圾碎片,也是并发收集,因为它可以做到预测停顿时间,和cms相比能获得尽可能高的收集效率,G1在运行时产生垃圾收集产生的内存和负载都比较高,所以内存比较大的情况下更加实用。还有我们在使用场景做的优化就是会将最小堆和最大堆设置成一致的,可以减少内存抖动带来的性能损耗。
面试题:你都做了哪些JVM的优化?
ReentrantLock是一个可重入锁。指的就是线程可以重复获取同一把锁。
是什么?
公平:指的是在绝对时间上,先对锁获取的请求一定先被满足,也就是说等待时间最长的那个线程优先获得锁。先进先出FIFO。
支持公平锁/非公平锁。
互斥锁,能够保证原子性。
Java实现的,获取锁和释放锁都是由编码人员手动实现所以更灵活;也可以控制获取锁的等待超时时间,可以解决死锁的问题。
优点
因为获取锁和释放锁都是由编码人员手动实现,所以对编码人员的技能要求也更高了,出错的概率也更大了,风险变大了。
缺点
ReentrantLock
Synchronized可重入,因为获取锁和释放锁是自动执行的,所以不必担心最后锁是否释放。
ReentrantLock可重入,但是获取锁和释放锁需要手动执行,而且次数必须一致,否则就可能会出现锁没有释放的情况,导致其他线程无法获取到锁。
重入
Synchronized是JVM实现的,操作系统级别的
ReentrantLock是JDK的并发包下的,Java级别的
实现
jdk1.5以前,synchronized性能不如ReentrantLock
jdk1.6之后,synchronized做了很多优化(锁升级),性能就追了上来
性能
在锁的粒度和灵活度上ReentrantLock要优于Synchronized;
ReentrantLock是支持公平锁和非公平锁的,而Synchronized只能是非公平锁;
ReentrantLock可以避免死锁的问题,因为他可以非阻塞地获取锁,还可以设置获取锁的等待时间。
ReentrantLock提供能够终端等待锁的机制:lockInterruptibly方法。能够使阻塞状态的线程响应中断信号,就有机会让阻塞线程释放掉曾经持有的锁,解决了死锁的问题。
功能
对比优缺点
两个锁都是可重入的,不过Synchronized的获取锁和释放锁都是自动执行的,所以不用担心锁没有释放的情况,而ReentrantLock的获取锁和释放锁都需要手动执行,并且次数必须一致,否则就可能会出现锁没有释放的情况,这也更考验编码人员的能力。Synchronized是JVM在操作系统级别实现的,ReentrantLock是JDK并发包下的,Java级别。性能上jdk1.6后给synchronized做了很多优化比如锁升级,性能就提高了很多,在锁的粒度和灵活度上ReentrantLock要比Synchronized要强很多,因为Synchronized的加锁只能是在类、方法、和代码块中。ReentrantLock还可以支持公平锁和非公平锁,synchronized只支持非公平锁。ReentrantLock提供了lockInterruptibly方法,能够使阻塞状态的线程响应中断信号,释放阻塞线程曾经持有的锁,可以用来解决死锁的问题。这也是它给我们提供的强大功能。
面试题:对比一下ReentrantLock和synchronized的优缺点?
是将字节码数据从不同的数据源读取到JVM中,并且映射为JVM认可数据结构。(class对象)
是用户参与的阶段,我们可以自定义类加载器(去实现自己的类加载过程)。
加载
核心的步骤,是把原始的类定义信息平滑的转化到JVM运行的过程中。
验证:是虚拟机安全的重要保证,JVM需要核验字节信息是否符合规范。
准备:创建类或接口中的静态变量,并且初始化静态变量的初始值。(这里的初始化和下面的显示初始化区别就在于这里仅分配了所需要的内存空间,不会去执行进一步的JVM指令)
解析:会将常量池中的符号引用替换为直接引用。
细化为三个小步骤:
链接
这一步才是真正执行类初始化的代码逻辑,包括静态字段的赋值,还有执行类定义中的静态初始化块内的逻辑,编译器会在变异阶段就把这部分的逻辑整理好,父类型的初始化逻辑要优于当前类型的逻辑。(双亲委派模型)
初始化
当类加载器试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。好处就是可以避免重复加载。
双亲委派模型
加载的是/jre/lib下面的jar文件,是一个超级公民。
启动类加载器(Bootstrap Class-Loader)
加载的是/jre/lib/ext/目录下的jar文件,就是所谓的extension机制。
扩展类加载器(Extension or Ext Class-Loader)
加载的就是classpath的内容。
应用类加载器(Application or App Class-Loader)
可见性。子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的。因为缺少了必要的隔离,我们就没办法利用类加载器去实现容器的逻辑。
单一性,由于父加载器的类型是子加载器可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是要注意类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相不可见。
类加载器的特征:
实现了类似进程内隔离,类加载器实际上用作不同的命名空间,提供类似容器、模块化的效果。
应用需要从不同的数据源获取类定义信息。
需要自己操作字节码,动态修改或者生成类型。
自定义类加载器的常见场景
扩展一下深度-常见的类加载器
主要分三个步骤:
Java类加载过程主要分为三个步骤,加载、链接和初始化。加载就是将字节码数据从不同的数据源读取到JVM中,并且映射为JVM认可的数据结构,链接阶段是类加载过程的一个核心步骤,是把原始的类定义信息平滑的转化到JVM运行过程中,链接里的通过验证核验字节信息是否符合规范,通过准备创建类或接口中的静态变量并且分配所需要的内存空间,通过解析将常量池中的符号引用替换为直接引用。初始化阶段才是真正执行类初始化的逻辑,包括静态字段的赋值,还有执行类定义中静态初始化块内的逻辑。这里就说到了我们常常说的双亲委派模型了,双亲委派模型的就是当类加载器加载某个类的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做,这样做的好处就是避免类重复加载。不过双亲委派也有一个需要注意的点就是,如果两个类是邻居级别的,那么同一类型仍然可以被加载多次,原因就是类加载器的可见性,它是子加载器可以见到父加载器的类型,但是父加载器不可以看见子加载器的类型。这里就还要再说一下常见的类加载器了,JDK中的本地方法类一般由启动类加载器装载,JDK 中内部实现的扩展类一般由扩展类加载器实现装载,而程序中的类文件则由应用类加载器实现装载。我们也可以使用自定义类加载器不交给父类来加载类,就破坏了双亲委派机制。常见的打破双亲委派机制的是tomcat,tomcat这么做的原因主要是tomcat想要部署多个应用,若多个应用中具有同名类(包结构也一致),但实现方式不同,只加载一份会导致异常,因此需要打破双亲委派机制。
面试题:说一下Java的类加载过程?
高效面试-JVM篇
0 条评论
回复 删除
下一页