Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列
Posted 来老铁干了这碗代码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列相关的知识,希望对你有一定的参考价值。
东北某不知名双非本,四面成功上岸阿里巴巴,在这里把自己整理的面经分享出来,欢迎大家阅读。
序号 | 文章名 | 超链接 |
---|---|---|
1 | 操作系统面经大全——双非上岸阿里巴巴系列 | 2021最新版面经——>传送门1 |
2 | 计算机网络面经大全——双非上岸阿里巴巴系列 | 2021最新版面经——>传送门2 |
3 | Java并发编程面经大全——双非上岸阿里巴巴系列 | 2021最新版面经——>传送门3 |
4 | Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列 | 2021最新版面经——>传送门4 |
5 | 面试阿里,你必须知道的背景知识——双非上岸阿里巴巴系列 | 2021最新版面经——>传送门5 |
本博客内容持续维护,如有改进之处,还望各位大佬指出,感激不尽!
第一章 引言
1. Java层级?
2. JVM内存结构?
常见JVM:HotSpot
方法区:类、类加载器、常量池等
运行时常量池:存放在堆中,运行时,每一个类型对应一个常量池,如字符串常量池(字符串)、基本数据类型常量池(存放final、全局变量等)
静态区:存放在堆中,存放静态变量和静态代码块。
以上都是JDK7后实现的。
而除上述之外的局部变量,都会存放在虚拟机栈中
第二章 内存结构
1. 深拷贝和浅拷贝?
浅拷贝(shallowCopy):增加了一个指针指向已存在的内存地址。
深拷贝(deepCopy):增加了一个指针并申请了一个新的内存,使这个增加的指针指向新内存
使用深拷贝时,释放内存时不会因为出现浅拷贝时同时释放同一个内存的错误。
2. 堆栈区别?
1、堆物理地址分配不连续,性能慢,因此分配的内存是在运行期确认的,大小不固定,灵活。 存放实例和数组
2、栈物理地址分配连续,性能快,但其内存大小在编译器即确定。 存放局部变量,操作数。
静态变量放在方法区,静态对象放在堆
3. 内存泄漏及场景?
概念:指不再使用的对象由于仍被其他对象引用,导致GC不能及时释放其内存,造成的空间浪费的情况。
原因:长生命周期对象持有短生命周期对象的引用
场景
1、 使用静态集合类:静态类与应用程序生命周期一致,若它引用某对象,则该对象就不能被及时回收。若必须使用,则在用完后需及时赋null
解决办法:使用弱引用。
2、 单例模式:单例模式只允许应用程序存在一个实例对象,且该对象生命周期和应用程序的生命周期一样长,因此同上。
解决办法:弱引用
3、 变量不合理的作用域:若为成员变量,要及时null。
4、 数据库、网络、输入输出流等资源未显示关闭:若对象正在使用资源,JVM无法判断这些对象是否在进行操作。
解决办法:资源使用后,要调用close()方法关闭
5、 使用非静态内部类 :因为内部类对象会一直持有外部类对象的this引用(编译器自动添加的)。
4. 程序计数器概念
定义
-
记住下一条指令的执行地址
-
对指令地址的存储,是通过寄存器实现的(寄存器的读取速度最快)
5. 解释一下虚拟机栈?
1. 定义
- 每个线程运行时需要的内存,称为虚拟机栈
- 每个栈由多个栈帧组成,对应每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
描述
-
栈:线程运行时需要的内存空间
-
栈帧(站内元素):每个方法运行时的内存
-
执行方法时——>压入栈;完成方法后——>弹出栈
-
用栈结构的原因:后调用的方法,一定是子方法,需要先完成。外部类和内部类的关系
问题一:垃圾回收是否涉及栈内存?
答:不涉及。每次调用方法时压入栈帧,方法结束后即弹出。
问题二:栈内存分配越大越好吗?
答: 不是。在总内存固定的情况下,栈内存越大,线程数就越少,并发效果就越差
小知识:默认情况下,windows的栈内存是由虚拟内存决定的,其他的操作系统一般为1MB
2. 方法内的局部变量是否线程安全。
答:虚拟机栈对于局部变量来说是线程私有的,但若为static静态变量,则为多个线程公用一个变量,需要加安全保护,否则存在安全隐患。
- 如果方法内局部变量没有逃离方法的作用范围,就是线程安全的。
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
例题分析:以下几个函数是线程安全的吗?
- m1是线程安全的,因为其定义的是局部变量,因此只在一个虚拟机栈内访问,不存在安全隐患。
- m2不是线程安全的,因为其作为参数传递进来,外部变量可以干预,因此存在安全隐患。
- m3不是线程安全的,因为其作为返回结果,可能会被用作不同场景,可能存在安全隐患。
3. 栈内存溢出
-
栈帧过多导致栈内存溢出(递归调用)
-
栈帧过大导致栈内存溢出
-
报错:Java.long.StackOverflowError
-
设置栈内存:
线程运行诊断
案例1:cpu占用过多
定位
-
nohup java cn.itcast.jvm.tl.Demo1_16 & (运行java代码)
-
用top定位哪个进程对cpu占用过高
-
ps H -eo pid,tid, %cpu | grep 进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)
-
jstack 进程id(显示该进程下的线程以及线程的状态)
- 可根据线程id找到有问题的线程,进一步定位到问题代码的源码上
案例2:程序运行很长时间没有结果
原因:可能是发生了死锁
- jstack 进程id 定位,查看
总结:通过jstack排查死锁问题。
6. 解释一下JVM堆?
1. 定义
- 通过new关键字,创建对象都会使用堆内存
特点
- 线程共享的,堆中对象都需要考虑线程安全问题。(程序计数器、虚拟机栈、本地方法栈都是线程私有的)
- 有垃圾回收机制
2. 堆内存溢出与诊断
- 错误:Java.lang.OutOfMemoryError:Java heap space
- VM Options:-Xmx 8m(更改堆空间大小)
堆内存诊断
1 jps工具
- 产看当前系统中有哪些java进程
- 用法:
jps
(terminal中使用。(相当于idea自带的cmd))
2 jmap工具
- 查看某一时刻堆内存占用情况
- 用法:
jmap -heap 进程id
3 jconsole工具
- 图形界面的,多功能的监测工具,可以连续监测。
- 用法:
jconsole
案例
-
问题:垃圾回收后,内存占用仍然很高。
-
jvisualvm工具:可视化的查找 使用教程
7. 解释一下方法区?
1. 方法区内存溢出
-
1.8前导致永久代内存溢出
* 演示永久代内存溢出 java.lang.OutOfMemoryError:PermGen Space * -XX:MaxPermSize=8m
-
1.8后导致元空间内存溢出
* 演示元空间内存溢出 java.lang.OutOfMemoryError:Metaspace * -XX:MaxMetaspaceSize=8m
场景
- spring(动态代理机制:也就是在运行阶段生成大量的类,存放在方法区)
- mybatis
2. 运行时常量池是什么?
常量池:常量池就是一张表,指令根据这个表找到要执行的类名,方法名,参数、自变量等信息。
运行时常量池:常量池是 .class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址。
8. 直接内存应用场景?
1. 定义:
- DirectMemory
- 常见与NIO操作时,用于数据缓冲区
- 分配回收成本高,读写性能高
- 不受JVM内存回收管理(因此内存的回收与释放也与垃圾回收无关)
2. 载体:
ByteBuffer
缓冲区(Buffer)就是在内存中预留指定大小的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区:
总结:少了一次缓冲区的赋值操作
9. 类常量池?
诞生时间:编译时
所处区域:堆
储存内容:符号引用和字面量
10. 字符串常量池?
诞生时间:编译时
所处区域:堆
存储内容:字符串对象的引用和字符串常量、
位置
1.7以前常量池在JVM中,叫做永久代,也就是方法区的位置。
1.7从永久代中移动到了堆内存中。属于堆内存一部分
1.8移除永久代,由元空间代替,存放在本地内存
垃圾回收调优
打印串池中实例参数: -XX:+PrintStringTableStatistics
打印垃圾回收信息:-XX:+PrintGCDetails -verbose:gc
性能调优
**演示串池大小对性能的影响:**桶个数为20w时运行时间为0.4s,桶个数为2k时,运行时间为8s。
原因:需要创建与遍历更多的链表
HashTable原理:数组+链表
- -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
11. 运行时常量池?
诞生时间:当类加载到内存中后
所处区域:本地内存
存储内容:.class文件信息、编译后的代码数据、引用类型数据
第三章 垃圾回收
1. 如何判断对象可以回收?
1. 引用计数法
被引用则+1,反之-1,若引用数为0则可被回收
2. 可达性分析法
-
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
-
扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
-
下面判断哪些对象可以作为GC Root
-
执行语句(抓取实时运行结果生成文件):
-
jmap -dump:format=b,live,file=1.bin 22748
- jmap:查看堆中相关内容
- dump:转储到文件
- format:文件格式
- live:只转储活进程
- file:文件位置
-
利用Eclipse memory analyze分析文件
-
以下对象为GC root,无法被垃圾回收
引用链
2. 详细说一下四种引用?
-
强引用(StrongReference)
- 概述:如果一个对象具有强引用,那垃圾回收器绝不会回收它。
- 实现:
Object o=new Object();
- 销毁:
o=null
- 若为局部变量,则方法结束即被回收,若为全局变量,则不使用对象后一定要置null,因为强引用不会被垃圾回收
-
软引用(SoftReference)
-
概述:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
-
实现:
SoftReference<Object> softRef=new SoftReference<Object>(o);
-
销毁:
o=null; System.gc()
-
应用:实现高速缓存
Browser prev = new Browser(); // 获取页面进行浏览 SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用 if(sr.get()!=null){ rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取 }else{ prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了 sr = new SoftReference(prev); // 重新构建 }
-
-
弱引用(WeakReference)
- 概述:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 实现:
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
- 销毁:同上
- 应用:偶尔使用且不影响垃圾收集的对象
-
虚引用
- 概述:和没有任何引用一样,随时可能被回收。必须与引用队列联合使用,当被回收前,把虚引用加入与之关联的引用队列中。
- 应用:通过判断引用队列中是否加入虚引用来了解被引用的对象是否被gc回收,可以起到哨兵的作用。
-
终结器引用
- 概述:配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用 finalize 方法,第二次 GC 时回收被引用对象
-
四种引用比较
-
引用队列
程序可以通过判断引用队列中是否已经加入了引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
-
finalize与析构函数
(1).对象不一定会被回收。
(2).垃圾回收不是析构函数。
(3).垃圾回收只与内存有关。
(4).垃圾回收和finalize()都是靠不住的,
3. 垃圾回收算法都有什么?
1. 标记清除算法
- 图示(先标记,再清除)
- 优点:速度快(清除时直接标记首位地址,下次使用直接覆盖即可。)
- 缺点:易产生内存碎片
2. 标记整理算法
- 图示(先标记,再整理)
- 优点:不会有内存碎片
- 缺点:速度慢
3. 复制算法
- 图示(把可用内存复制到b空间,清除a空间垃圾,最后ab空间交换地址)
- 优点:不会有内存碎片
- 缺点:需要占用双倍内存空间
4. 解释一下分代垃圾回收?
1. 概述
- 图示:(堆=新生代+老年代)
-
对象首先分配在伊甸园预期
-
新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄+1,交换from,to
-
minor gc会引发stop the word,暂停其他用户的线程,等垃圾回收结束后,用户线程恢复运行。
-
当对象寿命超过阈值(15)时 或 新生代空间太紧张,晋升至老年代
-
当老年代空间不足,尝试触发minor gc,如果空间不足,那么触发full gc,STW时间更长
-
大对象直接晋升老年区(不会触发GC)
-
若新生代实在空间不足,则直接晋升老年代
2. 相关VM参数
5. 垃圾回收器
1. 串行垃圾回收器
开启方法:-XX:+UseSerialGC = Serial + SerialOld
解释:serial:工作在新生代的复制算法, SerialOld:工作在老年代的标记整理算法(serial:表串行)
图示:当有线程需垃圾回收时,所有线程全部停止,到达安全点等待,接下来进行某一线程的垃圾回收
3. 吞吐量优先垃圾回收器
开启方法:
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
:新生代的复制算法,老年代的标记整理算法(parallel:表并行)-XX:GCTimeRatio=ratio
:垃圾收集停顿时间(ratio:比率。 若为19,则是1/(1+19) = 5%,也就是说:允许垃圾收集停顿的时间占5%)(默认值为1%)-XX:MaxGCPauseMillis=ms
:允许最大停顿时间-XX:ParallelGCThreads=n
:启动的最多线程的个数
4. 响应时间优先(CMS)垃圾回收器
相关参数:
-XX:+UseConcMarkSweepGC~ -XX:+UseParNewGC ~ SerialOld
:老年代并发(concurrent)标记清除(并发表示该进程在垃圾回收的同时,其他线程也可以运行 )新生代并行复制算法老年代串行标记清除(当并发失败时)XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
:并行线程数or并发线程数(并行为真正共同执行,一般并发为并行的1/4,也就是4个同时运行的线程中有一个为并发)-XX:CMSInitiatingOccupancyFraction=percent
:执行CMS垃圾回收的内存占比-XX:+CMSScavengeBeforeRemark
:开启在CMS重新标记阶段之前的清除尝试(cms gc会以新生代作为gc root的一部分,因此在remark之前做一次ygc,可以回收掉大部分对象,从而减少gc root扫描的开销)
运行流程:
- 初始标记: 暂停所有的其他线程,初始标记仅仅标记GC Roots能直接关联到的对象,速度很快;
- 并发标记:从GC roots的直接关联对象开始遍历整个对象图,耗时较长,但不停顿。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(采用多线程并行执行来提升效率);需要"STW",停顿时间稍长,但远比并发标记短;
- 并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫,回收所有的垃圾对象;
特点
-
低延迟
-
CMS中,确保应用程序用户线程有足够内存可用
-
应用标记-清除算法。因此会产生内存碎片
-
对CPU资源非常敏感,因为并发执行时会占用线程导致程序变慢,总吞吐量降低。
-
无法处理浮动垃圾。也就是无法处理并发标记阶段新产生的垃圾。
5. G1垃圾回收器
1. 定义
Garbage First。 G1跟踪各个region里面垃圾值大小,维护一个优先列表,优先回收最大值的垃圾。因此叫做Garbage First。
相关JVM参数
-
-XX:+UseG1GC
:使用G1垃圾回收算法 -
-XX:G1HeapRegionSize=size
:G1堆区域的划分大小 -
-XX:MaxGCPauseMillis=time
:默认的最大暂停时间
图示:
算法:
- Region间是复制算法,整体上是标记-压缩算法。
Houmongous:
- Houmongous:若一个对象>分区50%容量,则为巨型对象,默认存放于该区域(原因:若其为短期对象,则放入老年代会造成负面影响)。 若该区域空间不足,则Full GC
2. RSet集合
每一个Region都有一个对象的Remebered Set:
每次Reference类型数据写操作时,都会产生Write Barrier暂时中断操作,然后检查要写入的引用指向的对象是否和该Reference类型数据在不同的Region
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remebered Set中。
当进行垃圾回收时,在GC根节点枚举范围加入Remebered Set,就可以保证不进行全局扫描,也不会有遗漏
3. 指针碰撞
- 某个Region中新加入对象,就会发生指针碰撞,也就是指针所指地址后移
TLAB(新生代中线程私有缓冲区)
- TLAB thread-local allocation buffer(线程 局部(私有) 分配 缓冲区)
- 在new时,优先检查TLAB中是否有空间,若有,则优先分配这里
- 因为TLAB是线程私有的,因此即使多个线程同时分配内存,也不会产生冲突
G1垃圾回收过程
- 年轻代GC(ygc)
- 老年代并发标记过程(concurrent marking)
- 混合回收(mixed gc)
- (如果需要,单线程、独占式、高强度的F GC存在,针对GC的评估失败提供了一种失败保护机制,即强力回收)
应用程序分配内存,当Eden区用尽后开始ygc,为并行独占式收集器,暂停其他线程,将存活对象放入Survivor区or老年区。
当堆内存到达一定值(45%),开始老年代并发标记过程。
标记完成后开始Mixed Gc,清理部分老年代对象,不需要回收全部老年代,只需扫描一小部分Region。
4. 优劣
- 内存占用or程序运行时的额外执行负载都比CMS高。
- CMS在小内存应用上大概率由于G1
适用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms。
- 超大堆内存,会将划分为多个大小相等的Region
JDK8字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了CPU时间,新生代回收时间略微增加
-XX:+UseStringDeduplication
String s1 = new String("hello"); //char[]{'h','e','l','l','o'};
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果值一样,引用同一个char[]
- 注意,与String.intern()不同
- String.intern()关注的是字符串对象
- 字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
JDK8u40 G1_类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再被使用,则卸载它所加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark
默认启用
6. 垃圾回收如何调优?应用场景?
1. 确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收期
- CMS、G1、ZGC(超低延迟)
- ParallelGC
2. 常规检查
- 查看FullGC前后的内存占用,考虑以下问题
- 数据是不是太多
- resultSet = statement.executeQuery(“select * from 大表 limit n”);
- 数据表示是否臃肿
- 对象图
- 对象大小 16 Integer 24 int 4
- 是否存在内存泄漏
- static Map map =
- 软引用
- 弱引用
- 第三方缓存实现
- 数据是不是太多
3. 新生代调优
新生代特点
- 所有new 操作的内存分配非常廉价:TLAB
- 死亡对象的回收代价是零: 复制算法,复制后直接抛弃,无需整理等,因此代价为0
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
新生代越大越好吗?
-Xmn:设置新生代占用内存
-
新生代能容纳所有【并发量 * (请求-响应)】的数据
-
幸存区大到能保留【当前活跃对象+需要晋升对象】
-
晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
:打印幸存区中对象的年龄分布
最佳占比:25%~50%
4. 老年代调优
以CMS为例
- CMS的老年代内存越大越好(拥有更多空间避免浮动垃圾导致并发失败)
- 先尝试不做调优,观察是否发生Full GC,优先调试新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
-
-XX:CMSInitiatingOccupancyFraction=percent
-
5. 案例
案例1: Full GC 和Minor GC频繁
- 原因:内存设置过小。
- 解决方法:试着增大新生代内存
案例2:请求高峰期发生Full GC,单次暂停时间特别长(CMS)
- 查看GC日志,查看哪个阶段耗时长
- 如果是重新标记时间长,可以在重新标记前先进行一次ygc
案例3:老年代充裕情况下,发生Full GC(CMS jdk1.7)
- 1.7版本前,永久代空间不足,也会导致Full GC
第四章 类加载器
1. JVM加载Class文件的原理机制?
类加载器也是个类,工作是把class文件从硬盘读取到内存中。
1. 类装载方式:
1、隐式装载:程序运行时碰到new等方式生成的对象时,隐式调用类加载器加载对应类到JVM。
2、显式装载:使用class.formname()方法。动态的,不会一次性将所有类全加载再运行,而是保证程序运行的基础类完全加载到JVM中,其他类有需要再加载。
2. 类装载步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
2. 类加载器分类?
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
其他类加载器:
扩展类加载器(Extension ClassLoader):负责加载\\lib\\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
3. 双亲委派模式是什么?
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
注意
这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系
例如:
public class Load5_3 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
-
sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
-
sun.misc.Launcher$AppClassLoader // 2 处,委派上级
sun.misc.Launcher$ExtClassLoader.loadClass()
- sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
- sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader
查找
-
BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
-
sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 fifindClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 处
-
继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 fifindClass 方法,在classpath 下查找,找到了
4. 线程上下文类加载器?
问题:在使用 JDBC 时,都需要加载 Driver 驱动,但即使不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的(加载就是把Driver驱动加载到DriverManager中)
因为DriverManager中定义了加载Driver类的load方法:loadInitialDrivers()。
在loadInitialDrivers();中定义了两种方法,都能使Driver完成加载,两种方法使用的都是应用程序类加载器,也就是说DriverManager并没有遵守双亲委派机制。
总结:
DriverManager内定义了加载方法,可以用SPI机制或使用 jdbc.drivers 定义的驱动名加载驱动。在SPI机制中,使用线程上下文类加载器加载驱动,而前者默认就是应用程序类加载器。 在后一个机制中,直接使用应用程序类加载器完成加载。
5. 自定义类加载器?
什么时候需要自定义类加载器
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
-
继承 ClassLoader 父类
-
要遵从双亲委派机制,重写 fifindClass 方法
注意不是重写 loadClass 方法,否则不会走双亲委派机制
-
读取类文件的字节码
-
调用父类的 defifineClass 方法来加载类
-
使用者调用该类加载器的 loadClass 方法
第五章 运行期优化
1. 即时编译
1. 概述
运行:
class JIT1 {
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\\t%d\\n", i, (end - start));
}
}
}
结果:
0 96426
86 17920
136 853
原因:
JVM 将执行状态分成了 5 个层次:
- 0 层,解释执行(Interpreter)
- 1 层,使用 C1 即时编译器编译执行(不带 profifiling)
- 2 层,使用 C1 即时编译器编译执行(带基本的 profifiling)
- 3 层,使用 C1 即时编译器编译执行(带完全的 profifiling)
- 4 层,使用 C2 即时编译器编译执行
profifiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
即时编译器(JIT)与解释器的区别
-
解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
-
JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
-
解释器是将字节码解释为针对所有平台都通用的机器码
-
JIT 会根据平台类型,生成平台特定的机器码
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之
刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果
2. 逃逸分析
如果一个变量的使用,在运行期检测 他的作用范围不会超过一个方法或者一个线程的作用域。那么这个变量就不会被多个线程所共享,也就是说 可以不将其分配在堆空间中,而是将其线程私有化。
3. 热点探测
jvm 通过统计 每个方法调用栈的栈顶 一个方法栈帧的弹出频率 来作为一个指标。有两种方法,
-
一使用精确的计数器进行精确计数,超过阈值触发编译。
-
二是记录一段时间内方法调用次数(方法调用的频 率) 超过阈值触发编译。并存在热度衰减,超过一定时间范围没有继续调用 该方法则会 将其值减半。
2. 方法内联
方法内联
(Inlining)
private static int square(final int i) {
return i * i;
}
System.out.println(square(9));
如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:
System.out.println(9 * 9);
1. 原理
- 调用某个函数实际上将程序执行顺序转移到该函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。
- 这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。也就是通常说的压栈和出栈。
- 因此,函数调用要有一定的时间和空间方面的开销。那么对于那些函数体代码不是很大,又频繁调用的函数来说,这个时间和空间的消耗会很大。
2. 内联函数
-
那怎么解决这个性能消耗问题呢,这个时候需要引入内联函数了。内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。
-
显然,这样就不会产生转去转回的问题,但是由于在编译时将函数体中的代码被替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间代销上不象函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。
以上是关于Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列的主要内容,如果未能解决你的问题,请参考以下文章