主题
Java 虚拟机 已完结
JVM
说一下 JVM 的内存结构
提示
堆、虚拟机栈、本地方法栈、方法区、程序计数器
- 方法区:类对象、常量、常量池、静态变量
- 堆:存储对象实例
- 虚拟机栈
- 本地方法栈
- 程序计数器:存储下一条指令地址
说一下堆和栈的区别
提示
个数、内容、物理地址、生命周期
- 内存共享:每个线程都有各自的栈,所有线程共享一个堆。
- 存储内容:栈存放临时变量、操作数等,堆存放对象实例。
- 生命周期:栈跟随线程,堆跟随整个程序
- 物理地址:栈的地址是连续的,堆的地址是分散的。
内存溢出和内存泄漏的区别
- 内存泄漏:申请了内存没有释放
- 内存溢出:没有足够的内存去使用
栈溢出的原因
- 设置的栈内存过小
- 方法中存在错误,比如无限递归
方法区溢出的原因
- 使用动态代理生成了过多的类
- 设置的方法区内存过小
运行时常量池溢出的原因
- 常量池设置的的内存过小
- 大量调用了 intern 方法
什么是类加载?类加载的过程?
什么是类加载:将 class 文件读入内存,存放在 JVM 的方法区中的过程。
类加载的过程:加载、连接(验证、准备、解析)、初始化、使用、卸载。
什么是类加载器?类加载器有哪些?
类加载器:用于通过类的全限定名获取 Class 对象
类加载器:
- 启动类加载器
- 扩展类加载器
- 应用程序加载器
- 自定义加载器
什么是双亲委派模型?
一个类加载器收到类加载请求时,会先委托父类加载器完成加载,直至启动类加载器。只有父加载器无法完成加载时,会让子类加载器尝试加载。
为什么需要双亲委派模型?
- 保证 Java 的核心代码不会被恶意替换,保证代码安全。
- 避免一个类被重复加载
怎么判断两个类是否相等?
- 来源于一个 Class 文件(相同限定名)
- 被同一个类加载器加载
类的实例化顺序?
- 父类 static
- 当前类 static
- 父类普通代码
- 父类构造方法
- 当前类普通代码
- 当前类构造方法
Java 对象的定位/访问方式
提示
直接指针:引用指向堆中的地址
句柄:维护一个映射表,表中存放堆中的地址
Java 通过指向对象的引用来操作对象,访问方式分为句柄和直接指针两种。
- 直接指针:引用指向堆中对象的地址。优点是访问速度快。(Hotspot 虚拟机使用这种)
- 句柄:引用指向堆中的一个句柄,句柄里存放真实对象的地址。优点是对象移动改变地址时,不需要修改对象引用的地址。
如何判断一个对象是否存活?
提示
引用计数法、可发性分析
- 引用计数法:记录这个对象被引用的次数
- 可达性分析:通过存在的引用,分析是否能访问到该对象
GC 是什么?为什么需要 GC?
GC 是垃圾回收机制。
为什么需要 GC:内存回收非常容易出问题,Java 提供的 GC 可以实现自动回收内存。
可作为 GC Roots 的对象有什么?
- 栈中的引用的对象
- 静态属性引用的对象
- synchronized 持有的对象
- 常量引用的对象
- Native 方法引用的对象
- JVM 内部引用的对象
什么情况下类会被卸载?
- 所有实例已经被回收
- 加载该类的类加载器已经被回收
- 该类的类对象没有被引用,不能通过反射访问该类
可以对满足上述 3 个条件的类进行回收,但不一定会进行回收。
强引用、软引用、弱引用、虚引用是什么?
- 强引用:对象不会被回收。
- 软引用:内存不足时回收,常用于缓存机制。
- 弱引用:垃圾回收器运行时回收。
- 虚引用:用于跟踪对象回收状态。
聊聊 Java 的 GC,分别作用在什么范围?
- Young GC:新生代
- Old GC:老年代
- Full GC:整个堆
- Mixed GC(混合收集):新生代和部分老年代(G1 GC)
JVM 的内存分配策略
- 对象优先在 Eden(伊甸园区)分派
- 大对象和长期存活的对象直接进入老年代
- 动态对象年龄判定
- 空间分配担保
Full GC 的触发条件
- 主动调用
System.gc()
- 老年代空间不足
- 空间分配担保失败
垃圾回收算法有那些?
- 标记清除:通过标记不需要的对象,再统一清除。
- 复制清除:将存活对象复制到新空间,旧空间的对象全部清除。
- 标记整理:标记存活对象,再整理存活对象以消除碎片。
- 分类收集:将对象分为不同代,根据对象的生命周期选择不同的回收策略。
有哪些垃圾回收器?
对象头了解吗?
对象由对象头、实例数据、对齐填充字节组成。
对象头分为 MarkWord、指向类对象(Class 对象)的指针、数组长度(只有数组有)组成。
MarkWord 包含对象哈希码、GC 分代年龄和锁标志位。
对齐填充字节:JVM 要求对象占的内存大小是 8 byte 的倍数,因此后面有几个字节用于把对象的大小补齐至 8 byte 的倍数。
main 方法的执行过程
java
public class Application {
public static void main(String[] args){
Person p = new Person("wmh");
p.getName();
}
}
class Person {
public string name;
public Person(String name){
this.name = name;
}
public string getName(){
return this.name;
}
}
- 编译 Application.java 得到 class 文件,运行 class 文件得到一个 JVM 进程。从类路径中找到 Application.class 的二进制文件,将类信息加载到方法区中(类的加载)
- JVM 从 main 方法开始运行
Person p = new Person("wmh");
:加载 Person 类,放入方法区。在堆中申请内存,初始化对象,执行构造方法。p.getName();
:根据 p 的引用找到 p 对象,根据 Person 类对象的方法表获得 getName 方法,并执行。
对象创建过程
- 类加载检查:查看类对象是否被加载
- 分配内存
- 初始化零值:将分配的内存置为零值
- 设置对象头:在对象头标记是哪个类的实例、对象哈希码、GC 分代年龄等
- 执行 init 方法:根据构造方法初始化对象