Java虚拟机

81 阅读9分钟

1. 类加载机制

1.1 定义

类的加载指的是将类的class文件中的二进制数据读入到内存中,并对数据进行校验、解析和初始化最终形成可被虚拟机直接使用的Java类型

1.2 类的生命周期

类的生命周期包括:加载、连接、初始化、使用和卸载,其中前三步是类加载的过程 image.png

  • 加载:通过类的全限定类名(包名+类名)来加载,在内存的Java堆中创建一个java.lang.Class类的对象
  • 连接:包含验证、准备以及解析
    • 验证:通过文件格式校验、元数据验证、字节码验证等验证方式生成的class文件
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:将符号引用转为直接引用(指针引用)
  • 初始化:为类的静态变量赋予正确的初始值
  • 使用:new出对象所在程序中使用
  • 卸载:执行垃圾回收

1.3 类初始化时机

只有当对类的主动使用的时候才会导致类的初始化,主动使用主要包括以下几种方式

  • 创建类的实例(new的方式)
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 初始化某个类的子类,则其父类也会被初始化
  • 使用反射(如 Class.forName(“com.btpj.Demo”))
  • Java虚拟机启动时被标明为启动类的类(JavaTest)、直接使用 java.exe命令来运行某个主类

1.4. 类加载器

类加载器主要是用来实现通过类的全限定类名获取该类的二进制字节流的代码块的功能

public class JavaClassLoader {

    public static void main(String[] args) {
        ClassLoader classLoader = JavaClassLoader.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
        System.out.println(classLoader.getParent().getParent());
    }
}

执行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

可以看到至少包含了AppClassLoader(应用程序类加载器)ExtClassLoader(扩展类加载器),其实还有一个更上层的加载器BootstrapClassLoader(启动类加载器),只是它是由C/C++实现的,无法被java程序直接引用,所以返回null;他们之间的关系如下

af2d913e21febb8fcc4d3a1a408c4a80_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

  • 启动类加载器(Bootstrap ClassLoader) :用来加载java核心类库,无法被java程序直接引用,C/C++实现
  • 扩展类加载器(Extensions ClassLoader) :它用来加载Java的扩展库,Java虚拟机的 实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载Java类。
  • 应用程序类加载器(Application ClassLoader) :它根据 Java 应用的类路径(CLASSPATH)来加载Java类。如果程序没有自定义过加载器,Java应用的类一般都是由它来完成加载的。也可以通过ClassLoader.getSystemClassLoader()来获取它。
  • 自定义类加载器:通过继承java.lang.ClassLoader类的方式实现(例如从网络传输Java类的字节码,通过自定义类加载器对字节码进行加密处理)

1.4.1 Android中主要的类加载器

  • BootClassLoader:Android系统启动时会使用BootClassLoader来预加载常用类(与Java中的Bootstrap ClassLoader不同的是BootClassLoader是由Java实现的)
  • DexClassLoader:加载dex文件以及包含dex的压缩文件(apk和jar文件)
  • PathClassLoader:加载系统类和应用程序的类

1.5 类加载的方式

  • 命令行启动应用时由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载
public class LoaderTest {

    static {
        System.out.println("我是LoaderTest类的静态代码块,我被执行了");
    }
}
public static void main(String[] args) throws ClassNotFoundException {
    ClassLoader classLoader = LoaderDemo.class.getClassLoader();
    System.out.println(classLoader);

    // 1、通过ClassLoader.loadClass()来加载类,静态代码块不会执行
    classLoader.loadClass("class_loader.LoaderTest");

    System.out.println("-------------------------");

    // 2、使用Class.forName()来加载类,默认会执行静态代码块
    Class.forName("class_loader.LoaderTest");

    System.out.println("-------------------------");

    // 3、使用Class.forName()来加载类,但指定了classLoader,便不会执行静态代码块
    Class.forName("class_loader.LoaderTest", false, classLoader);
}

运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
-------------------------
我是LoaderTest类的静态代码块,我被执行了
-------------------------
  • 由此可见,通过ClassLoader.loadClass()的方式不会执行加载类的静态代码块,而默认情况下通过Class.forName()的方式则会执行加载类的静态代码块,除非调用Class.forName(name,initialize,loader)的方式指定类加载器才不会执行

