这个是自己在学习JVM结构中的笔记,非原创。

运行时数据区中的程序计数器、虚拟机栈和本地方法栈是线程私有的,堆和方法区是线程共享的。

1 运行时数据区

1.1 程序计数器(Program Counter Register)

程序计数器(Program Counter (PC))是在电脑处理器中的一个寄存器,用来指示电脑下一步要运行的指令序列。依照特定机器的细节而不同,他可能是保存着正在被运行的指令,也可能是下一个要运行指令的地址。程序计数器在每个指令周期会自动地增加,所以指令会正常地从寄存器中连续地被取出。某些指令,像是跳跃和子程序调用,会中断程序执行的序列,将新的数值内容存放到程序计数器中。

对于一个运行中的Java程序而言,其中的每一个线程都有自己的PC,在线程启动时创建,大小是一个字长。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

1.2 虚拟机栈(VM Stack)&本地方法栈

虚拟机栈描述的是Java方法执行的内存模型,属于线程私有,其生命周期与线程生命周期相同。每个方法在执行的同时都会创建一个帧栈(Stack Frame)用于存储局部变量表(存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型)、操作数栈、动态链表、方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个帧栈在虚拟机中入栈到出栈的过程。

JVM规范为了允许native代码可以调用Java代码,以及允许Java代码调用native方法,还规定每个Java线程拥有自己的独立的本地方法栈(native方法栈)。

虚拟机栈和本地方法栈都是JVM规范所规定的概念上的东西,并不是说具体的JVM实现真的要给每个Java线程开两个独立的栈。以Oracle JDK / OpenJDK的HotSpot VM为例,它使用所谓的“mixed stack”——在同一个调用栈里存放Java方法的栈帧与native方法的栈帧,所以每个Java线程其实只有一个调用栈,融合了JVM规范的JVM栈与native方法栈两个概念。

1.3 堆(heap)

Java 虚拟机具有一个堆(Heap),堆是运行时数据区域,所有类实例和数组的内存均从此处分配,在 Java 虚拟机启动时创建的,为所有线程共享。

随着JIT编译器的发展与逃逸技术逐渐成熟,栈上分配、标量替换优化技术会导致一些微妙的变化,所有对象都分配在堆上也逐渐变得不那么“绝对”了。

堆是JVM GC主要作用区域,详情参见JVM 垃圾回收算法及垃圾回收器

1.4 方法区(Method Area)

用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。Java虚拟机规范把方法区描述为堆的一个逻辑部分,很多人会将其成为“永久代”(Permanent Generation),但本质上两者并不等价,仅仅因为HotSpot虚拟机的设计团队选择将GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的GC可以像管理Java堆一样管理这部分内存。
Java 8 使用Metaspace取代了方法区的概念,参见Java 8: From PermGen to Metaspace

多谢 @wong 网友的提醒。

1.5 运行时常量池( Run-Time Constant Pool)

运行时常量池是方法区的一部分。
用于存储class文件的常量池(constant pool table),包含编译期生成的各种字面量和符号引用。

2 执行引擎

Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观,在不同的实现中可能有解释执行和编译执行两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。

执行引擎是个非常核心部件,不在这里深入。

3 类加载子系统

JVM中类加载器有两种:

  • 启动类加载器(Bootstrap ClassLoader)

    是JVM实现的一部分。这个类负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时如果需要委派给引导类加载器那直接使用null代替即可。

  • 其他的类加载器(ClassLoader Objects)

    这些类加载器对象是运行中程序的一部分,都由Java实现,独立于虚拟机外部,并且全部集成自java.lang.ClassLoader。
    这部分类加载器由可以细分以下几种:

    • 扩展类加载器(Extension ClassLoader)

      这个加载器是由sun.misc.Launcher$ExtClassLoader实现,负载加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

    • 应用程序加载器(Application ClassLoader)

       这个类加载器是由sun.misc.Laucher$APP-ClassLoader实现,是ClassLoader中的getSystemClassLoader()方法的返回值,负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。

类加载器之前的关系,称为双亲委派模型(Patterns Delegation Model)

下面是java.lang.ClassLoader中loadClass的实现:

     protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    

4 本地方法接口(Java Native Interface)

Java 虚拟机(VM) 内部运行的Java 代码通过JNI与用其它编程语言(如 C、C+ + 和汇编语言)编写的应用程序和库进行互操作。

5 JVM生命周期

以HotSpot为例:

启动

  • 解析命令行参数
  • 如果命令行中未明确指定,则建立Java堆和JIT编译器类型(Client或Server)
  • 建立LD_LIBRARY_PATH and CLASSPATH等环境参数
  • 寻找Java Main函数。如果命令行中未指定,则在Jar包中查找
  • 用标准的java Native接口方法:JNI_CreateJavaVM在一个新创建的非原生的线程中创建HotSpotVM(非原生的是为了可以改变线程栈大小)
  • HotSpotVM创建并初始化完成之后,Main-Class就会被加载。
  • HotSpotVM使用java本地接口方法CallStaticVoidMethod来运行java的main方法

终止

  • 等待直到只有一个非守护进程为止
  • 调用java方法:java.lang.Shutdown.shutdown(),这个方法会触发java层面的关闭钩子并且如果finalization-on-exit为true的话调用java对象的finalizers
  • 调用HostSpot VM层面的关闭钩子(通过JVM_OnExit()方法注册),终止以下HotSpot VM线程:profiler,stat sampler,watcher,和gc线程。
  • 调用HotSpot的方法JavaThread::exit()来释放Jva本地接口的处理块,删除守护页面,从已知的线程列表中删除当前线程。从现在开始HotSpot VM不能执行任何的Java Code
  • 终止HotSpot VM线程。这会导致剩下的HotSpot VM线程都到达一个安全点并且终止JIT线程。
  • 禁止在java本地接口,HotSp VM和JVMTI上的跟踪活动。
  • 为那些在本地代码中运行的线程设置HotSpot 的“Vvm exited"标记
  • 删除当前线程
  • 删除所有的输入输出流。
  • 返回到调用者

References

Inside the Java 2 Virtual Machine
Java虚拟机的堆、栈、堆栈如何去理解
Chapter 2. The Structure of the Java Virtual Machine
Java Performance