目录
14. Java 8 为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
22. JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代?
23. JVM新生代中为什么要分为Eden和Survivor?
【写在前面】
此文题目和答案都非原创。
是搜集了网络上分享的关于Java面试常见问题汇总,然后又逐个搜寻了答案,整理于此。
供自学,感谢相关题目和答案的原创分享者。
1. 如何判断一个常量是废弃常量 ?
运行时常量池主要回收的是废弃的常量。
假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。
注:JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
2. 程序计数器为什么是私有的?
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
注:如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
3. JRE、JDK、JVM 及 JIT 之间有什么不同?
(1)java虚拟机(JVM)
使用java编程语言的主要优势就是平台的独立性。java实现平台的独立性就是通过虚拟机,它抽象化了硬件设备,开发者和他们的程序得以操作系统。虚拟机的职责就是处理和操作系统之间的交流。java不同的接口规范对任何平台都有良好的支持,因为jvm很好的实现了每个平台的规范。jvm可以理解伪代码字节码,在用户和操作系统之间建立了一层枢纽。
(2)java运行时环境(JRE)
java运行时环境是JVM的一个超集。JVM对于一个平台或者操作系统是明确的,而JRE是一个一般的概念,他代表了完整的运行时环境。我们在jre文件夹中看到的所有的jar文件和可执行文件都会变成运行时的一部分。事实上,运行时JRE变成了JVM。所以对于一般情况时候使用JRE,对于明确的操作系统来说使用JVM。当你下载了JRE的时候,也就自动下载了JVM。
(3)java开发工具箱(JDK)
java开发工具箱指的是编写一个java应用所需要的所有jar文件和可执行文件。事实上,JRE是JDK的一部分。如果下载了JDK,会看到一个名叫JRE的文件夹在里面。JDK中要被牢记的jar文件就是tools.jar,它包含了用于执行java文档的类还有用于类签名的jar包。
(4)即时编译器(JIT)
即时编译器是种特殊的编译器,它通过有效的把字节码变成机器码来提高JVM的效率。JIT这种功效很特殊,因为他把检测到的相似的字节码编译成单一运行的机器码,从而节省了CPU的使用。这和其他的字节码编译器不同,因为他是运行时(第一类执行的编译?) the firs of its kind to perform the compilation (从字节码到机器码)而不是在程序运行之前。正是因为这些,动态编译这个词汇才和JIT有那么紧密的关系。
4. JVM调优命令有哪些?
4.1 前置操作先定位pid、tid:
一般是运维团队首先受到报警信息(CPU Memory)
top命令观察到问题:内存不断增长 CPU占用率居高不下
top -Hp 观察进程中的线程,哪个线程CPU和内存占比高
4.2 常用调优命令
(1)jstack:导出java进程中各个栈内线程信息 eg:jstack -l pid >>pid.txt
(2)jinfo pid: 查看当前虚拟机的运行参数。
(3)jconsole/jvisualVM:图形化界面监测jvm进程中线程、cpu、内存、变量 如果连接远程机需要远程机上的项目启动时设置相关参数,本地线程无需设置,如何实现不配置远程主机的启动参数实现监测。
(4)jmap:是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生了哪些对象,及其数量,对象内部存值等)。 jmap可以主动导出jvm内存信息,也可以在项目启动时指定在程序内存溢出时自动导出内存信息。
(5)Memory Analyzer(MAT):分析堆内存DUMP 文件。
(6)jhat:是sun 1.6及以上版本中自带的一个用于分析JVM 堆DUMP 文件的工具,基于此工具可分析JVM HEAP 中对象的内存占用情况:jhat -J-Xmx1024M [file] 。
5. 说一下 JVM 调优的工具
《JVM性能调优》:JVM性能调优_白白胖胖充满希望-CSDN博客_jvm调优
《JVM系列(七):jvm调优-工具篇》:JVM系列(七):jvm调优-工具篇 - 简书
《10. JVM常用命令》:10. JVM常用命令_wwww641788662的博客-CSDN博客_jvm命令
《JVM调优之JConsole和JVisualVM工具使用》:JVM调优之JConsole和JVisualVM工具使用_Thancks-CSDN博客_jconsole
《jstack查找CPU高和死锁》:jstack_JustryDeng-CSDN博客_jstack
6. 介绍一下类文件结构吧
根据 Java 虚拟机规范,类文件由单个 ClassFile 结构组成:
7. 如何判断一个类是无用的类
方法区主要回收的是无用的类。
要判定一个类是否是“无用的类”需要同时满足下面3个条件才能算是 “无用的类” :
(1)这个类的实例被回收了,java堆中没有任何这个类的实例。
(2)这个类的ClassLoader被回收了。
(3)这个类的.class对象没有在任何地方被引用,无法在任何地方通过反射访问这个类的方法。
8. 如何判断对象已经死亡了呢
(1)引用计数法
程序给对象添加一个引用计数器,每有一个变量引用它时,计数器加1。当引用断开时,计数器减1。当计数器为0时,代表着没有任何变量引用它,该对象就是死亡状态,JVM需要对此类对象进行回收。
引用计数法的实现简单,效率也很高。但绝大数主流的虚拟机并没有采取此计数算法来管理内存,原因是此计数算法无法回收那些具有相互循环引用的对象,此类对象确实已经不再被使用,但由于互相引用着对方,导致各自的计数器都不为0,因此JVM无法回收它们。
(2)可达性分析法
程序创建一系列的GC Roots作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象与GC Roots没有任何引用链相连的话,即此对象到GC Roots不可达,则证明此对象是不可用的,JVM稍后将会对此类对象进行回收。
大多数主流的JVM都采用这样的算法来管理内存,它能够解决对象之间的循环引用的问题。对象与对象之间虽然有循环引用,当他们到GC Roots没有任何引用链,系统还是判定它们为可回收对象。
当通过这两种方式确定对象已经没有任何变量引用它们时,JVM将在合适的时机对此类对象进行回收。
9. Java会存在内存泄漏吗?请简单描述
9.1 内存泄露:就是指一个不再被程序使用的对象或变量一直被占据在内存中。java中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉。由于Java使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的
9.2 java中的内存泄露的情况:
理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。例如Hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。
(1)长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
检查java中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。
(2)如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
(3)当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。
10. Minor Gc和Full GC 有什么不同呢
(1)新生代GC(Minor GC)
指发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
(2)老年代GC(Major GC/Full GC)
指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scanvenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
11. 堆内存中对象的分配的基本策略
(1)将新生代分为Eden区,From区,To区是基于其所用的垃圾回收决定的(标记复制算法)。这3个区的内存分配过程如下:
1)对象优先在Eden区分配,当进行YGC时,会将存活的对象放到From区,To区空着不用哈。
2)当第二次进行YGC时,会将From区和Eden区存活的对象复制到To区,此时Eden区和From区就为空哈。
3)当第三次进行YGC时,会将To区和Eden区存活的对象复制到From区,此时Eden区和To就为空。按照此规律,循环往复下去
(2)对象的分配策略:
对象优先在Eden区分配,如果能放下,就放在Eden区。如果放不下就会进行一次YGC(此时Eden区和一个Survivor区为空哈)。再次尝试在Eden区分配,如果能放下,就放在Eden区。
如果放不下,看Old区能否放得下。如果能放下,就放在Old区。如果放不下,则进行一次FGC,再次尝试在Old区分配,如果能放下,就放在Old区,否则报OOM
(3)当进行YGC时,可能会引起后续一系列的各种操作:
当老年代可用空间大于新生代大小时,直接触发YGC即可。否则判断老年代可用空间是否大于历次晋升到老年代对象的平均大小。是的话触发YGC,否则说明Old区有可能放不下新生代存活下来的对象,先触发一次FGC,再触发YGC
前面说过当发生YGC的时候,会将Eden区和其中一个Survivor区存活的对象移动到另一个Survivor区。当Survivor空间够时,将存活的对象放在Survivor区。
当Old区空间够时,将存活对象放在Old区。如果不够,触发FGC,再次判断是否能放下。是,则放在Old区,否则OOM
(4)内存分配策略:
对象优先在Eden分配
长期存活的对象将进入老年代
空间分配担保
12. 对象的访问定位有哪几种方式
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:①使用句柄和②直接指针两种:
(1)句柄:如果使用句柄,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
(2)直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。
13. Java对象的创建过程
(1)类加载检查
(2)分配内存
(3)初始化零值
(4)设置对象头
(5)执行init方法
(参考文章:Java对象的创建过程_hash_Delhi的博客-CSDN博客_说一下java对象的创建过程)
14. Java 8 为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
参考文章:
JDK8-废弃永久代(PermGen)迎来元空间(Metaspace) - 余磊 - 博客园 (cnblogs.com)
Davids原理探究:JDK8将永久代(PermGen)替换为元空间(MetaSpace)的原因_Davids_的博客-CSDN博客
15. 说一下堆和栈的区别
16. 怎么打破双亲委派模型?
16.1什么是双亲委派模式
(1)原理
当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。
(2)具体解析
根据双亲委派模式,在加载类文件的时候,子类加载器首先将加载请求委托给它的父加载器,父加载器会检测自己是否已经加载过类,如果已经加载则加载过程结束,如果没有加载的话则请求继续向上传递至Bootstrap ClassLoader。如果请求向上委托过程中,如果始终没有检测到该类已经加载,则Bootstrap ClassLoader开始尝试从其对应路劲中加载该类文件,如果失败则由子类加载器继续尝试加载,直至发起加载请求的子加载器为止。
16.2 为什么要使用双亲委派模型
(1)假设没有双亲委派模型,试想一个场景:
黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
(2)而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
(3)或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
(4)举个简单例子:
ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。
参考文章:
(1条消息) 什么是双亲委派模型,JDBC是如何打破双亲委派模型的_不努力怎么娶方总的博客-CSDN博客_jdbc 双亲委派
16.3 为什么要打破双亲委派模式
参考文章:
(1条消息) 聊聊JDBC是如何破坏双亲委派机制的_P19777的博客-CSDN博客_jdbc破坏双亲委派
17. 怎么打出线程栈信息
(1)jvisualvm
(2)jstack命令
18. 说说你知道的几种主要的JVM参数
(1)堆栈配置相关
-Xmx3550m: 最大堆大小为3550m。
-Xms3550m: 设置初始堆大小为3550m。
-Xmn2g: 设置年轻代大小为2g。
-Xss128k: 每个线程的堆栈大小为128k。
-XX:MaxPermSize: 设置持久代大小为16m
-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
(2)垃圾收集器相关
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20: 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
(3)辅助信息相关
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
19. 什么是happen-before原则
参考文章:
happen-before原则的理解 - 牛新宇 - 博客园 (cnblogs.com)
20.什么是内存屏障
参考文章:
(1条消息) 什么是内存屏障(Memory Barrier)以及在java中的应用_MayMatrix 的博客-CSDN博客_什么是内存屏障
21. 什么是指令重排序?
重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。
重排序的 3 种情况:
(1)编译器优化
编译器(包括 JVM、JIT 编译器等)出于优化的目的,例如当前有了数据 a,把对 a 的操作放到一起效率会更高,避免读取 b 后又返回来重新读取 a 的时间开销,此时在编译的过程中会进行一定程度的重排。不过重排序并不意味着可以任意排序,它需要需要保证重排序后,不改变单线程内的语义,否则如果能任意排序的话,程序早就逻辑混乱了。
(2)CPU 重排序
CPU 同样会有优化行为,这里的优化和编译器优化类似,都是通过乱序执行的技术来提高整体的执行效率。所以即使之前编译器不发生重排,CPU 也可能进行重排,我们在开发中,一定要考虑到重排序带来的后果。
(3) 内存的“重排序”
内存系统内不存在真正的重排序,但是内存会带来看上去和重排序一样的效果,所以这里的“重排序”打了双引号。由于内存有缓存的存在,在 JMM 里表现为主存和本地内存,而主存和本地内存的内容可能不一致,所以这也会导致程序表现出乱序的行为。
22. JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代?
(1)对象诞生即新生代->eden,在进行minor gc过程中,如果依旧存活,移动到from,变成Survivor,进行标记。当一个对象存活默认超过15次都没有被回收掉,就会进入老年代。
(2)详解:
Java堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。
大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;
如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。
(3)可达性分析算法(GC Roots)
有一种引用计数法,可以用来判断对象被引用的次数,在任何某一具体时刻,如果引用次数为0,则代表可以被回收。
这种实现方式比较简单,但对于循环引用的情况束手无策,所以 Java 采用了可达性分析算法。
即判断某个对象是否与 GC Roots 的这类对象之间的路径可达,若不可达,则有可能成为回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
在 Java 中,可作为 GC Roots 的对象包括以下几种:
虚拟机栈(本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
23. JVM新生代中为什么要分为Eden和Survivor?
(1)如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
(2)Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
(3)设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。
24. 什么情况下会发生栈内存溢出?
(1)Java中的栈一般存储的是栈帧。所以栈内存溢出就是栈帧的数量太多超过了系统预先设定的值,所以导致内存溢出。可能的原因就是方法循环调用,栈帧充满了整个栈后溢出。
(2)栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口灯信息。局部变量表又包含基本数据类型,对象引用类型(局部变量表编译器完成,运行期间不会变化)
(3)所以我们可以理解为栈溢出就是方法执行是创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调用产生这种结果。
(4)如何解决:我们需要使用参数 -Xss 去调整JVM栈的大小. 但是需要注意递归程序的风险。
25. Java对象的布局了解过吗?
(1)只有类和数组在java虚拟机里面是对象的形式的创建的,所以我们说的对象通常就是指类和数组的实例。
(2)根据java虚拟机规范里面的描述:java对象分为三部分:对象头(Object Header), 实例数据(instance data),对齐填充(padding)。以对象(Ojbect)为例分析每一部分组成。数组与对象类似,只是对象头部分多了数组长度Length的存储长度为4字节。
(3)普通对象
------对象头 markword
------ClassPointer指针 (启动参数 -XX:+UserCompressedClassPointers 开启时为4字节,不开启为8字节)
------实例数据 (引用类型, -XX:+UserCompressedClassPointers 开启时为4字节,不开启为8字节)
------Padding对齐 8的倍数
数组对象
------对象头 markword
------ClassPointer指针 (启动参数 -XX:+UserCompressedClassPointers 开启时为4字节,不开启为8字节)
------数组的长度
------实例数据 (引用类型, -XX:+UserCompressedClassPointers 开启时为4字节,不开启为8字节)
------Padding对齐 8的倍数
26. Tomcat是怎么打破双亲委派机制的呢?
参考文章:1.5 tomcat是如何打破双亲委派机制的? - 盛开的太阳 - 博客园 (cnblogs.com)
27. 说下有哪些类加载器?
(1)Bootstrap Loader(引导类加载器)。它的实现依赖于底层操作系统,由C编写而成,没有继承于ClassLoader类。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。默认为jre目录下的lib目录下的class文件,该加载器没有父加载器。负责加载虚拟机的核心类库,如java.lang.*。Object类就是由根类加载器加载的。
(2)Extended Loader(标准扩展类加载器)。它的父加载器为根类加载器。由java编写而成,是ClassLoader的子类。它从java.ext.dirs中加载类库,或者从JDK安装目录jre\lib\ext子目录下加载类库。如果把用户创建的jar文件放在该目录下,也会自动由扩展类加载器加载。
(3)AppClass Loader(应用程序类路径类加载器)。它的父加载器为扩展类加载器。由java编写而成,是ClassLoader的子类,它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义的类加载器的默认父加载器。
此外还有参考文章:我把JVM的类加载器整理了一下 - 良许Linux - 博客园 (cnblogs.com)
28. 说说类加载的过程
类加载的过程详解:加载、验证、准备、解析和初始化。
详解参考文章:面试官:说说类的加载过程 | 建议收藏 - 知乎 (zhihu.com)
29. ZGC收集器中的染色指针有什么用?
参考文章: (1条消息) 深入理解JVM - ZGC垃圾收集器_极客侠的博客-CSDN博客
30. 说说ZGC垃圾收集器的工作原理
参考文章: (1条消息) 深入理解JVM - ZGC垃圾收集器_极客侠的博客-CSDN博客
31. 说说G1垃圾收集器的工作原理
参考文章:深入理解G1垃圾收集器 - 简书 (jianshu.com)
32. 说说CMS垃圾收集器的工作原理
参考文章;
(1条消息) 深入理解JVM之——CMS垃圾收集器的工作原理解析_天涯一隅的博客-CSDN博客
33. 你了解过哪些垃圾收集器?
参考文章:关于垃圾收集器你了解多少?一文总结七大垃圾收集器 - 尚码园 (shangmayuan.com)
34. 对象都是优先分配在年轻代上的吗?
(1)对象的内存分配,在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
(2)对象的内存分配——对象优先在Eden分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
35. GC Roots有哪些?
最开始的这些对象集合又被叫做 GC Roots集合。一般可以理解为堆外指针指向堆内的引用。GC Roots包括:
- 在虚拟机栈中引用的对象
- 在本地方法栈中JNI(即通常说的native方法)引用的对象
- 在方法区中类静态属性引用的对象
- 在方法区中常量引用的对象,比如字符串常量池(String Table)里的引用
- 所有被同步锁(synchronized关键字)持有的对象
- java虚拟机内部的引用(比如基本数据类型对应的Class对象,常驻的异常对象NullPointExcepiton、OutOfMemoryError等),系统类加载器
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
- 根据垃圾回收器和当前回收的内存区域不同,其他“临时性”加入的对象。主要是和当前回收区域相联的其他内存区域
36. JVM怎么判断一个对象是不是要回收?
(1)简单来说,如果一个对象没有被其他对象所引用,那么jvm就会认为它是“无用”的对象,可以回收了。那么怎么判断一个对象有没有被引用呢?
(2)最容易想到的就是引用计数法。用一个变量来记录对象被引用的次数,如果对象被引用了就+1,如果对象被取消了引用就-1。当这个变量的值为0时,就说明它没有被引用,可以被回收了。这种方法简单明了,容易判断。在实际应用中,也有使用引用计数法来管理内存的,比如Python ,使用ActionScript 3的FlashPlayer等。那么它有什么缺点吗?首先想到的就是记录引用次数的变量需要频繁的更新,要保证这个值绝对正确。除此之外,引用计数法还有一个比较大的漏洞,就是无法判断循环引用的情况。
(3)什么是循环引用呢?假设有两个对象a和b。a和b互相引用,除此之外没有其他对象引用a或b。那么a和b其实是“无用的”,对程序来说没有任何用途,应该被回收。如果用引用计数法来判断的话,a和b的引用计数都为1,不为0,a和b不应该被回收。如果出现极端情况,系统中存在大量的循环引用,那么jvm将有大量的内存无法回收。
(4)所以如果要使用引用计数法,需要配合大量额外的处理才能保证它正确的工作。
(5)目前主流的jvm采用可达性分析算法来判断对象是否可以回收。那么什么是可达性分析算法?简单来说,可达性分析就是从特定的对象集合出发,分析所有能被这个对象集合引用的对象。如果从对象集合出发,没有任何路径可以到达一个对象,说明这个对象是“不可达的”,也就是没有被其他对象引用的,是“无用的,应该被回收掉。
37. Java里有哪些引用类型?
(1)java内存管理分为内存分配和内存回收,都不需要程序员负责,垃圾回收的机制主要是看对象是否有引用指向该对象。
(2)java对象的引用包括
强引用,软引用,弱引用,虚引用
(3)Java中提供这四种引用类型主要有两个目的:
第一是可以让程序员通过代码的方式决定某些对象的生命周期;
第二是有利于JVM进行垃圾回收。
38. 你熟悉哪些垃圾收集算法?
参考文章:垃圾收集算法有哪些?以及它们各自的优缺点 - 掘金 (juejin.cn)
39. 字符串常量存放在哪个区域?
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。
40. 程序计数器有什么作用?
程序计数器是用于存放执行指令的地方。
为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。而程序计数器正是起到这种作用,所以通常又称为指令计数器。
在程序开始执行前,必须将它的起始地址,即程序的一条指令所在的内存单元地址送入PC,因此程序计数器(PC)的内容即是从内存提取的第一条指令的地址。
41. 栈帧里面包含哪些东西?
当线程执行到某个方法时就会往方线程栈中压入一个帧,称为栈帧,栈帧中包含了方法的局部变量表、操作数栈、返回地址、动态连接等信息