1.6 双亲委派模型机制

  • 描述:当一个类收到了类加载请求时,不会自己先去加载这个类,而是将请求委派给父类去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
  • 流程
    • 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
    • 2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
    • 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载
    • 4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常 ClassNotFoundException
  • 意义防止内存中出现多份同样的字节码从而保证Java程序安全稳定地运行

3. JVM内存结构

ae9716f87511b8987124fe6c0aecced5_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.jpg

  • JVM分为堆区、栈区以及方法区,初始化的对象实例放在堆里,引用放在栈里,class类信息、常量(static常量和static变量)等放在方法区。其中堆和方法区是所有线程共享的内存区域;而java栈、本地方法栈和程序计数器是线程私有的内存区域。

3.1 内存模型

  • Java堆:所有线程共享的运行时内存区域,是Java虚拟机所管理的内存中最大的一块。主要存储new出来的对象实例、成员变量,使用完毕后等待gc清理(从内存回收的角度可粗略的分为新生代和老年代,再细致一点的有Eden空间、From Survivor空间、To Survivor空间等)。
  • 程序计数器:又名PC寄存器(无OOM区域),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器(Native方法直接指向空undefined),每个线程私有,确保多线程下线程切换时各线程的正常运行
  • Java虚拟机栈:与程序计数器一样,他也是线程私有的;一个Java虚拟机栈包含了多个栈帧,一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法执行完成,这个栈帧就从Java栈中弹出。我们平常所说的栈内存(Stack)指的就是Java虚拟机栈
  • 本地方法栈:与Java虚拟机栈类似,只不过本地方法栈是用来支持Native方法服务,(HotSpot VM将本地方法栈和Java虚拟机栈合二为一)
  • 方法区: 所有线程共享的运行时内存区域,存储已经被Java虚拟机加载的类的结构信息,包括:运行时常量池、字段和方法信息、静态变量等数据(方法区中包含了运行时常量池)
  • 注意
    • JDK1.7及版本之后将运行时常量池从方法区中移出,在Java堆中开辟了一块区域存放运行时常量池
    • JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)

3.2 对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值,对象便进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,小于则检查HandlePromotionFailure设置,true则只进行Monitor GC,false便进行Full GC。

4. 垃圾回收与GC算法

JVM会自动管理和释放内存,这里面就涉及到垃圾标记算法和垃圾清除算法

4.1 垃圾标记算法

  • 引用计数算法:每个对象都有一个引用计数器,当对象在某处被引用的时候,它的引用计数器就加1,引用失效时就减1,当引用计数器中的值变为0,则表示该对象未被使用就成了垃圾
    • 注意:主流的Java虚拟机没有选择引用计数算法,主要原因是引用计数算法没有解决对象之间相互循环引用的问题
  • 可达性分析(根搜索)算法:选定一些对象作为GC Roots,并组成根对象集合,然后从这些作为GC Roots的对象作为起始点,向下进行搜索,如果目标对象到GC Roots是连接着的,我们则称该目标对象是可达的,如果目标对象不可达则说明目标对象是可以被回收的对象,下面是作为GC Roots的主要对象
    • Java虚拟机栈中的引用的对象
    • 本地方法栈中JNI引用的对象
    • 方法区中运行时常量池引用的对象
    • 方法区中静态属性引用的对象
    • 运行中的线程
    • 由引导类加载器加载的对象
    • GC控制的对象

4.2 GC回收算法

  • 标记-清除算法(Mark-Sweep)
    • 在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法,有两个不足:
      • 标记和清除阶段的效率不高
      • 清除后会产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。
  • 复制算法(Copying)
    • 复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制算法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以会分成1块大内存Eden和两块小内存Survivor(大概是8:1:1),每次使用1块大内存和1块小内存,当回收时将2块内存中存活的对象赋值到另一块小内存中,然后清理剩下的。
  • 标记-整理算法(Mark-Compact)
    • 标记-整理算法和复制算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代
  • 分代收集(Generational Collection)
    • 分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法

参考:JVM系列文章