JVM原理

  1. JVM虚拟机
  • Classic VM:JDK1 与 JDK2 的官方默认虚拟机,世界第一种 java 虚拟机。通过纯解释器执行 Java 代码,即时编译器只能通过外挂的形式存在,并且不能与解释器一起运行。(那个时候的 Java 很慢)
  • Exact VM:Sun 公司为了解决 Classic VM 的效率问题而计划研发的,但只在 Solaris 系统上发布过,后来就被 HotSpot 取代了因其使用准确式内存管理而闻名。(知道内存中某一块区域存放的是哪一种数据结构,有利于垃圾收集)
  • HotSpot:JDK3 之后的官方默认虚拟机,同样有准确式内存管理,因其热点探测技术而闻名。(知道哪一段代码经常执行,将其编译成机器代码,提高运行效率)
  • JRockit:BEA 公司研发的,对服务端高度优化的虚拟机,其垃圾收集机制和 MissionControl 服务套件,一直处于 Java 虚拟机的领先水平。Oracle 在 2008 年收购 BEA 公司,在 2009 年收购 Sun 公司。Oracle 计划从 JDK8 开始将两种虚拟机融合成一种。(JDK8 的 HotSpot 已经放弃用永久代来实现方法区,转而使用元空间)
  • J9:IBM 公司研发,主要为了在自家研发的产品上跑 Java 程序。
  1. jvm体系结构:
  • 类装载器ClassLoader:用来装载.class文件
  • 执行引擎:执行字节码,或者执行本地方法
  • 运行时数据区:方法区、堆、Java栈、程序计数器、本地方法栈
  1. JVM生命周期与实例介绍
  • JVM实例和JVM执行引擎实例:
  • JVM实例对应了一个独立运行的java程序——进程级别
    一个运行时的Java虚拟机(JVM)负责运行一个Java程序。
    当启动一个Java程序时,一个虚拟机实例诞生;当程序关闭退出,这个虚拟机实例也就随之消亡。
    如果在同一台计算机上同时运行多个Java程序,将得到多个Java虚拟机实例,每个Java程序都运行于它自己的Java虚拟机实例中。
  • JVM执行引擎实例则对应了属于运行程序的线程——线程级别
  • JVM的生命周期:
  • (1)JVM实例的诞生
    当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
  • (2)JVM实例的运行
    main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由 JVM自己使用,java程序也可以标明自己创建的线程是守护线程。
  • (3)JVM实例的消亡
    当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。
  1. JVM架构
    1. 类加载器
  • 负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
    JVM在运行时会产生三个ClassLoader:根装载器,ExtClassLoader(扩展类装载器)和AppClassLoader(应用类加载器):
  • 根类加载器:这个类加载器负责将存放在<JAVA_HOME>\lib目录中的。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。如图可以查看根类加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的jdk中提供的核心jar包路径:
  • JVM原理分析_jvm

  • 拓展类加载器:这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器,父类加载器为null。
  • 系统类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,父类加载器为ExtClassLoader,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH还将变量所指定的JAR包和类路径。
  • 类加载器加载Class.
  • 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
  • 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
  • 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
  • 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
  • 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
  • 从文件中载入Class,成功后跳至第8步。
  • 抛出ClassNotFountException异常。
  • 返回对应的java.lang.Class对象
  • 双亲委派机制
  • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的 父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
  • 当JVM加载一个类的时候,下层的加载器会将任务给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查之后,按照相反的顺序进行加载。如果Bootstrap加载器不到这个类,则往下委托,直到找到这个类。一个类可以被不同的类加载器加载。
    可见性限制:下层的加载器能够看到上层加载器中的类,反之则不行,委派只能从下到上。
    不允许卸载类:类加载器可以加载一个类,但不能够卸载一个类。但是类加载器可以被创建或者删除。

JVM原理分析_java_02


5. 内存区域方法区

  • 各个线程共享的区域,存放类元信息、常量、静态变量, 有时候也称为永久代(Permanent Generation)/元空间,在方法区中,存储了每个类的元信息包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等。当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,在这里进行的GC主要是方法区里的常量池和类型的卸载。当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
  • 类元信息包括:
  • 类型信息
  • 类型的常量池
  • 字段信息
  • 方法信息
  • 类变量
  • 指向类加载器的引用
  • 指向Class实例的引用
  • 方法表
  • 在方法区中有一个非常重要的部分就是运行时常量池,用于存放静态编译产生的字面量和符号引用。运行时生成的常量也会存在这个常量池中,比如String的intern方法。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。