前言
题目汇总来源 史上最全各类面试题汇总,没有之一,不接受反驳
目录
你知道哪些或者你们线上使⽤什么GC策略?它有什么优势,适⽤于什么场景?
Java类加载器包括⼏种?它们之间的⽗⼦关系是怎么样的?双亲委派机制是什么意思?有什么好处?
如何⾃定义⼀个类加载器?你使⽤过哪些或者你在什么场景下需要⼀个⾃定义的类加载器吗?
Perm Space中保存什么数据?会引起OutOfMemory吗?
做GC时,⼀个对象在内存各个Space中被移动的顺序是什么?
你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理 过程中有哪些收获?
StackOverflow异常有没有遇到过?⼀般你猜测会在什么情况下被触发?如何指定⼀个线程的堆栈⼤⼩?⼀般你们写多少?
请解释StackOverflowError和OutOfMemeryError的区别?
JVM
谈谈你对解析与分派的认识。
分派:静态分派与动态分派。
解析和分派属于方法调用的内容。
方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部具体的运行过程。
解析
将一部分(编译期可知,运行期不可变)的符号引用转为直接引用
调用方法的四种字节码:
invokestatic 调用静态方法
invokespacial 调用<init>、私有方法和父类方法
invokevirtual 调用所有虚方法
invokeinterface 调用接口方法,运行时确定实现此接口的方法
能被 invokestatic 和 invokespecial 调用的方法可以在解析时确定唯一版本调用。
Java中符合的有静态方法、私有方法、实例构造器和父类方法,它们不可继承或重写,称为非虚方法。
另外,final方法也是非虚方法,虽然是 invokevirtual 指令调用,但无法被覆盖,对多态来说选择结果唯一。
分派
分派与面向对象的三个基本特征之一:多态有关,如重载和重写。
解析和分派不是对立关系,如重载静态方法,既发生了解析,又发生了静态分派。
静态分派
依据静态类型来定位方法执行版本的分派动作称为静态分派。
和方法重载有关,重载方法的参数类型是依据静态类型,而静态类型编译期已知。
动态分派
运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
单分派和多分派
Java静态多分派,动态单分派
静态分派,根据静态类型和参数类型两个宗量进行选择,多分派
动态分派,传入参数已确定,只要判断最终执行对象类型,单分派。
你知道哪些或者你们线上使⽤什么GC策略?它有什么优势,适⽤于什么场景?
Minor GC:指发生在新生代的垃圾收集动作
MajorGC / Full GC:指发生在老年代的GC
对象优先分配在Eden区
eden区空间不足时Minor GC
大对象直接进入老年代
避免短命大对象
长期存活对象进入老年代
每个对象有一个对象年龄(Age)计数器
Eden区一个对象第一次Minor GC存活并能被Survivor区容纳,则移至Survivor且对象年龄设为1
在Survivor中每经过一次Minor GC对象年龄+1
到达一定年龄(默认15)移至老年代
动态对象年龄判断
Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,大于等于该年龄的对象直接进入老年代
空间分配担保
Minor GC时,若之前每次晋升入老年代的平均大小>老年代剩余空间,则直接Full GC
否则若设置允许担保失败,则只Minor GC,否则Full GC
Java类加载器包括⼏种?它们之间的⽗⼦关系是怎么样的?双亲委派机制是什么意思?有什么好处?
Java类加载器包括几种?父子关系?
这里的层次关系为组合,即子加载器持有父加载器的引用,而非继承关系。
Application ClassLoader 应该和 System ClassLoader 是一个意思
双亲委派机制
双亲委派模型如上图所示。
工作过程:一个类加载器收到类加载请求,先把这个请求委托给父类加载器完成,因此所有加载请求都会传送到顶层启动加载器中,只有当父加载器反馈自己无法完成加载请求(其搜索范围没有找到所需类),子类才尝试自己加载。
双亲委派机制好处
保证java核心库的安全性(例如:如果用户自己写了一个java.lang.String类就会因为双亲委派机制不能被加载,不会破坏原生的String类的加载)
如何⾃定义⼀个类加载器?你使⽤过哪些或者你在什么场景下需要⼀个⾃定义的类加载器吗?
- 加载特定路径的class文件
- 加载一个加密的网络class文件
- 热部署加载class文件
双亲委派流程
loadClass
找缓存 → 找父加载器 loadClass → 自己尝试 findClass → 根据条件解析 class
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass
JDK1.2后不建议覆盖 loadClass,将自定义加载类的过程放在 findClass 里。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
双亲委派机制打破
服务提供者接口SPI存在于rt.jar,由Bootstrap类加载器加载,但服务方提供的SPI实现类无法由Bootstrap类加载器加载,需由App类加载器加载。Thread类中的contextClassLoader默认保存App类加载器的引用,Bootstrap类加载器以此委托App类加载器加载SPI实现类。但这就打破了双亲委派模型。
堆内存设置的参数是什么?
类加载过程
加载:通过类的全限定名获取二进制流,加载入方法区,生成一个代表这个类的java.lang.Class对象。
验证:确保class字节流符合规定且安全。
准备:类变量赋初值(准确说是置零)。
解析:符号引用转换为直接引用。
初始化:调用类的<clinit>()方法,初始化类的资源和变量。
类加载时机
- new新对象 / 读取或设置static属性 / 调用static方法;
- java.lang.reflect对类反射;
- 子类初始化时先初始化父类;
- 虚拟机启动时先初始化main方法所在的主类;
- jdk1.7动态语言支持
Perm Space中保存什么数据?会引起OutOfMemory吗?
Java8内存模型—永久代(PermGen)和元空间(Metaspace)
JAVA 方法区与堆--java7前,java7,java8各不相同
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
java7中,存储在永久代的部分数据就已经转移到Java Heap或者Native memory。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap。
java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。
垃圾回收算法
标记清除算法 Mark-Sweep
第一遍扫描内存标记出无用对象,第二遍扫描内存清除无用对象。
效率低且会造成内存碎片化。
复制算法 Copying
将存活对象复制到新内存空间中,清空旧内存空间。
适用于对象存活率低的情况。
JVM中,新生代的Survivor区用到该算法。
标记整理算法 Mark-Compact
在标记清除算法基础上,把对象移向内存一端,留出连续的内存空间,解决碎片化问题。
适用于对象较大、存活率较高的情况。
JVM中,老年代使用该算法。
分代收集算法 Generational Collection
JVM中,新生代使用Copying,老年代使用Mark-Compact。
做GC时,⼀个对象在内存各个Space中被移动的顺序是什么?
参照上面GC策略的过程。
普通对象生于Eden区,然后在两个Survivor区中往返,最后进入老年代。中途死于各种GC。
大对象或通过动态对象年龄判定的对象直接进入老年代。死于Major GC。
哪些对象可以作为GC Root
- 通过System Class Loader或者Boot Class Loader加载的class对象,通过自定义类加载器加载的class不一定是GC Root
- 处于激活状态的线程
- 栈中的对象
- JNI栈中的对象
- JNI中的全局对象
- 正在被用于同步的各种锁对象
- JVM自身持有的对象,比如系统类加载器等。
吞吐量和暂停时间
吞吐量:应用程序线程用时占程序总用时的比例。GC线程会导致应用程序线程暂停,吞吐量越高,程序执行越快。
暂停时间:GC线程导致应用程序线程暂停的时长。过长的暂停时间会影响用户体验。
垃圾收集器
GC触发时机
你能不能谈谈,Java GC是在什么时候,对什么东西,做了什么事情?
java基础—常用的GC策略,什么时候会触发YGC,什么时候触发FGC?
minor GC: 新生代满了
full GC: 老年代满了;新生代晋升到老年代的平均大小超过剩余空间……
你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理 过程中有哪些收获?
常见的原因如下:
1)内存加载的数据量太大:一次性从数据库取太多数据;
2)集合类中有对对象的引用,使用后未清空,GC不能进行回收;
3)代码中存在循环产生过多的重复对象;
4)启动参数堆内存值小。
StackOverflow异常有没有遇到过?⼀般你猜测会在什么情况下被触发?如何指定⼀个线程的堆栈⼤⼩?⼀般你们写多少?
触发情况:
栈内存溢出,一般由栈内存的局部变量过爆了,导致内存溢出。出现在递归方法,参数个数过多,递归过深,递归没有出口。
栈大小设置:
JVM优化系列之一(-Xss调整Stack Space的大小)
-Xss:如-Xss128k
JDK5.0以后每个线程堆 栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
内存模型以及分区,需要详细到每个区放什么。
程序计数器
较小的内存空间,当前线程所执行的字节码的行号指示器。
线程私有。
Java虚拟机栈
线程私有。
描述java方法执行的内存模型。
每个方法执行时创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等。
局部变量表:基本数据类型+reference,long和double两个slot,其余1个。
局部变量表的大小在编译期确定。
异常情况:栈帧过大 StackOverflowError;虚拟机栈扩展无法申请足够内存 OutOfMemoryError。
本地方法栈
与虚拟机栈类似,不过是执行Native方法服务。
java堆
线程共享。
对象实例和数组在此分配。
垃圾收集器管理的主要区域。
可以划分线程私有的分配缓冲区(TLAB)。
方法区
线程共享。
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
运行时常量池
方法区的一部分。
存放编译期生成的各种字面量和符号引用。
直接内存
不属于Java虚拟机内存。
某些io操作直接操控native堆。
虚拟机在运行时有哪些优化策略
请解释StackOverflowError和OutOfMemeryError的区别?
根据《深入理解Java虚拟机》里的说法,StackOverflowError出现在虚拟机栈和本地方法栈中,对虚拟机栈来说,StackOverflowError在线程请求的栈深度大于虚拟机所允许的深度时抛出;OutOfMemoryError出现在除程序计数器之外的其他区域,基本都是因为无法满足内存分配且无法扩展而产生的。
在JVM中,如何判断一个对象是否死亡?
首先使用根搜索算法判定对象是否存活。
若根搜索算法不可达,还要经历两次标记:
- 判定对象是否有必要执行 finalize() 方法。若对象没有覆盖 finalize() 或已被虚拟机执行过则会被判定“不必要执行”。
- 若被判定要执行 finalize() 方法,对象会被移进 F-Queue等待方法执行,若在方法中对象被引用链上的任意对象引用,在第二次标记中会被移出“即将回收”集合。
对象被回收后,判定真正死亡。