JVM相关知识点
0、大纲介绍
介绍关于常见的JVM面试题目
Java源代码运行在JVM上面,为什么Java是跨平台语音,这个和JVM有很大的关系。
即Java的源代码被编译成字节码之后,运行在jvm这个平台之上,和所处的操作系统没有直接的关系。
所以才有一次编译,随意运行。Complie One, Run Away.
这边主要是介绍一下JVM的内存模型以及常见的垃圾回收算法和Java自带的垃圾回收器
还有实战之中较为常见的GC排查以及调参的一些知识点。
1、JVM内存模型
- JVM运行时,分为两个区域,一个是线程私有,一个是线程共享
- 线程私有分为三个部分组成,程序计数器、Java虚拟机栈、本地方法栈
- 程序计数器是唯一一个不会产生OOM的区域,通过改变计数器的值来选取下一条需要执行的字节码指令
- Java虚拟机栈则是为方法创建一个栈帧,用来存放局部变量表。
- 本地方法栈和Java虚拟机栈。虚拟机栈执行的是Java方法,本地方法栈执行的是native方法。
- Java虚拟机栈和本地方法栈都有可能产生OOM。
- 线程共享分为两个部分组成,方法区以及堆区。
- 堆区是JVM内存当中最大的一块区域,在虚拟机启动时候并创建,所有的对象实例都存放在堆区当中。
- 方法区里面存放的是一些常量、静态变量或者虚拟机加载的类信息,即编译器编译之后的代码等数据。
- 这两个区域也是会产生OOM问题。
2、怎么判定对象需要被回收
本质上,只要对象不再被引用,就可以认定需要被回收。
1、引用计数器
有被其他对象使用的时候则+1,当被赋予了新值或者超过生命周期,则-1.
无法避免循环引用的情况,这种对象则永远无法被回收
2、可达性分析
从GC ROOT开始查找,如果可以被遍历到,那么就是还存在使用,无法被遍历到,那么就需要被回收
3、垃圾回收算法
1、标记-复制算法
- 从GC ROOT开始扫描,标记处所有存活的对象,并将这些存活的对象复制到一块新的内存区域,然后将旧的内存回收掉。
- 这种回收算法适用于存活对象比较少的情况下比较高效,同时由于内存被进行拆分,所以可用的内存空间为原本的一半。同时需要复制移动对象。
- 适用于新生代,因为新生代里面的对象迭代比较频繁。
2、标记-清除算法
- 标记清楚算法总的分为两个步骤,一个是标记阶段,一个是清除阶段
- 从GC ROOT进行查找,将所有可达的对象进行打标。未被打标的对象就是要被回收的对象
- 保留被标记的对象,清除未被标记的对象。
- 这样子的问题就是容易产生碎片,比如ABC三个对象,B是被回收的,之后B区域如果不是太大,则无法分配新对象进行,就造成了浪费,同时也要进行两次空间扫描。
- 这个算法比较适合存活多的区域,适用于年老代。
3、标记-整理算法
- 标记整理算法也叫做标记压缩算法
- 从GC ROOT开始,对所有可达性对象进行一次标记
- 将被标记过的对象进行重整合并到一起,然后清空其他的不可达对象
- 这种方法避免了碎片的产生,同时也不需要向复制算法一样需要拆分内存区域。是标记清除算法的一种优化
4、分代算法
- 目前虚拟机使用回收算法就是分代算法。因为上述的三个算法各有自己的优缺点,所以需要根据不同的区域使用不同的垃圾回收算法。比如年轻代的对象活跃迭代快,适合标记复制算法。而年老代对象多存活周期较久,就适合标记清除或者标记整理算法
4、垃圾收集器
1、CMS收集器
- 初始标记,该阶段是单线程执行,标记可达的对象
- 并发标记,这时候GC线程和应用线程并发执行,通过初始标记进行查找这些对象可达的对象
- 重新标记,修正因为程序继续运行导致的部分对象的标记产生变动
- 并发清理,标记清除算法
2、G1收集器
- 前三个步骤和CMS是类似的
- CMS是并发清理,将不可达的对象都情况,这部分会有一定的耗时操作,G1的话在这部分可以根据用户设置的时间值,在这期间来指定回收计划。
5、频繁GC的原因以及审查
1、GC触发的条件
- GC的话,根据不同代分为Minor GC、Major GC 和 Full GC
- Minor GC,是由于新生代的Eden区满了,所以触发
- Major GC是年老代的空间满了才会触发的GC
- Full GC则是两个都满了之后才会触发的GC
2、如何减少GC的频率以及Full GC的次数
- 尽量不用分配过大的对象,使得对象无法被新生代存放而直接进入年老代,提前GC
- 尽量先让新生代写满,让对象在新生代多存活一段时间再进入年老代
- 不要手动调用System.gc(),要尽量让JVM自主调用GC
- 默认的JVM的新生代:年老代=1:2,同时新生代当中的Eden:SurTo:SurFrom=8:1:1
3、线上频繁GC排查
- 查看Full GC的时长,超过2S的话,则需要尽快排查了
- 将内存的使用情况进行dump下来,利用内置指令jmap即可。这个会导致Java进程中断
- 查看是哪个方法持有的内存最不合理
- 改动代码,然后本地测试看一下效果
6、类的加载过程
1、类加载器
根据指定的全限定名称将class文件加载到JVM当中,转为Class对象。
- 启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中,这部分是由C++实现。
- 其他的类加载器,由Java实现,继承抽象类ClassLoader
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
- 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
2、双亲委派机制
- 一个类加载器在收到类加载的请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个加载器都是如此,只有当父类加载器在自己的范围内找不到指定的类时,子类加载器才回去加载。
- 避免内存当中出现多个相同的字节码。
- 打破双亲委派机制,需要自己继承ClassLoader,同时重写loadClass和findClass方法即可。
7、引用的区别
1、强引用
通过New出来的对象,进行GC的时候,即可内存不够,也不回收。
2、弱引用
对象被重新赋值或者超过了生命周期,那么进行GC的时候,无论内存是否充足,这个对象都会被回收。
3、软引用
当内存足够时候,不会回收,内存不够的时候,则会回收。
4、虚引用
相当于没有引用,主要用来跟踪对象,在任何时候都可能被垃圾回收器回收