1. 入门部分
1.1为什么要学习JVM?
可以帮助我们从平台角度提高解决问题的能力,例如:
- 有效防止内存泄漏(Memory leak).
- 优化线程锁的使用 (Thread Lock).
- 科学进行垃圾回收(Garbage collection).
- 提高系统吞吐量(throughput).
- 降低延迟(Delay),提高其性能(performance).
1.2 你了解哪些JVM产品?
- HotSpot VM(Sun/Oracle JDK和Open JDK的默认虚拟机)
- JRockit VM
- J9 VM
- TaobaoVM
- Dalvik VM
1.3 JVM的构成有哪几部分?
- 类加载系统 (ClassLoader System): 负责加载类到内存.
- 运行时数据区(Runtime Data Area): 负责存储数据信息(对象、方法等).
- 执行引擎(Execution Engine): 负责解释执行字节码、执行GC操作等.
- 本地库接口(Native Interface): 负责融合不同的编程语言为 Java 所用.
2. 类加载部分
2.1 你知道哪些类加载器?
- BootStrapClassLoader(根/引导类加载器):java的核心类库都是使用引导类加载器进行加载的
- ExtClassLoader(扩展类加载器):该类加载器负责加载java的扩展库或者java.ext.dirs路径下的内容
- AppClassLoader(应用/系统类加载器):对于用户自定义的类来说,默认使用这个类加载器加载
2.2 什么是双亲委派类加载模型?
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式.父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常.
2.3 双亲委派方式加载类有什么优势、劣势?
- 优势:
1. 避免类的重复加载,确保一个类的全局唯一性
2. 保护程序的安全,防止核心API被随意篡改
- 劣势:
1. 父级加载器无法加载子级类加载器路径中的类
2.4 描述一些类加载时候的基本步骤是怎样的?
- 通过一个类的全限定名(类全名)来获取定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的进行时数据结构
- 在Java堆中生成一个代表这个类的Java.lang.Class对象,作为方法区中这些数据的访问接口
2.5 什么情况下会触发类的加载?
类加载方式主要有两种:隐式加载和显示加载
- 隐式加载:
1. 调用静态成员时,会加载静态成员真正所在的类及其父类,通过子类调用父类的静态成员时,只会加载父类而不会加载子类.
2. 第一次new对象的时候加载(第二次在new同一个类时,不需要再加载).
3. 加载子类会先加载父类.
- 显式加载:
1. 通过ClassLoader.loadClass()方法进行加载.
2. 通过Class.forName()方法进行加载.
2.6 类加载时静态代码块一定会执行吗?
不一定,静态代码块块发生在类的"初始化"阶段,不初始化则不会执行静态代码块.
2.7 如何理解类的主动加载和被动加载?
- 主动加载:是有目的性的,是显示加载,会执行加载,连接,初始化静态域.
- 被动加载:是被触发的,是隐式加载,只执行加载,连接,不初始化类静态域.
2.8 为什么要自己定义类加载器,如何定义?
- 隔离加载类(不同框架有相同全限定名的类).
- 修改类加载的方式(打破类的双亲委派模型).
- 扩展加载源(例如从数据库中加载类).
- 防止源码泄漏(对字节码文件进行加密,用时再通过自定义类加载器对其进行解密).
- 继承ClassLoader.
- 重写findClass,在findClass里获取类的字节码,并调用ClassLoader中的defineClass方法来加载类,获取class对象.
3. 字节码增强部分
3.1 为何要学习字节码?
对于开发人员来说,了解字节码可以更准确,直观的理解java语言中更深层次的东西,比如通过字节码,可以直观地看到Volatile关键字如何在字节码上生效.另外,字节码增强技术在Spring AOP,各种ORM框架,热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益.除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala,Groovy,Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法.理解字节码后再学习这些语言,可以"逆流而上".从字节码视角看它的设计思路.学习起来也"易如反掌".
3.2 如何解读字节码内容?
- 可以通过notepad++(需要安装HEX-Editor插件)打开.class文件.
- jdk自带的反编译工具–javap.
- Idea中安装插件jclasslib.
3.3 字节码内容由哪几部分构成?
- magic(魔数)
- minor_version(次版本号)
- major_version(主版本号)
- constant_pool_count(常量池计数器)
- constant_pool<constant_pool_count-1>(常量池)
- access_flags(类的访问标志)
- this_class(当前类名索引值)
- super_class(父类名索引值)
- interfaces_count(接口计数)
- interfaces<interfaces_count>(接口数组)
- fields_count(成员变量计数)
- fields<fields_count>(成员变量数组)
- methods_count(方法计数)
- methods<methods_count>(方法数组)
- attributes_count(属性计数)
- attributes<attributes_count>(属性数组)
3.4 什么是字节码增强?
字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以对现有字节码进行修改或者动态生成新的字节码,进而对运行中的程序做修改,实现热部署.也可以跟踪JVM运行中程序的状态,进行性能诊断等.
3.5 为什么要进行字节码增强?
掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题,方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率.
3.6 你了解哪些字节码增强技术?
- ASM技术
- Javaassist技术
- Java Agent技术
3.7 什么是热替换以及如何实现?
- 什么是热替换:
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为.
热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中.
- 如何实现:
1. 创建业务service类,将此类作为字节码增强对象.
package com.java.jvm.bytecode.service;
public class CycleService {
public void doCycle(){
System.out.println("doCycle()");
}
}
- 创建Transformer对象,用于对CycleService对象进行功能增强.
package com.java.jvm.bytecode.instrument;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class DefaultClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Transforming " + className);
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("com.java.jvm.bytecode.service.CycleService");
CtMethod m = cc.getDeclaredMethod("doCycle");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
byte[] bytes = cc.toBytecode();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- 创建Agent对象,用于调用DefaultClassTransformer对象执行字节码增强,在Agent中可定义两个方法进行不同时间点进行增强.
package com.java.jvm.bytecode.instrument;
import com.java.jvm.bytecode.service.CycleService;
import java.lang.instrument.Instrumentation;
/**
* 定义Agent对象
*/
public class DefaultAgent {
/**假如你希望在main方法执行之前执行,就这样定义方法*/
public static void premain(String args, Instrumentation inst){
System.out.println("premain->"+args);
inst.addTransformer(new DefaultClassTransformer(),true);
}
/**
* 这种方式是要以attach的方式进行载入,然后在java程序启动后执行。
* @param args
* @param inst
*/
public static void agentmain(String args, Instrumentation inst){
System.out.println("agentmain->"+args);
inst.addTransformer(new DefaultClassTransformer(),true);
try {
//指明哪些类需要重新加载
inst.retransformClasses(CycleService.class);
}catch (Exception e){
System.out.println("agent error");
}
}
}
- 添加maven插件用于对项目进行打包.
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.java.jvm.bytecode.instrument.DefaultAgent</Premain-Class>
<Agent-Class>com.java.jvm.bytecode.instrument.DefaultAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
- 创建CycleServiceTests类,对CycleService对象进行调用,也就是启动服务.
package com.java.jvm.bytecode;
import com.java.jvm.bytecode.service.CycleService;
import java.lang.management.ManagementFactory;
public class CycleServiceTests {
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
//打印当前Pid
System.out.println("pid:"+s);
CycleService cs=new CycleService();
while(true) {
try {
cs.doCycle();
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
- 添加tools依赖.
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
- 创建AgentInstrumentTests类.
package com.java.jvm.bytecode;
import com.sun.tools.attach.VirtualMachine;
import java.lang.management.ManagementFactory;
public class AgentInstrumentTests {
public static void main(String[] args) throws Exception{
//传入目标 JVM pid(这里的id为CycleServiceTests类中打印的进程id)
VirtualMachine vm = VirtualMachine.attach("4328");
vm.loadAgent("E:/TCGBIV/DEVCODES/CGB2112CODES/01-java/target/01-java-1.0-SNAPSHOT.jar");
}
}
- 分别运行CycleServiceTests、AgentInstrumentTests类进行测试 .
4. JVM运行内存部分
4.1 JVM运行内存是如何划分的?
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 方法区
- 堆
4.2 JVM中的程序计数器用于做什么?
CPU运行时,需要在线程间来回切换,当切换回当前线程时,为了知道从哪里开始执行,就需要一个计数器记录CPU在当前线程的执行位置.
4.3 JVM虚拟机栈的结构是怎样的?
- 局部变量表.
- 操作数栈(或表达式栈).
- 动态链接(或指向运行时常量池的方法引用).
- 方法返回地址(或方法正常退出或者异常退出的定义).
- 一些附加信息.
4.4 JVM虚拟机栈中局部变量表的作用是什么?
用于存放方法参数和方法内部定义的局部变量信息.
4.5 JVM虚拟机栈中操作数栈的做用时什么?
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据.
4.6 JVM堆的构成是怎样的?
堆内存在jvm中可分为年轻代和老年代,年轻代又分为Eden和两个Survivor区.
4.7 Java对象分配内存的过程是怎样的?
- 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配.
- 如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配.
- 如果TLAB上无法直接分配,则在Eden加锁区进行分配(线程共享区).
- 如果Eden区无法存储对象,则执行Yong GC(Minor Collection).
- 如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代.
4.8 JVM年轻代幸存区设置的比较小会有什么问题?
伊甸园区被回收时,对象要拷贝到幸存区,如果幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率,而分代回收的思想就会被弱化.
4.9 JVM年轻代伊甸园区设置的比例比较小会有什么问题?
伊甸园区设置的比较小会增加GC的频率,可能导致STW的时间变长,影响系统性能.
4.10 JVM堆内存为什么要分成年轻代和老年代?
为了更好的实现垃圾回收.
4.11 如何理解JVM方法区以及它的构成是怎样的?
- 如何理解JVM方法区:
方法区是一种规范,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据.
- JVM方法区的构成是怎样的:
1. 类信息包括对每个加载的类型(类,接口,枚举,注解)以及属性和方法信息.
2. 常量信息可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型.
4.12 什么是逃逸分析以及可以解决什么问题?
逃逸分析是一种数据分析算法,基于此算法可以有效减少java对象在堆内存中的分配.
4.13 何为内存溢出以及导致内存溢出的原因?
- 何为内存溢出
内存中剩余的内存不足以分配给新的内存请求就会内存溢出,内存溢出可能直接导致系统崩溃.
- 导致内存溢出的原因:
1. 内存泄漏.
2. 创建的对象太大导致堆内存溢出.
3. 创建的对象太多导致堆内存溢出.
4. 方法出现了无限递归调用导致栈内存溢出.
5. 方法区内存空间不足导致内存溢出.
4.14 何为内存泄漏以及内存泄漏的原因是什么?
- 何为内存泄漏:
动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束,这个现象称之为内存泄漏.
- 内存泄漏的原因是什么:
1. 大量使用静态变量(静态变量与程序生命周期一样).
2. IO/连接资源用完没关闭(用完没有close).
3. 内部类的使用方式存在问题(实例内部类或默认引用外部类对象).
4. 缓存应用不当(尽量不要强引用).
5. ThreadLocal应用不当(用完没有remove).
4.15 JAVA中的四大引用你知道多少?
- 强引用:
我们使用的最普遍的引用,如果一个对象具有强引用,那么当虚拟机内存不足时,虚拟机宁愿抛出OutOfMemoryError错误,也不愿随意回收强引用.
- 软引用:
如果内存空间足够,就不会回收它;如果内存空间不足,就会回收这类对象的内存,只要垃圾回收期没有回收它,就可以一直被程序使用.
- 弱引用:
当垃圾回收器扫描内存空间时,一旦发现只有弱引用的对象,不管当前内存空间足够与否,都会对其进行回收. 在使用过程中,其可以和引用队列(ReferenceQueue)一同使用.
- 虚引用:
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收.虚引用主要用来跟踪对象被垃圾回收的活动.
5. JVM垃圾回收部分
5.1 何为GC以及为何要GC?
- 何为GC:
GC为JVM对堆内存进行垃圾回收的一个动作.
- 为何要GC:
程序在运行过程中会不断地产生内存碎片,如果一直不进行GC操作,那么程序迟早会因为内存耗尽而抛出OutOfMemoryError错误而终止程序.
5.2 你知道哪些GC算法?
-
标记清除:分为”标记”和”清除”阶段,首先会标记出内存中所有不需要回收的对象,然后从内存中清除所有未标记的对象.
-
标记复制:将内存分为大小相同的两块,当一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区域.
-
标记整理:结合了”标记-清除”和”复制”两个算法的优点,第一阶段从根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把存活对象”压缩”复制到堆的其中一块空间中,按顺序排放,第三阶段,清理掉存活边界以外的全部内存空间.
5.3 JVM中有哪些垃圾回收器?
- Serial收集器(串行收集器)
- Parallel收集器(并行收集器)
- CMS收集器(并发收集器)
- G1收集器
5.4 服务频繁fullgc,younggc次数较少,可能原因?
- 系统承载高并发请求,或者处理数据量过大,导致Young GC很贫乏,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代,频繁触发Full GC.
- 系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年代,必然频繁触发Full GC.
- 系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发Full GC.
- Metaspace(永久代)因为加载类过多触发Full GC.
- 误调用System.gc()触发Full GC.
文章到这里就先结束了,后面还会持续更新,希望能帮助到各位大佬。如果文章有需要改进的地方还请各位大佬斧正。