Java虚拟机

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区(S0S1)。新对象存放在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