1. 入门部分
1.1 为什么要学习JVM?
- 有效防止内存泄露
- 优化线程锁的使用
- 科学的进行垃圾回收
- 提高系统的吞吐量
- JVM调优 提高性能
1.2 你了解哪些JVM产品?
- HotSpot VM 目前市面上应用最官方,最主要的一款JVM,sun公司研发Oracle公司收购
- JRockit VM 由BEA公司研发,也是被Oracle公司收购
- J9 VM IBM内部使用
- TaobaoJVM 阿里JVM团队开发,基于OpenJDK开发了AlibabaJDK
1.3 JVM的构成有哪几部分?
- 类加载器 负责加载类到内存中
- 运行时数据区 储存数据 对象,方法等等
- 执行引擎 负责解释执行字节码,执行GC操作等
- 本地库接口 融合其他语言为Java所用
2 类加载部分
2.1 你知道哪些类加载器?
-
启动类加载器:BootstrapClassLoader
这个类加载器负责放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
-
扩展类加载器:ExtensionClassLoader
负责\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。
-
应用程序类加载器:ApplicationClassLoader
负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个
-
自定义类加载器:UserClassLoader
自己定义的类加载
2.2 什么是双亲委派加载模型?
-
一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,
-
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,
-
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,
这就是双亲委派模式
2.3 双亲委派方式加载类有什么优势、劣势?
优势:
1、避免类重复加载,确保一个类的全局唯一性
2、保护程序安全,防止核心API被随意篡改
劣势:
父级加载器无法加载子级类加载器路径中的类
2.4描述一些类加载时候的基本步骤是怎样的?
-
通过一个类的全限定名(类全名)来获取其定义的二进制字节流。
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
-
在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
-
加载过程大致可以分为加载,验证,准备,解析,初始化
2.5什么情况下会触发类的加载?
-
调用静态成员时
-
创建子类实例时,父类会被先加载
-
构建类的实例对象
-
直接调用ClassLoader.loadClass,或Class.forName
2.6类加载时静态代码块一定会执行吗?
不一定,通过loadClass方法加载类,静态代码块不会执行,class.forName设置不执行初始化动作,静态代码块也不会执行,因为静态块是在初始化阶段执行
2.7如何理解类的主动加载和被动加载?
1.主动使用:显式加载,会执行加载、连接、初始化静态域
2.被动使用:隐式加载,只执行加载、连接,不初始化类静态域
2.8 为什么要自定义类加载器,如何定义?
实际开发中需要实现某种特定的业务需求时,我们可能需要自定义类加载器:
- 隔离类的加载,不同框架下有全限定名相同的类
- 扩展加载源,加载整个工程以外的类
- 打破双亲委派机制
定义方法:- 继承URLClassLoader
- 重写findClass方法
3.字节码增强部分
3.1 为何要学习字节码?
- 可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效
- 字节码增强技术在Spring AOP、各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。
3.2如何解读字节码内容?
-
直接解读:
可以通过notepad++(需要安装一下HEX-Editor插件)打开IntTests.class文件,文件内容默认是一种16进制的格式
-
对类进行反编译
javap -verbose IntTests.class //可以使用javap –help查看帮助
-
jclasslib插件应用
代码在编译后,我们可以在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。
3.3 字节码内容由哪几部分构成?
major_version(主版本号)
magic: 魔数
minor_version: 次版本号
major_version: 主版本号
constant_pool_count: 常量池计数器
constant_pool[constant_pool_count-1]: 常量池
access_flags: 类的访问标志
this_class: 当前类名索引值
super_class: 父类名索引值
interface[interfaces_count]: 接口数组
fields_count: 成员变量计数
fields[fields_count]: 成员变量数组
method_count: 方法计数
methods[methods_count]: 方法数组
attribute_count: 属性计数
attribute[attributes_count]: 属性数组
3.4 什么是字节码增强?
字节码增强技术相当于是一把打开运行时JVM的钥匙, 利用它可以对现有字节码进行修改或者动态生成新的字节码, 进而对运行中的程序做修改, 实现热部署. 也可以跟踪JVM运行中程序的状态, 进行性能诊断等.
此外, 我们平时使用的动态代理, AOP也与字节码增强密切相关, 它们实质上还是利用各种手段生成符合规范的字节码文件.
3.5 为什么要进行字节码增强?
掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。
3.6 你了解哪些字节码增强技术?
- ASM技术
- Javassist技术
- Java Agent技术
3.7什么是热替换以及如何实现?
在一个持续运行,并已经加载了所有类的JVM中,还能利用字节码增强技术,对其中的类行为做替换并重新加载,这个过程我们有时通常称之为热替换。
实现:
- 创建Transformer对象, 用于对目标对象进行功能增强.
- 创建Agent对象, 用于调用Transformer对象执行字节码增强.
- 通过VirtualMachine的attach api加载Java Agent, 这组api其实是JVM进程之间沟通的桥梁, 底层通过socket进行通信.
- 创建Agent启动类,传入目标JVM pid.
- 分别启动目标类和Agent启动类即可.
4 JVM运行内存部分
4.1 JVM运行内存是如何划分的?
分为堆内存,栈内存,程序计数器,方法区
4.2 JVM中的程序计数器用于做什么?
程序计数器(Program Counter Register)也称之为PC寄存器,是一块较小的内存空间,用来存储指向下一条指令的地址,也可以看作是当前线程执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条 要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
4.3 JVM虚拟机栈的结构是怎样的?
Java虚拟机栈是线程私有的,每个方法在被线程调用时都会创建一个栈帧(Stack Frame),每个栈帧中储存着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 附加信息:
每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈的生命周期和线程一致,线程结束,该虚拟机栈也销毁了。
4.4 JVM虚拟机栈中局部变量表的作用是什么?
局部变量表也称之为局部变量数组或本地变量表
作用:用于存放方法参数和方法内部定义的局部变量信息。
在Java程序被编译为Class文件时,就已经确定了每个方法所需局部变量表的大小。
4.5 JVM虚拟机栈中操作数栈的做用时什么?
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和出栈(pop)
4.6 JVM堆的构成是怎样的?
堆内存
- 年轻代
1.伊甸园区(Eden)
2.幸存区(Survivor) - 老年代
4.7 Java对象分配内存的过程是怎样的?
- 编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配。 如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。
- 如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)。
- 如果Eden区无法存储对象,则执行Yong GC(Minor Collection)。
- 如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代。
- 新生代由Eden 区和两个幸存区构成(假定为s1,s2), 任意时刻至少有一个幸存区是空的(empty),用于存放下次GC时未被收集的对象。
- GC触发时Eden区所有”可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代。
- GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2,同时清空eden区和s1幸存区。
- GC再次触发时Eden区和s2幸存区中的”可达对象”会被复制到另一个幸存区s1,同时清空eden区和s2幸存区.依次类推。
- 当多次GC过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参数 -XX:+MaxTenuringThreshold 来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代。
4.8 JVM年轻代幸存区设置的比较小会有什么问题?
伊甸园区被回收时,对象要拷贝到幸存区,假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率。而分代回收的思想就会被弱化。
4.9 JVM年轻代伊甸园区设置的比例比较小会有什么问题?
伊甸园设置的比较小,会增加GC的频率,可能会导致STW(Stop-The-World)的时间边长,影响系统性能。
4.10 JVM堆内存为什么要分成年轻代和老年代?
为了更好的实现垃圾回收
4.11 如何理解JVM方法区以及它的构成是怎样的?
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。
1. 类信息包括对每个加载的类型(类class、接口interface、枚举enum、注解annotation)以及属性和方法信息。
2. 常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
4.12 什么是逃逸分析以及可以解决什么问题?
逃逸分析一种数据分析算法,基于此算法可以有效减少Java对象在堆内存中的分配。Hotspot虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象分配到堆上。例如:
1. 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
2. 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
4.13 何为内存溢出以及导致内存溢出的原因?
内存中剩余的内存不足以分配给新的内存请求就会内存溢出。内存溢出可能直接导致系统崩溃。
内存泄漏是导致内存溢出的一种原因,但内存溢出不全是由内存泄漏引起的,还可能是:
1. 创建的对象太大导致堆内存溢出
2. 创建的对象太多导致堆内存溢出
3. 方法出现了无限递归调用导致栈内存溢出
4. 方法区内存空间不足导致内存溢出。
4.14 何为内存泄漏以及内存泄漏的原因是什么?
动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束。这个现象称之为内存泄漏。因此良好的代码规范,可以有效地避免这些错误。
导致内存泄露的原因有:
1. 大量使用静态变量(静态变量与程序生命周期一样)
2. IO/连接资源用完没关闭(记得执行close操作)
3. 内部类的使用方式存在问题(实力内部类或默认引用外部类对象)
4. 缓存(Cache)应用不当(尽量不要使用强引用)
5. ThreadLocal应用不当(用完记得执行remove操作)
4.15 JAVA中的四大引用你知道多少?
Java中的四大引用分为强、软、弱、虚:
强引用:使用强引用的对象永远不会被GC回收,即便是发生内存溢出,平时代码中用的最多的就是强引用。
软引用:软引用有对应的实体列为SoftReference,使用软引用引用的对象只有在程序发生内存溢出异常前才会回收,也就是说如果内存充足永远不会被回收,只有在内存不足时才会回收,很好的避免内存溢出,非常适合做缓存。
弱引用:弱引用与对象的生命周期有关,在进行垃圾回收时,如果所引用的对象为null,则不论内存是否充足都会被回收,否则不会被回收,当然内存不足时会被直接回收。
虚引用对应的实体类为PhantonReference。虚引用不论所引用的对象是不是null,不论内存空间是否充足,都会被垃圾回收器回收
5、JVM垃圾回收部分
5.1 何为GC以及为和要GC?
定义: GC(Garbage Collection)称之为垃圾回收,是对内存中的垃圾对象,采用一定的算法进行内存回收的一个动作。比方说,java中的垃圾回收会对内存中的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是垃圾对象,然后基于特定算法进行回收。
作用: GC是为了释放空间,防止内存溢出,深入理解GC机制,写出更好的Java应用,提高开发效率,同时也是进军大规模应用开发的一个前提。
5.2 你知道哪些GC算法?
- 标记清除:标记清除(Mark-Sweep)算法分为“标记”和“清除”阶段,它首先会标记出内存中所有不需要回收的对象,然后从内存中清除所有未标记的对象。标记清除算法的的优点是简单直接,缺点是效率低,并且可能会产生大量不连续的碎片。说它效率低是因为标记和清除两个过程都需要扫描内存空间(第一次:标记存活对象,第二次:清除没有标记的对象)。还有就是,清除后产生的大量不连续的内存碎片空间,无法满足较大对象的存储需求,这样就可能会再次触发垃圾回收。所以此垃圾回收算法,应该适合对象存活率较高的的内存区域(比方说JVM中的老年代)。
- 标记复制:标记复制(Mark-Copy)算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。“标记-复制”算法的缺点显而易见,就是内存空间利用率低,适用于那些对象生命周期短、回收频率高的内存区域(比方说JVM中的年轻代)。
- 标记整理:标记整理清除(Mark-Sweep-Compact)算法结合了“标记-清除”和“复制”两个算法的优点。第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象“压缩”复制到堆的其中一块空间中,按顺序排放。第三阶段清理掉存活边界以外的全部内存空间。标记整理算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题,由于需要向一侧移动等一系列操作,其效率相对低一些,但对内存空间管理上十分优异。适用于那些生命周期长、回收频率低,但注重回收一次内存空间得到足够释放的场景。
5.3 JVM中有哪些垃圾回收器?
1. 串行垃圾回收器
2. 并行垃圾回收器
3. CMS收集器
4. G1垃圾收集器
5.4服务频繁fullgc,younggc次数较少,可能原因?
1. 系统承载高并发请求,或者处理数据量过大,导致YoungGC很贫乏,每次YoungGC过后存活的对象太多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代
2. 系统一次性加载过多数据进内存,有很多大的对象直接进入老年代
3. 内存泄露,大量对象始终无法被祸首,一直占用老年代