- 入门部分
- 为什么要学习JVM?
答:
第一: 深入理解JVM可以帮助我们从平台角度提高解决问题的能力,例如:
- 有效防止内存泄漏(Memory Leak).
- 优化线程锁的使用(Thread Lock).
- 科学进行垃圾回收(Garbage Collection).
- 提高系统吞吐量(Throughput).
- 降低延迟(Delay),提高其性能(Performance).
第二: 企业面试需要(中高级程序员必备技能).
-
- 你了解哪些JVM产品?
答:
- HotSpot VM(Sun公司研发, 2010年由Oracle公司收购).
- JRockit VM(BEA公司研发, 2008年由Oracle公司收购).
- J9 VM(IBM内部使用).
- TaobaoJVM(AliJVM团队开发, 基于OpenJDK开发了AlibabaJDK5).
说明: HotSpot目前是应用最官方, 最主要的一款JVM虚拟机.
-
- JVM的构成有哪几部分?
答:
- 类加载系统(ClassLoader System): 负责加载类到内存.
- 运行时数据区(Runtime Data Area): 负责存储数据信息(对象,方法等).
- 执行引擎(Execution Engine): 负责解释执行字节码,执行GC操作等.
- 本地接口(Native Interface): 负责融合不同的编程语言为Java所用.
- 类加载部分
-
- 你知道哪些类加载器?
答:
- AppClassLoader(系统类加载器): 对于用户自定义类来说, 默认使用系统类加载器.
- ExtClassLoader(扩展类加载器): 该类加载器负责加载Java的扩展库JAVA_HOME/jre/lib/ext/*.jar或者java.ext.dirs路径下的内容.
- BootstrapClassLoader(引导类加载器): Java的核心类库都是使用引导类加载器进行加载的.
-
- 什么是双亲委派类加载模型?
答:
Java虚拟机对class文件采用的是按需加载的方式, 也就是说需要使用该类时才会将它的class文件加载到内存生成class对象. 而且加载某个类的class文件时, Java虚拟机采用双亲委派模式, 即把请求交给父类处理, 它是一种任务委派模式.
具体过程如下:
- 如果一个类加载器收到了类加载请求, 它并不会自己先去加载, 而是把这个请求委托给父类的加载器去执行.
- 如果父类加载器还存在其父类加载器, 则进一步向上委托, 依次递归, 请求最终将到达顶层的启动类加载器.
- 如果父类加载器可以完成类加载任务, 就成功返回, 倘若父类加载器无法完成此加载任务, 子加载器才会尝试自己去加载, 这就是双亲委派模式.
- 父类加载器一层一层往下分配任务, 如果子类加载器能加载, 则加载此类, 如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常.
-
- 双亲委派方式加载类有什么优势、劣势?
答:
优势: 基于双亲委派模式机制实现了类加载时的优先级层次关系, 同时可以保证同一个类只被一个加载器加载(例如Object类只会被BootstrapClassLoader加载), 这样更利于java程序的稳定运行.
劣势:
- 无法扩展加载源.
- 无法实现对字节码文件进行加密后再通过类加载器对其进行解密.
- 无法实现隔离类的加载.
-
- 描述一些类加载时候的基本步骤是怎样的?
答:
- 通过一个类的全限定名(类全名)来获取其定义的二进制字节流.
- 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构.
- 在Java堆中生成一个代表这个类的java.lang.Class对象, 作为对方法区中这些数据的访问入口.
- 加载过程大致可以分为加载, 验证, 准备, 解析, 初始化几大阶段, 顺序: 加载, 验证, 准备和初始化发生的顺序是确定的, 而解析阶段则不一定, 且加载, 验证, 准备和初始化这四个阶段按顺序开始不一定按顺序完成.
- 另外, 一个已经加载的类被卸载的几率很小, 至少被卸载的时间是不确定的, 假如需要卸载的话可以尝试System.exit(0);
-
- 什么情况下会触发类的加载?
答:
Java中的类加载方式主要两种: 隐式加载和显示加载
隐式加载:
- 访问类的静态成员(例如类变量, 静态方法)
- 构建类的实例对象(例如使用new关键字构建对象或反射构建对象)
- 构建子类实例对象(构建类的对象时会首先加载父类类型)
显示加载
- ClassLoader.loadClass(...)
- Class.forName(...)
-
- 类加载时静态代码块一定会执行吗?
答:
通过ClassLoader对象的loadClass方法加载类不会执行静态代码块.
-
- 如何理解类的主动加载和被动加载?
答:
主动加载, 是有目的性的, 是显示加载, 会执行加载、连接、初始化静态域;
被动加载, 是被触发的, 是隐式加载, 只执行加载、连接,不初始化类静态域.
-
- 为什么要自己定义类加载器,如何定义?
答:
有时需要定制类的加载方式以满足某些特殊需求, 例如:
- 扩展加载源(从数据库中加载类);
- 防止源码泄漏(对字节码文件进行加密, 用时再通过自定义类加载器对其进行解密);
- 隔离类的加载(不同框架有相同全限定名的类)
如何定义?
一种简单的方式就是继承URLClassLoader, 此类可以直接从指定目录, jar包, 网络中加载指定的类资源
- 字节码增强部分
- 为何要学习字节码?
答:
对于开发人员, 了解字节码可以更准确, 直观地理解Java语言中更深层次的东西, 比如通过字节码, 可以很直观地看到Volatile关键字如何在字节码上生效. 另外, 字节码增强技术在SpringAOP, 各种ORM框架, 热部署中的应用屡见不鲜, 深入理解其原理对于我们来说大有裨益. 除此以外, 由于JVM规范的存在, 只要最终可以生成符合规范的字节就可以在JVM上运行, 因此这就给了各种运行在JVM上的语言(如Scala, Groovy, Kotlin)一种契机, 可以扩展Java所没有的特性或者实现各种语法糖. 理解字节码后再学习这些语言, 可以”逆流而上”. 从字节码视角看它的设计思路, 学习起来也”易如反掌”.
-
- 如何解读字节码内容?
- 直接解读:
IntTests.java的源代码编译后,可以通过notepad++(需要安装一下HEX-Editor插件)打开IntTests.class文件,文件内容默认是一种16进制的格式.
2.javap指令应用
在IntTests.class目录使用如下代码对类进行反编译.javap -verbose IntTests.class
3.jclasslib插件应用
如果每次查看反编译后的字节码都使用javap命令的话,会非常繁琐。这里推荐一个Idea插件,这个插件的名字为jclasslib。代码在编译后,我们可以在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。
-
- 字节码内容由哪几部分构成?
答:
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]: 属性数组
-
- 什么是字节码增强?
答:
字节码增强技术相当于是一把打开运行时JVM的钥匙, 利用它可以对现有字节码进行修改或者动态生成新的字节码, 进而对运行中的程序做修改, 实现热部署. 也可以跟踪JVM运行中程序的状态, 进行性能诊断等.
此外, 我们平时使用的动态代理, AOP也与字节码增强密切相关, 它们实质上还是利用各种手段生成符合规范的字节码文件.
-
- 为什么要进行字节码增强?
答:
掌握字节码增强后可以高效的定位并快速修复一些棘手的问题(如线上性能问题, 方法出现不可控的出入参需要紧急加日志等问题), 也可以在开发中减少冗余代码, 大大提高开发效率.
-
- 你了解哪些字节码增强技术?
答:
- ASM技术: 对于需要手动操纵字节码的需求, 可以使用ASM, 它可以直接生产.class字节码文件, 也可以在类被加载入JVM之前动态修改类行为. ASM的应用场景有AOP(Cglib就是基于ASM), 热部署, 修改其他jar包中的类等.
- Javassist技术: Javassist是一个用于分析, 编辑和创建Java字节码的类库, 相比ASM在指令层次上操作字节码会更加简单直接. 可以无须关注字节码刻板的结构, 直接使用Java编码的形式, 而不需要了解虚拟机指令.
- Java Agent技术: Java Agent是Java Instrumentation API的一部分, 它提供了向现有已编译的Java类添加字节码的功能, 相当于字节码插桩的入口. 可以侵入运行的JVM上的应用程序, 进而修改应用程序中的各类字节码.
-
- 什么是热替换以及如何实现?
答:
热替换就是通过一定的字节码增强技术, 侵入运行在JVM上的应用程序, 进而修改应用程序中各种类的字节码, 实现不停机,不停服更新或升级服务的技术.
- 创建Transformer对象, 用于对目标对象进行功能增强.
- 创建Agent对象, 用于调用Transformer对象执行字节码增强.
- 通过VirtualMachine的attach api加载Java Agent, 这组api其实是JVM进程之间沟通的桥梁, 底层通过socket进行通信.
- 创建Agent启动类,传入目标JVM pid.
- 分别启动目标类和Agent启动类即可.
- JVM运行内存部分
- JVM运行内存是如何划分的?
答:
分为:方法区, 堆, 虚拟机栈, 本地方法栈, 程序计数器
-
- JVM中的程序计数器用于做什么?
答:
用来存储指向下一条指令的地址, 也可以看做是当前线程执行的字节码的信号指示器.
-
- JVM虚拟机栈的结构是怎样的?
答:
每个线程都有自己的栈(弹匣样式), 栈中的数据都是以栈帧(Stack Frame)的格式存在, 在这个线程上正在执行的每个方法各自对应一个栈帧, 每个栈帧中存储着:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 一些附加信息
- JVM虚拟机栈中局部变量表的作用是什么?
答:
局部变量表也称之为局部变量数组或本地变量表, 用于存放方法参数和方法内部定义的局部变量信息. 在Java程序被编译为.class文件时, 就已经确定了每个方法所需局部变量表的大小.
-
- JVM虚拟机栈中操作数栈的做用时什么?
答:
操作数栈, 在方法执行过程中, 根据字节码指令, 往栈中写入数据或提取数据, 即入栈和出栈.
-
- JVM堆的构成是怎样的?
答:
Java堆内存在JVM中可分为年轻代和老年代, 年轻代又分为Eden和Survivor.
-
- Java对象分配内存的过程是怎样的?
答:
- 编译器通过逃逸分析, 确定对象是在栈上还是在堆上分配;
- 如果是在堆上分配, 则首先检测是否可以在TLAB(Thread Local Allocation Buffer)上直接分配;
- 如果TLAB上无法直接分配则在Eden加锁去进行分配(线程共享区);
- 如果Eden区无法存储对象, 则执行Yong GC(Minor Colection);
- 如果Yong GC之后Eden区仍然不足以存储对象, 则直接分配在老年代;
- 新生代由Eden区和两个幸存区构成, 任意一个时刻至少有一个幸存区是空的, 用于存放下次GC时未被收集的对象;
- GC触发时Eden区所有”可达对象”会被复制到另一个幸存区, 假设为s1, 当幸存区s1无法存储这些对象时会复制到老年代;
- GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2, 同时清空Eden区和s1区;
- GC在次触发Eden和s2幸存区中的”可达对象”会被复制到另一个幸存区s1, 同时清空Eden区和s2幸存区. 以此类推.
- 当多次GC过程完成后, 幸存区中的对象存活时间达到一定阈值(可以用参数-XX:_+<MaxTenuringThreshold来指定上限, 默认15), 会被看成”年老”的对象然后直接移动到老年代.
- JVM年轻代幸存区设置的比较小会有什么问题?
答:
伊甸园区被回收时, 对象要拷贝到幸存区, 假如幸存区比较小, 拷贝的对象比较大, 对象就会直接存储到老年代, 这样就会增加老年代GC的频率. 而分代回收的思想就会被弱化.
-
- JVM年轻代伊甸园区设置的比例比较小会有什么问题?
答:
伊甸园设置的比较小, 会增加GC的频率, 可能导致SWT的时间变长, 影响系统性能.
-
- JVM堆内存为什么要分成年轻代和老年代?
答:
为了更好的实现垃圾回收.
-
- 如何理解JVM方法区以及它的构成是怎样的?
答:
方法区是一种规范, 用于存储已被虚拟机加载的类信息, 常量, 静态变量, 即时编译后的代码等数据. 不同jdk, 方法区的实现不同, HotSpot虚拟机在JDK8中使用Native Memory来实现方法区.
构成:
- 类信息包括对每个加载的类型(类class, 接口interface, 枚举enum, 注解annotation)以及属性和方法信息.
- 常量信息可以看做是一张表, 虚拟机指令根据这张常量表找到要执行的类名, 方法名, 参数类型, 字面量等类型.
- 什么是逃逸分析以及可以解决什么问题?
答:
逃逸分析是一种数据分析算法, 基于此算法可以有效减少Java对象在堆内存中的分配. Hotspot虚拟机的编译器能够分析出一个新对象的引用范围, 然后决定是否要将这个对象分配到堆上. 例如:
- 当一个对象在方法中被定义后, 对象只在方法区内部使用, 则认为没有发生逃逸.
- 当一个对象在方法中被定义后, 它被外部方法所引用, 则认为发生逃逸.
解决问题:
- 栈上分配: 将堆分配转化为栈分配. 如果一个对象在方法区创建, 要使指向该对象的引用不会发生逃逸, 对可能是栈上分配候选.
- 同步省略: 如果一个对象被发现只有一个线程被访问到, 那么对于这个对象的操作可以不考虑同步.
- 分离对象或标量替换: 有的对象可能不需要作为一个连续的内存结构在也可以被访问到, 那么对象的部分(或全部)可以不存储在内存, 而是存储在CPU寄存器中.
-
- 何为内存溢出以及导致内存溢出的原因?
答:
内存中剩余的内存不足以分配给新的内存请求就会内存溢出. 内存溢出可能导致系统崩溃.
内存泄漏是导致内存溢出的一种原因, 但内存溢出不全是内存泄漏引起的, 还可能是:
- 创建的对象太大导致堆内存溢出;
- 创建的对象太多导致堆内存溢出;
- 方法出现了无限递归调用导致栈内存溢出;
- 方法区内存空间不足导致内存溢出.
- 何为内存泄漏以及内存泄漏的原因是什么?
答:
动态分配的内存空间, 在使用完毕后未得到释放, 结果导致一直占据该内存单元, 直到程序结束. 这个现象称之为内存泄漏. 因此, 良好的代码规范, 可以有效的避免这些错误.
原因:
- 大量使用静态变量(静态变量与程序生命周期一致);
- I/O连接资源用完没关闭(记得执行close操作)
- 内部类的使用方式存在问题(实例内部类或默认引用外部类对象);
- 缓存(Cache)应用不当(尽量不要使用强引用);
- ThreadLocal应用不当(用完记得执行remove操作).
-
- JAVA中的四大引用你知道多少?
答:
- 强引用: 通过关键字new创建的对象所关联的引用就是强引用. 当Java内存空间不足, JVM宁愿抛出OutOfMemoryError运行时错误, 使程序异常终止, 也不会靠随意回收具有强引用的”存活”对象来解决内存不足的问题.对于一个普通对象, 如果没有其他的引用关系, 只要超过了引用的作用域或者显式地将相应的(强)引用赋值为null, 就可以被垃圾回收器收集了, 具体回收时机还是要看垃圾收集策略.
- 软引用: 通过SoftReference类实现. 软引用的生命周期比强引用短一些. 只有当JVM认为内存不足时, 才会去试图回收软引用的对象: 即JVM会确保在抛出OutOfMemoryError之前, 清理软引用指向的对象. 软引用可以和一个引用队列联合使用, 如果软引用所引用的对象被垃圾回收器回收, Java虚拟机就会把这个软引用加入到与之关联的引用队列中. 后续, 我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收. 如果队列为空, 将返回一个null, 否则该方法返回队列中前面的一个Reference对象.应用场景: 软引用通常用来实现内存敏感的缓存. 如果还有空闲内存, 就可以暂时保留缓存, 当内存不足时清理掉, 这样就保证了使用缓存的同时, 不会耗尽内存.
- 弱引用:
弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
应用场景:弱应用同样可用于内存敏感的缓存。
- 虚引用
特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。
应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。
- JVM垃圾回收部分
- 何为GC以及为和要GC?
答:
GC(Garbage Collection)称之为垃圾回收, 是对内存中的垃圾对象, 采用一定的算法进行内存回收的一个动作. 比如说, Java中的垃圾回收会对内存中的对象进行遍历, 对存活的对象进行标记, 其未标记对象可认为是垃圾对象, 然后基于特定算法进行回收.
如果没有GC, 我们的内存会很快溢出. 深入理解GC的工作机制, 可以帮助我们写出更好的Java应用, 提高开发效率, 同时也是进军大规模应用开发的一个前提.
-
- 你知道哪些GC算法?
答:
- 标记清除: 该算法分为”标记”和”清除”两个阶段,它会首先标记处内存中所有不需要回收的对象, 然后从内存中清除所有未标记的对象.
- 标记复制: 该算法是将内存分为大小相同的两块, 当这一块使用完了, 就把当前存活的对象复制到另一块, 然后一次性清除当前区块.
- 标记整理: 该算法结合了”标记”和”复制”两个算法的优点. 第一阶段从根节点开始标记所有被引用对象, 第二阶段遍历整个堆, 把存活对象”压缩”复制到堆的其中一块空间中, 按顺序排放, 第三阶段清理掉存活边界以外的去全部内存空间.
-
- JVM中有哪些垃圾回收器?
答:
- Serial收集器;
- Parallel收集器;
- CMS收集器;
- G1收集器.
-
- 服务频繁fullgc,younggc次数较少,可能原因?
答:
- 系统承载高并发请求或处理数据量过大;
- 系统一次性加载过多数据进内存, 搞出很多大对象, 导致频繁有大对象进入老年代;
- 系统发生了内存泄漏, 莫名创建大量的对象, 始终无法回收, 一直占用在老年代里;
- Metaspace因为加载类过多;
- 误调用System.gc()触发GC.