Java虚拟机,即Java Virtual Machine,简称JVM。Java源代码编译为.class字节码后,由JVM运行。它解决了Java程序的跨平台问题,做到“一次编译,到处运行”。
类加载机制
类加载过程
1. 加载
通过类全名获取字节码.class文件,加载进JVM,将字节码转成方法区中的运行时数据结构,在堆中生成Class对象。
为了支持热加载(插件化、热修复),类加载不一定从磁盘,有多种来源:
- 本地 class 文件
- jar / dex(Android)
- 网络
- 动态生成(代理)
2. 验证
校验字节码是否符合格式且安全。
如是否实现接口、类型转换是否正确、引用的类是否存在、方法是否能访问……
JVM是可以执行外部代码的(比如下载的class),因此需要安全性验证。
3. 准备
给静态变量分配内存,赋类型默认值。
注意此时还没执行代码,因此对于 static int a = 10,此时只是为其分配空间,a = 0。
4. 解析
将常量池内的符号引用转化为直接引用。可以延迟绑定(懒解析)提高启动速度。
5. 初始化
对类进行初始化,执行类构造器。为静态变量赋初值、执行静态代码块。
类加载过程是先只走前面几步,先加载类,这一步初始化只有在触发了初始化的条件才会执行:
- new对象
- 访问静态变量(非final)
- 调用静态方法
- 反射
- 子类初始化 → 父类先初始化
双亲委派机制
核心思想是,当类加载器要加载一个类时,会先将加载任务委托给父类加载器,这个过程会一直递归直到最顶层的启动类加载器。只有当父类加载器无法完成加载任务时,才由子类加载器执行。
类加载器的种类包括:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用类加载器(Application ClassLoader)
- 自定义类加载器
为什么需要双亲委派机制?
- 避免类的重复加载
- 防止核心API被注入篡改,已经被加载过的类不会重复加载,避免了被篡改注入恶意代码的类被加载
内存模型
程序计数器
栈空间
本地方法栈
堆空间
方法区
用于存储类信息、常量池。在JDK8之前把方法区由永久代实现,永久代在堆空间中。JDK8开始方法区改为由元空间实现,元空间在本地内存而不是堆中。
永久代改为元空间的原因是:永久代在堆中,大小固定,类加载太多或者常量池太大容易导致OOM。
垃圾回收机制
JVM堆空间分为新生代和老年代,其中新生代又分为一个Eden区和两个Survivor区(S0和S1)。新对象存放在Eden区。
Q:新时代对象经过多少次回收会被移入老年代?
A:TODO
如何判断对象该不该回收
可达性分析
从GC Roots出发,看对象是否可达,可达则保留,不可达说明是垃圾。
GC Roots 包括:
- 虚拟机栈中的引用(方法参数、局部变量)
- 方法区中的静态变量
- 方法区中的常量
- JNI引用(Native)
引用计数法
为每个对象记录其被引用的次数,当被引用次数为0时就回收。但若出现循环引用(如对象A中有成员变量对象B,对象B中也有成员变量对象A),则它们永远无法被回收。
因此这种方法基本不用。
垃圾回收算法
标记清除法
找出要回收的垃圾对象并标记,直接进行回收清除。方法简单粗暴,但是这样清除完垃圾对象后,堆空间中会出现碎片空间且不连续,难以被使用。
复制法
每次只使用一半的堆空间,进行GC时将A区需要保留的对象复制到空闲的B区,再清空原来的A区。
该方法不会产生碎片空间且速度较快,但只能使用一半的堆空间。
因此新生代使用该方法,因为新生代的对象大多短命,每次只需复制少量对象即可。
标记整理法
确定有效对象后进行标记,并将标记对象移动到堆空间的一端。
这种方法优点是不会产生碎片空间,且不像复制法只能用一半堆空间,但缺点是移动成本高速度较慢。
因此老年代使用该方法,因为老年代对象较少,且存活时间久,移动代价不像新生代那么高。
三种GC方式
- Minor GC:新生代GC,触发较为频繁,GC过程快速
- Major GC:老年代GC,不一定Full
- Full GC:整个堆 + 方法区进行GC,速度很慢,
垃圾回收器
四种引用类型
运行时数据区
TODO
JVM调优
TODO