JVM面试八股文,整理了出来。排版不太好!
目录
JVM入门部分
为什么要学习JVM?
你了解哪些JVM产品?
JVM的构成有哪几部分?
JVM类加载部分
你知道哪些类加载器?
为什么需要多个类加载器?
什么是双亲委派类加载模型?
双亲委派方式加载类有什么优势、劣势?
描述一下类加载时候的基本步骤是怎样的?
什么情况下会触发类的加载?
类加载时静态代码块一定会执行吗?
如何理解类的主动加载和被动加载?
为什么要自己定义类加载器,如何定义?
内存中一个类的字节码对象可以有多个吗?
JVM运行内存部分
JVM运行内存是如何划分的?
JVM中的程序计数器用于做什么?
JVM虚拟机栈的结构是怎样的?
JVM虚拟机栈中局部变量表的作用是什么?
JVM虚拟机栈中操作数栈的做用是什么?
JVM堆的构成是怎样的?
Java对象分配内存的过程是怎样的?
JVM年轻代幸存区设置的比较小会有什么问题?
JVM年轻代伊甸园区设置的比例比较小会有什么问题?
JVM堆内存为什么要分成年轻代和老年代?
项目中最大堆和初始堆的大小为什么推荐设置为一样的?
什么情况下对象会存储到老年代?
Java中所有的对象创建都是在堆上分配内存的?
如何理解JVM方法区以及它的构成是怎样的?
JDK8中Hotsport虚拟机的方法区内存在哪里?
什么是逃逸分析以及可以解决什么问题?
主观上如何判断一个对象是否发生了逃逸?
如何理解对象的标量替换,为什么要进行标量替换?
什么是内存溢出以及导致内存溢出的原因?
什么是内存泄漏以及导致内存泄漏的原因?
JAVA中的四大引用类型有什么特点?
JVM执行引擎部分
执行引擎的作用是什么?
JIT是什么,要做什么?
何为GC,为什么要GC?
如何判定对象是否为垃圾?
说说垃圾对象的回收策略?
你知道哪些GC算法?
JVM中有哪些垃圾回收器?
JAVA中的堆区为什么要分代?
服务频繁fullgc,younggc次数较少,可能原因?
JVM是java虚拟机,java程序可以跨平台运行 都要归功于jvm。学习jvm可以使我们更好的理解java,以便于我们进行一些性能调优。
深入学习jvm后,我们可以通过jvm 进行一些线上问题,比如 java程序热部署。
1、oracle 公司的 hotspot ,在开发过程中应用的是比较多的,比较大的特点是 GC机制,分为频繁GC 和 FULL、 GC(大GC)。我们知道 GC 线程执行时 程序是会暂停的,所以如果想优化程序 可以从 减少FULL GC次数方面下手。(注:hotspot是sun公司的产品 现已被oracle收购)
2、oracle 公司的 JRockit, 可以看做是兼容标准的JDK基础上的JVM,同原有的JVM相比,JRockit 声称在速度上有显著提升(甚至超过70%)和硬件成本的减少(超50%).
JRockit 对线程 和 网络方面 都做了大量的优化和技巧的工作。
JRockit 的GC机制 和 hotspot 的GC机制有很大的不同,其中之一就是没有频繁GC和FULL GC的概念,而且每次GC的操作时间较长。
3、IBM 公司的J9 , 这三个jvm产品是使用者最多的 jvm产品。市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM。
IBM J9VM 虚拟机的职责分离 与 模块化 做得比HotSpot 更优秀。
2017年左右IBM 将J9VM进行完全开源,并命名为OpenJ9 VM ,后捐给了Eclipse 并重新命名为 Eclipse OpenJ9。如果为了学习虚拟机技术 而去阅读源码,更加模块化的 OpenJ9 是比HotSpot 更好的选择。如果是为了使用java虚拟机时多一种选择,那可以通过AdoptOpenJDK来获得采用OpenJ9 搭配上 OpenJDK 其他的类库组成完整的JDK来使用。
4、阿里巴巴公司的 TaoBaoVM ,由AliJVM团队发布,国内使用Java最强大的公司。基于OpenJDK开发了自己定制的的 AlibabaJDK,简称AJDK。是整个阿里java体系的基石。
TaoBao JVM 基于OpenJDK HotSpot JVM 开发,发布的国内第一个优化、深度定制确开源的高性能服务器版Java虚拟机。
TaoBao VM 有什么优化呢? 创新的GCIH 技术 实现了off-heap 即 将生命周期较长的java对象 从heap中 移到heap之外,并且GC 不能管理GCIH内部的java对象,以此达到降低GC的回收频率 和 提升GC回收效率的目的。
Taobaovm 硬件严重依赖intel的cpu ,损失了兼容性,但提高了性能。
总结:对于一般的应用而言,oracle的 hotspot 就足够使用了。 对于对性能要求很高的应用而言,建议使用oracle (BEA)的 JRockit vm,如java版的游戏服务器。对于大规模的公司而言,建议实用IBM的 OpenJ9 vm ,它是一整套的解决方案,后期维护方便。
--多样化进行类加载吧 灵活指定加载源 、 自定义加载模式
类的加载机制是 双亲委派加载模型。
双亲委派类加载模型可以理解为 从AppClassLoader 向上询问 是否已经加载过这个类 如果已经加载过、则不在进行加载。
假如没有加载过 则从 BootStrapClassLoader开始进行向下依次尝试 加载
假如可以 则进行加载
优势:可以保证同一个类 在内存中 不会被多次加载。
劣势:对效率有影响、当有同包名 同名称的类时 会出现只加载一个类的情况。
不一定,静态代码块是否执行取决于是否执行了类的初始化操作
Ex: ClassLoader.getSystemClassLoader()//获取类加载器
.loadClass(“com.java.jvm.loader”)//loader类中的静态代码块不会被执行
主动加载 通过访问本类 属性 或 方法 进行的加载 会初始化
被动加载 通过本类访问对应的父类 属性 方法时,本类属于被动加载。 不会初始化
当系统提供的类加载器不满足我们的使用要求时,我们就要自己定义类加载器。
比如想打破双亲委派模型的加载机制时,就要重新定义
如何定义?
直接或间接继承ClassLoader
重写相关方法:findClass() 有 查找 校验 初始化 创建字节码对象的操作
loadClass() 一般不需要重写、如果想打破双亲委派加载模型则重写
可以。
即使是同一个类,当他的类加载器不同时 生成的Class对象也不同。虽然有双亲委派模型 但是不同的加载器调用 也会有不同的Class对象
线程共享区:
堆:
作用:存储对象
落地:
年轻代:
伊甸园区:新建的对象一般存储在这里
幸存区:有两个, 没有GC前都是空的 GC后有一块是空的 存活下来的对象存储在幸存区。
老年代:
大GC后存储到老年代、(多次GC没有回收掉的对象)
如果新建的对象内存占用过大 也会存储到老年代
问题:
堆内存溢出 – 对象太多 对象太大
堆内存泄露 – 解决 能用静态内部类不用实例内部类,实例内部类会默认保存外部类引用、减少static变量应用
方法区:
作用:存储类的字节码信息、存储常量池信息
落地:JDK1.8Metaspace – 堆外内存
问题:类太多了导致内存溢出
线程私有区:
栈:
作用:存储局部变量、方法参数、执行计算、存储方法返回值
落地:
Java方法栈:
局部变量表 – 存储方法内部定义的局部变量
操作数栈 - 方法内部进行的计算操作
动态连接
方法返回值
其它
本地方法栈
问题:栈内存溢出 – 一般是出现了无线递归
寄存器:
作用:记录线程要执行的下一条指令地址值
落地:JAVA寄存器、线程私有
问题:关于内存溢出 寄存器不会出现
用来记录当前线程要执行的下一条指令的偏移量地址,每一个线程都有一个程序计数器
操作数栈 -(用于执行计算)
局部变量表 -(存储方法内部的局部变量)
方法返回值 -(存储方法返回值)
动态连接 -(方法中要访问的一些常量池数据,要调用的方法都
会对应出一个连接)
局部变量表 – 存储方法内部的局部变量
操作数栈 – 进行执行计算
年轻代:伊甸园区、幸存区(2个)
老年代:
伊甸园区对象越来越多的时候,会频繁的yongGC,当幸存区内存太小了,则进行回收到老年代。老年代满了则执行FullGC
影响程序的执行效率
还是会影响系统执行效率。会增加gc的次数。当GC执行的时候用户线程会短暂的暂停。
通过分代的设计 减少GC的次数,提高系统的性能。
Jvm调优中,为了避免程序在GC前后 带来的内存变化,所以推荐设置成一样的。如果在GC后 扩充了初始堆的内存,这样带来的系统开销可能是很大的,所以推荐开始时 即设置为最大值(阿里开发手册中也是推荐设置为一样)
1、如果初始化时 占用内存过大,年轻代没有足够的空间储存 ,则会直接储存到老年代中。
2、年轻代反复GC后 幸存区存储不下,则会储存到老年代
3、年轻代反复GC后,依然没有被GC掉(默认是15次后)。
JDK1.8 后 jvm出现了逃逸算法,逃逸了的对象 ,会在堆中分配,如果为逃逸的对象,会直接存储在栈上,当然 只能是很小的对象,如果占用内存超过栈内存 则未逃逸对象还是会储存在堆中
不同的版本称呼方法区的名称也不同,jdk1.7中称之为 持久代、jdk1.8中称之为 元空间metaspace.这个元空间 MetaSpace 可以是不占用堆中的内存的。
Jvm中方法区的作用有 存储类的字节码信息 、 存储常量池信息
Jdk1.7中HotSpot的方法区被称之为永久代
Jdk1.8中hotspot删除了永久代的概念,改为 元空间MetaSpace
元空间不受jvm内存的影响 受系统内存的影响,存储在系统内存中。
逃逸分析本质上是一种数据分析算法,基于这个算法判断对象是否发生了逃逸。未逃逸的对象可以直接分配在栈上 如果内存足够的话。
这样可以减少在堆上的内存分配,从而减少了GC的次数。使得程序更优化 提高执行效率。
如果分配在堆上则发生了逃逸。如果直接分配在栈 上 则是未发生逃逸。
标量替换是一种将对象打散分配在栈上的技术(将对象中成员以局部变量方式进行设计)
减少对象在堆中创建次数,减低GC 频率 从而提高系统执行效率。
内存中剩余空间 不足以给新对象分配空间了,继续分配的话 就会出现内存溢出。
出现内存溢出的原因可能有:
1、创建的对象太大,直接超过了伊甸园区 老年代 中的内存。
2、创建的对象太多,又有大量的内存泄露。
3、方法区的类太多,没有足够的空间存储新的类型。
4、方法出现了无线递归调用,可能会导致栈内存溢出
内存泄露:对象使用完后 本应该释放掉的内存,并没有被释放掉,即不用了的对象 还存在着引用。
常见的存储泄露有:
Java中为了更好地控制对象的生命周期,提高对象对内存的敏感度,设计了四种引用类型,按其在内存中的生命力强弱,可以分为 强引用 弱引用 软引用 虚引用。
其中 强引用 的生命力最强。其他引用 引用的生命力依次递减。JVM的GC系统被触发时,回应为对象的引用不同,执行不同的回收逻辑
JVM主要的任务 是装载字节码文件,但是字节码文件并不能直接被系统运行,需要被JVM加载 解释一下 才可以运行,那这个时候就轮到 执行引擎登场了。想要让JAVA程序运行起来,就必须需要jvm的执行引擎将class对象中的字节码 解释成 机器底层语言、即为将字节码解释/编译 成对应的计算机平台能读懂的指令。
执行引擎的执行过程如下:
执行引擎执行的过程中依赖于程序计数器中的地址 找到所对应的栈 地址。
在方法的执行过程中,通过栈中的局部变量表中储存的信息找到堆中的实例。
谈论JIT之前,我们先提起解释器的一个概念。什么是解释器呢,java设计的初衷是跨平台执行 即一次编写 多次运行,想要实现跨平台性,就运用到了解释器。可以把解释器理解成“翻译者”即多个平台都可以使用。那么使用到解释器会有什么影响呢?会影响到系统的执行效率。
解释器需要在执行代码时 进行纯软件代码 模拟字节码执行,效率十分低。现在解释器的执行已经沦为低效的代名词。针对该问题,jvm平台支持一种称为即时编译的技术。它的目的是避免函数被解释执行,而是将整个函数体 编译为机器码。每次执行时只执行编译后的机器码,这样实现了提高程序的执行效率。
JIT是什么呢? JIT是编译器 可以实现字节码的实时编译。这种即时编译的技术,可以提高程序的执行效率。
那么为什么不把解释器 抛弃了 直接使用JIT编译器呢?
JVM执行代码主要有两种方式:
解释执行:一段代码,解释一行 执行一行。即解释器逐行的将字节码解释成机器语言 并执行
编译执行:事先已经被编译成机器码,直接执行,不需要解释。即jvm读取到字节码文件后 JIT直接把字节码文件编译成机器语言 不需要解释直接由CPU执行。
虽然JIT编译器能提高程序的执行效率,但是在执行机器指令之前,JIT还是要欲将字节码文件转成机器语言,因此造成的响应时间较长,而解释器可以直接 对字节码进行解释执行。
因此 HotSpotVM 采取解释器 、JIT共存的方式,在JVM执行过程中。二者相互协作,各自取长补短,尽力的去权衡编译本地代码所需的时长 和 直接解释代码所需的时长,保证执行的效率
GC称之为垃圾回收。在JVM的执行引擎中 自带一个GC系统,此系统会按照一定的算法对内存进行监控 和 垃圾回收
如果程序运行中 一直不进行GC 则会出现大量的堆内存泄露,即用完的对象 指向没有消除 其会一直占用着堆空间。
从两个角度进行分析
算法:
标记清除法:先扫描内存中活着的对象并标记,再扫描并清楚内存中未标记的对象。
标记复制法:先准备两块内存 一块储存对象 一块空闲。GC执行时将活着的对象复制到空闲区,之后把原来的区删除
标记整理法:扫描活着的对象 并往一侧移动,之后将剩余没有被移动的空间清除。
线程策略:
单线程:回收垃圾的线程只有一个
多线程:并行(多核cpu同时执行GC线程) 或 并发(执行GC的用户线程并发执行)
GC算法:
标记清除法:先扫描内存中活着的对象并标记,再扫描并清楚内存中未标记的对象。
标记复制法:先准备两块内存 一块储存对象 一块空闲。GC执行时将活着的对象复制到空闲区,之后把原来的区删除
标记整理法:扫描活着的对象 并往一侧移动,之后将剩余没有被移动的空间清除。
串行垃圾回收器
并行垃圾回收器
并发垃圾回收器
G1垃圾回收器
堆区分为年轻代 老年代。年轻代又按照8:1:1 的比例分为 伊甸园区 幸存区1 幸存区2 。 这样的设置最终的目的是为了减少GC的次数,从而提高系统执行的效率。