JVM面试题
- JVM由哪几个部分组成?各自有什么作用?
- 运行时数据区
- 1.说一下JVM的内存模型吧,有哪些区?分别干什么的?
- 2.为什么要用程序计数器存储字节码指令地址?
- 3.程序计数器为什么要设置为线程私有?
- 4.CPU是怎样切换线程的呢?
- 5.栈和堆的区别?
- 6.堆是分配对象存储的唯一选择吗?方法中定义的局部变量是线程安全的吗?
- 7.堆内存中,JVM给对象分配内存的流程
- 8.为什么要有新生代和老年代?
- 9.为什么要有Survivor区?
- 10.为什么还需要两个Survivor区?
- 11.什么是TLAB(Thread Local Allocation Buffer)?为什么有TLAB?
- 12.JVM内存的参数设置
- 13.jvm内存模型,java8做了什么修改?
- 14.为什么永久代会改为元空间?为什么jdk7之后把StringTable字符串常量池放到堆空间中?
- 15.方法区中有垃圾回收吗?
- 16.GC如何判断对象可以被回收
- 类加载系统
JVM由哪几个部分组成?各自有什么作用?
由类加载器,运行时数据区,执行引擎和本地方法接口组成
- 首先通过类加载器把java代码转换成字节码
- 运行时数据区把字节码加载到内存中
- 字节码文件只是JVM一套指令集规范并不能直接交给底层操作系统去执行,因此需要特定的命令解析器–执行引擎,执行引擎将字节码指令翻译成底层系统指令再交由CPU去执行
- 而这个过程中需要调用其它语言的本地库接口来实现整个程序的功能。
运行时数据区
1.说一下JVM的内存模型吧,有哪些区?分别干什么的?
- 程序计数器就是用于存储下一条指令的指令地址的
- 虚拟机栈主管java程序的运行,它的基本单位是栈帧。每个方法执行时都会创建一个栈帧来保存局部变量表,操作数栈,动态连接,方法返回地址。虚拟机栈是线程私有的区域
- 本地方法栈如果是管理本地方法的运行的
- 堆是线程共享的一块区域,主要是用来存储对象的,JVM启动的时候就会创建
- 方法区主要是用来存储已被虚拟机加载的类型信息,常量,静态变量。
2.为什么要用程序计数器存储字节码指令地址?
因为CPU需要不断的切换线程,这时候切换回来以后,就得知道接着从哪开始执行。所有需要用程序计数器记录下一条指令的指令地址
JVM的字节码解释器就是通过改变程序计数器的值来确定下一条应该执行什么样的字节码指令
3.程序计数器为什么要设置为线程私有?
因为CPU需要不断的切换线程,如果程序计数器设置为线程共享的,那么程序计数器记录当前线程的指令地址就会覆盖上一个线程的下一条指令的指令地址。如果再切换回上一个线程,因为指令地址被覆盖就不知道接着从哪开始执行了。所以程序计数器必须设置为线程私有,为每一个线程分配一个程序计数器,这样就不会相互干扰了。
4.CPU是怎样切换线程的呢?
- 使用CPU时间片进行切换的。
- 多线程从宏观上来看它是多个线程一起运行的。
- 但是微观角度,CPU同一时刻只能运行一个线程,需要不断的切换线程。为了保证每个线程公平处理,就是给每个线程分配一个时间片,时间到了就换另一个线程运行。
5.栈和堆的区别?
栈是运行时的单位,解决程序的运行问题。 堆是存储的单位,解决数据存储的问题。
栈是每个线程私有的,堆是线程共享的一块内存区域。
JVM对栈操作只有进栈和出栈操作,因此栈中是没有垃圾回收;而堆是垃圾回收的主要区域。
6.堆是分配对象存储的唯一选择吗?方法中定义的局部变量是线程安全的吗?
如果对象是方法内部生成,方法内部消亡的就认为没有发生逃逸。当一个对象在方法内部生成后,它被其它线程所调用,则认为发生了逃逸。
如果经过逃逸分析,一个对象如果没有逃逸出方法的话,那么就可能被优化成栈上分配
局部变量是不是线程安全要看具体情况具体分析,主要是看这个局部变量引用的对象有没有被其它线程调用。
局部变量引用的对象是在内部生成,内部消亡就是线程安全的
形参的局部变量线程不安全
作为返回值返回也是线程不安全
特殊情况是线程安全的
7.堆内存中,JVM给对象分配内存的流程
1)新对象申请,先判断伊甸园空间能否放得下,放得下就给对象分配内存
2)如果Eden空间放不下,就先进行minor gc,清除Eden空间中所有不使用的对象,正在使用的对象转到Survivor区,再判断新对象能否放得下
3)如果放得下就给对象分配内存,如果还放不下就判断新对象在老年区能否放得下
4)放得下就分配内存,如果还放不下就进行FullGC,将所有区中不使用的对象都清除,再判断对象是否放得下老年区
5)放得下就分配内存,如果还放不下就报OOM异常。
因为FULL GC的性能比YGC要慢10倍,所以调优的时候就要尽量减少FULL GC的次数。
补充:
- 如果对象在幸存者区存活的时间等于阈值,默认是15次垃圾回收,则会将幸存者区幸存下来的转去老年区。
- 如果ygc的时候,正在使用的对象转到幸存者区放不下了,那么会直接晋升到老年区。
- 幸存者区空间满了不会触发minor gc,只有伊甸园区空间不足才会触发minor gc,minor gc也会同时清理幸存者区不使用的对象。
- 幸存者区s0,s1,复制之后有交换,谁空谁是to,另一个为from
8.为什么要有新生代和老年代?
分代的唯一理由就是优化GC性能。如果没有分代,那所有对象都放在一块。GC的时候如果要找到哪些对象能被回收,就需要对堆中所有区域进行扫描。然而实际上很多对象都是"朝生夕死"的,如果我们把这些寿命短的对象放在一个区,gc的时候优先清理这块区域,就样就会很快腾出空间出来,提高效率。
9.为什么要有Survivor区?
如果没有Survivor区,每次Eden空间满了发生了minor gc的时候,存活的对象就会放到老年区,老年区满了就会触发Full GC,Full GC是非常耗时的,我们应该尽量减少Full GC的次数。如果把Eden空间存活的对象放到Survivor区,如果Survivor区对象反复清理几遍都没清理掉再放到老年区,这样老年区的压力就会小很多。Survivor相当于一个筛子,过滤掉生命周期短的,将生命周期长的放到老年代区,减少老年代发生full gc的次数。
10.为什么还需要两个Survivor区?
为了解决内存碎片化的。如果只有一个Survivor区,那么进行minorGC的时候 仍然存活的对象转移到Survivor区,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
如果是两个Survivor区,to区始终是空的,我们将eden和survivor的from区存活的对象一起复制到to区(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)这样to区内存空间就连续了,然后再将to区和 from区的身份互换。
上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。
11.什么是TLAB(Thread Local Allocation Buffer)?为什么有TLAB?
因为堆区是线程共享区域,如果多个线程先后把对象的引用指向同一个内存区域就会造成非线程安全问题,为了解决这个并发问题,对象的内存分配过程就必须进行同步控制,也必定会影响内存的分配效率。由于jvm创建实例是非常高频的。所以JVM在伊甸园为每个线程分配了一个私有缓存区域,如果需要分配内存,就优先自己的空间上分配,这样就不存在竞争情况,可以大大提高分配效率。
12.JVM内存的参数设置
参数 | 说明 |
---|---|
-Xss | 设置栈空间大小 |
-Xms | 初始堆空间大小 |
-Xmx | 最大堆空间大小 |
-Xmn | 设置新生代的大小 |
-XX:NewRatio=2 | 配置新生代和老年代的比例 |
-XX:SurvivorRatio=8 | 配置伊甸园区和幸运者区的比例 相当于8:1:1 |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
-XX:+PrintGC | 打印gc简要信息 |
-XX:MaxTenuringThreshold | 设置新生代垃圾的最大年龄 |
-XX:HandlePromotionFailure | 是否设置空间分配担保 (JDK6之后代码中不会再使用此参数,在minor gc之前规则变为只要老年代的连续空间大于新生代对象总大小或者历代晋升到老年代对象的平均大小就会进行Minor,否则将进行Full GC) |
-XX:+PrintFlagsInitial | 查看所有参数默认初始值 |
-XX:+PrintFlagsFinal | 查看所有参数的最终值 |
-XX:MetaspaceSize | 元空间初始可分配空间(默认约为21M) |
-XX:MaxMetaspaceSize | 元空间的最大可分配空间(默认-1没有限制) |
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8
13.jvm内存模型,java8做了什么修改?
这里指的是静态变量而不是静态变量的引用,静态变量引用的对象实体始终都是存在堆空间。
jdk7及其以后版本,HotSpot虚拟机选择把静态变量与引用的对象存放在一起,存储于java堆之中。
14.为什么永久代会改为元空间?为什么jdk7之后把StringTable字符串常量池放到堆空间中?
永久代设置空间大小是很难确定的,如果动态加载的类过多,容易产生Perm区的OOM。而元空间跟永久代最大的区别是,元空间并不在虚拟机设置的内存中,而是使用本地内存。因为默认情况下,元空间大小仅受本地内存限制。
因为方法区回收效率很低,只有当方法区空间不足才会触发full gc,这就导致字符串常量池回收效率不高,而我们开发中会有大量的字符串被创建,放到堆空间能及时回收内存。
15.方法区中有垃圾回收吗?
方法区中的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。但是这个区域回收效果不太理想,尤其是类型信息的回收,条件非常苛刻。
16.GC如何判断对象可以被回收
引用计数法(java中的GC不是采用此方法,但是其它语言的GC会采用此方法):每一个对象有一个引用计数属性,新增一个引用计数加1,引用释放时计数减1,计数为0时可以回收
优点:引用计数法实现简单,判定效率也很高
缺点:主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。(可能会出现A引用B,B又引用了A,这时候就算他们都不再使用了,但因为相互引用 计数器=1 永远无法被回收)
解释:由于A引用了B,如果B想要回收,必须先让A释放引用B,A如果要想释放引用B就要被回收,可是B又引用了A,A无法被回收。
可达性分析法:从GC Roots开始向下搜索,搜索走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
GC Roots的对象有
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法中JNI(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。
当对象变成不可达时,GC会判断该对象是否覆盖了finalize()
方法,如果没有覆盖则直接将其回收。如果覆盖了就执行finalize()
方法完毕后,GC会再次判断对象是否可达,若不可达则进行回收,否则对象"复活"
类加载系统
1.java类加载器有哪些
jdk自带的有三个类加载器:BootStrapClassLoader、ExtClassLoader、AppClassLoader,除此之外还有很多用户自定义的加载器
BootStrapClassLoader引导类加载器是ExtClassLoader的父类加载器,负责加载所有JAVA的核心类库(rt.jar、resource.jar、sun.boot.class.path)
ExtClassLoaderr扩展类加载器是AppClassLoader的父类加载器,负责加载jre/lib/ext文件夹下的jar包和class类
AppClassLoader系统加载器是自定义类加载器的父类,负责加载classpath下的类文件
2.双亲委派模型
java虚拟机对Class文件采用的是按需加载,而且加载class文件时,java虚拟机使用的是双亲委派模式
流程:
(1)当一个类加载器收到类加载请求时,会先检查自己的缓存是否加载了该类,
(2)如果没有,就向它的父类加载器的缓存中查找,如果有直接返回,如果没有,则继续向上委托,依次递归,直到委派到顶层的引导类加载器为止
(3)如果顶层加载器缓存中没有,就到加载路径中查找,找到就加载返回
(4)没有就交给子加载器,从子加载器加载路径查找。直到向下查找到发起加载的加载器为止,如果找到就进行加载返回,否则抛出ClassNotFindException
总结:
向上委派到顶层加载器为止,查找缓存,是否加载了该类;
向下查找到发起加载的加载器为止,查找加载路径有没有该类。
好处:
(1)避免了类的重复加载(向上委派查找缓存,如果缓存中存在,说明该类已经被加载直接返回,避免了类的重复加载)。
(2)保护程序安全,防止核心API被随意篡改。
3.jvm如何认定两个对象属于同一个类型
(1)类的完全限定名必须一致
(2)加载这个类的ClassLoader必须相同
4.获取ClassLoader的途径
public class Demo {
public static void main(String[] args) {
//1.获取当前类的ClassLoader
ClassLoader classLoader = Demo.class.getClassLoader();
//2.从当前线程上下文中获取ClassLoader
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//3.获取系统的ClassLoader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(classLoader);
System.out.println(contextClassLoader);
System.out.println(systemClassLoader);
}
}
结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2