朋友们,金三银四跳槽季,又要开始卷了。
关于 JVM ,冷风准备了这些问题,希望对你有帮助。
0 JDK、JRE、JVM 三者是什么关系?
按照包含的范围从大到小,JDK > JRE > JVM 。
JDK = Java development Kit( 包括 javac、javap 等 ) + JRE
JRE = JVM + Java CoreLib
1 java是编译执行的还是解释执行的?
首先 java 文件编译成 class 文件后,被 classLoader 加载进内存。
加载进内存后,会有不同的处理套路。
- 由字节码解释器去逐句解释,交予执行引擎执行,大多数情况是这样。
- 对于经常使用到的代码,例如循环,由 JIT 即时编译器编译成本地代码,直接交给执行引擎执行,提高执行效率。
默认情况下,JVM 使用的是 mixed mode(混合模式)。当然,也可以根据实际情况,调整为其他模式。
| 模式 | 优点 | 缺点 |
|---|---|---|
| 混合模式 | 开始解释执行,然后对热点代码进行检测并编译 | |
| 解释模式 | 启动快 | 执行慢 |
| 编译模式 | 启动慢 | 执行快 |
所以,JAVA 不能说是解释还是编译执行,具体跟设置有关。
2 JVM 只能执行 java 文件吗?
其实,任何可以按照 class 文件规范,被编译成 class 文件的程序都可以在 JVM 上执行,例如 scala 、kotlin 等。
3 你知道 JVM 有几种实现吗?
最常见的有实现就是 HotSpot 了,现在 sun 公司的 JDK,用的就是这款虚拟机。
C:\Users> java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) Client VM (build 25.191-b12, mixed mode)
除此之外,还有 JRockit 、 IBM 的 J9 、MicroSoft VM 、Taobao VM 等。
4 说说 class 文件大致包含哪些内容?
class 文件是java编译而成的,符合某种规范的二进制文件,大致包括 魔数(class文件标识)、JDK 版本号、常量池、访问标记、接口、方法、属性等信息。
5 说说对象创建的过程。
首先是类加载过程,该过程大致分 3 步:
- Loading , 将文件二进制字节流加载进内存。
- Linking
- Verification ,验证 class 文件是否符合格式规范,例如是否以 CAFEBABE 开头等。
- Preparation ,该过程中,将 JAVA 静态变量赋默认值,例如 int 默认值为 0 。
- Resolution , 将常量池里的符号引用,转换成直接内存地址。
- Initializing ,静态变量赋初始值,执行类初始化代码,执行静态语句块。
其次,是对象创建过程。
构建对象,为对象分配内存空间
为成员变量赋默认值
初始化对象
- 成员变量赋初始值
- 执行构造方法
6 JVM 的类加载器是怎样的。(说说双亲委派是什么意思?)
类加载器有三个层次,自上而下分别是:
- BootStrapClassLoader 加载 rt.jar 等 class。
- ExtClassLoader 加载 jre\lib\ext 下的 jar。
- AppClassLoader 加载 classpath下的 class。
当一个类需要被加载到内存时,首先由最底层的 AppClassLoader 进行查找(它不会首先去加载这个class),是否已加载,如果 APP 加载了,则返回结果。如果 App 未加载,则继续向上查找父加载器 Ext 是否已加载,如果 Ext 加载了,则返回,如果 Ext 未加载,则请求父加载器 BootStrap,如果 BootStrap 加载了,则返回。
如未加载,则BootStrap 判断是否应该自己加载,如果是,则加载。如果不是,则逐级向下找到职责范围内的 classLoader,直到 class 最终被加载。
如果所有的 ClassLoader 都未加载,则报错 ClassNotFoundException。
这样,自下而上进行查找,又自上而下加载,就叫做双亲委派机制。
相应的双亲委派代码(有递归):
protected Class<?> loadClass(String paramString, boolean paramBoolean)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(paramString))
{
Class localClass = findLoadedClass(paramString);
if (localClass == null)
{
long l1 = System.nanoTime();
try
{
if (this.parent != null) {
localClass = this.parent.loadClass(paramString, false);
} else {
localClass = findBootstrapClassOrNull(paramString);
}
}
catch (ClassNotFoundException localClassNotFoundException) {}
if (localClass == null)
{
long l2 = System.nanoTime();
localClass = findClass(paramString);
PerfCounter.getParentDelegationTime().addTime(l2 - l1);
PerfCounter.getFindClassTime().addElapsedTimeFrom(l2);
PerfCounter.getFindClasses().increment();
}
}
if (paramBoolean) {
resolveClass(localClass);
}
return localClass;
}
}
双亲委派机制,主要目的是为了安全,防止自定义类随意覆盖系统固有类。
7 什么场景下会用到 ClassLoader.loadClass 方法?
当 spring 生成动态代理类后,需要手动将此代理类加载到内存。 或者 tomcat 的热部署,就是将修改的class 手动加载到内存。
8 如何自定义一个类加载器?
ClassLoader 类的 findClass 方法抛出了一个异常。
自定义类加载器只需要:
- 继承 ClassLoader
- 重写模板方法 findClass
- 调用 super.defineClass 将二进制字节流转换为 Class 对象。
另外,重写 loadClass 方法,其实是可以破坏 java 的双亲委派机制。
9 JAVA是懒加载( lazyLoading,更准确说,是 lazyInitializing ),那会类在什么时候被系统初始化呢?
- new
- Class.forName
- 访问类的静态变量或方法
不会初始化的情况
- 访问静态 final 变量
10 单例模式中的单例对象定义时,需要加 volatile 吗?
需要,voldatile 会防止 jvm 指令重排,保证对象完全初始化后返回。
指令重排可能会导致出现半初始化状态的对象。
11 volatile 是如何防止指令重排和做到内存可见的?
voldatile 是通过插入内存屏障(Memory Barrier)来防止指令重排的。 volatile的实现分 3 个层次:
- 字节码层面
当一个变量 i 被修饰为 volatile 时,在编译的字节码上会增加一个 access_flag ACC_VOLATILE;
- JVM 层面
JVM 解析 字节码访问标记 ACC_VOLATILE 时,将会在变量读写之前都加入屏障:
对所有 volatile 写操作前面增加 StoreStoreBarrier ,后面增加 StoreLoadBarrier;
在所有 volatile 读操作前面增加 LoadLoadBarrier ,后面增加 LoadStoreBarrier ;
- 操作系统层面 (以 x86 为例)
通过汇编指令 lock 来实现。 lock 后的写操作会回写已修改的数据到主存,同时让其他的 CPU 缓存数据失效,从而重新从主存中加载数据; lock 指令能够阻止屏障两边的指令重排序。
12 聊聊 synchronized 的实现细节?
和 volatile一样,分 3 个层面:
- 字节码层面
其实就是在同步块前后用 monitorenter 和 monitorexit 标识;或者增加 ACC_SYNCHRONIZED 访问标志符;
- JVM 层面
会调用操作系统提供的同步函数
- 操作系统层面 (以 x86 为例)
实际上是一条 lock cmpxchg 指令,防止其他线程进入。
13 对象的内存布局是怎样的?
普通对象组成部分:
对象头 markword 8字节
ClassPoiter 指针
- 开启压缩(-XX:+UseCompressedClassPointers) 4 字节
- 不开启压缩 8 字节
对象数据
- 引用类型(-XX:+UseCompressedOops) 开启压缩为 4 字节
- 引用类型 不开启压缩 8 字节
- 基本类型
对齐 对象大小不为 8 的倍数时,需要对齐(padding)
数组对象较普通对象,多一个数组长度( 4 字节)
例如: new Object() 这个对象占多少字节呢?
指针压缩情况:对象头 8 + ClassPoiter 指针 4 + padding 4 = 16个字节;
不压缩的情况:对象头 8 + ClassPoiter 指针 8 = 16个字节
缓存行对齐 伪共享,CPu 会将缓存行的整个数据读到缓存中,多数实现是64个字节。
解决伪共享
JVM 运行时数据区和常用指令
JVM运行时数据区
程序技术器 (program counter)
栈
- JVM 栈 (每个线程对应一个栈)
栈帧 (每个方法对应一个栈帧)
- 局部变量表
- 操作数栈
- 动态链接
- 返回值地址
- 本地方法栈
- JVM 栈 (每个线程对应一个栈)
直接内存(JVM可以直接访问的内存 NIO)
方法区
堆
方法区
- Perm Space (<1.8) 字符串常量位于PermSpace FGC不会清理 大小启动的时候指定,不能变
- Meta Space (>=1.8) 字符串常量位于堆 会触发FGC清理 不设定的话,最大就是物理内存