JVM与内存机制
JVM
JVM是Java Virtual Machine的缩写。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
类的加载机制
Java文件从编码到最终执行,一般主要包括两个过程:编译和运行。其中编译就是把写好的".java"拓展名类文件,通过javac命令编译成字节码,也就是我们常说的".class"文件。当需要使用某个类时,就把".class"文件交给Java虚拟机来执行。虚拟机通过类加载器把".class"文件加载进内存,并创建对应的class对象,将class文件加载到虚拟机内存的过程称为类的加载。如下图:

在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外,这几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载
- 加载:类加载过程的第一个阶段。通过一个类的完全限定查找此类字节码文件,即.class文件;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在Java堆中生成一个代表这个类的
java.lang.Class
对象,作为对方法区中这些数据的访问入口。该过程可以使用系统提供的类加载器来完成,也可以自定义类加载器来完成。
连接
- 验证:确保Class文件的字节流中包含信息符合当前虚拟机要求,且不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备:为类中静态变量(即static修饰的变量)分配内存并将其初始化为默认值(如0、0L、null、false等),这些内存都在方法区中分配。不包含用final修饰的static,因为final在编译的时候分配了。如果类字段的字段属性表中存在 ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
如:public static final int value=3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机根据 ConstantValue的设置将value赋值为3。 - 解析:将常量池中的符号引用替换为直接引用的过程。解析针对类或接口,字段,类方法,接口方法,方法类型,方法句柄,和调用点限定符7种符号引用进行。直接引用是指直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 初始化:类加载最后阶段,为类的静态变量赋予正确的初始值。若这个类还没有被加载和连接,则程序先加载并连接该类;若该类的直接父类还没有被初始化,则先初始化其直接父类;若类中有初始化语句,则系统依次执行这些初始化语句(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
结束生命周期
Java虚拟机将结束生命周期的几种情况:执行了 System.exit()
方法;程序正常执行结束;程序在执行过程中遇到了异常或错误而异常终止;由于操作系统出现错误而导致Java虚拟机进程终止。
类加载器
类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class
对象实例,属于类的加载阶段。类加载器可以大致分为3类:启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也叫应用程序类加载器)。三种类加载器的父子关系如图所示:

这里不再展开说明,可以参考以下链接
深入理解Java类加载器(ClassLoader)
jvm系列(一):java类的加载机制
内存结构
大致了解了Java类的加载过程之后,我们可以通过下图来进一步了解JVM和系统调用之间的关系。运行时数据区即JVM内存区,主要包括堆、方法区、Java栈、本地方法栈、程序计数器五部分。

- 堆(Heap):线程共享
用于存放对象实例和数组,是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。Java堆是垃圾回收的主要场所,因此很多时候也被称做“GC堆”。由于现在收集器基本都是采用的分代收集算法,所以Java堆从结构上可以细分为新生代和老年代。而新生代又可以分为Eden 空间、From Survivor 空间、To Survivor 空间。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,通过以下参数来控制:
-Xms
设置堆的最小空间大小。
-Xmx
设置堆的最大空间大小。
-XX:NewSize
设置新生代最小空间大小。
-XX:MaxNewSize
设置新生代最大空间大小。
若堆中没有内存来完成实例分配,且无法再扩展时,将会抛出OutOfMemoryError 异常。

- 方法区(Method Area):线程共享
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区中的信息一般需要长期存在,而且它是堆的逻辑分区。很多人把方法区称为“永久代”(Permanent Generation),对此区域会涉及很少的垃圾回收,其内存回收目标主要是针对常量池的回收和对类型的卸载。
和堆一样,方法区允许固定大小,也允许可扩展的大小,此外还允许不实现垃圾回收。其控制参数有:
-XX:PermSize
设置最小空间
-XX:MaxPermSize
设置最大空间
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池
方法区中的常量存储在运行时常量池中。
当某个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且
在运行期间,可以向常量池中添加新的常量。如:
String类的intern()方法就能在运行期间向常量池中添加字符串常量。
当运行时常量池中的某些常量没有被对象引用,也没有被变量引用,就需要垃圾收集器回收 - java虚拟机栈(JVM Stacks):线程私有
虚拟机栈是描述Java方法执行过程的内存模型。Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,包括局部变量表(存放基本数据类型变量、引用类型的变量、returnAddress类型的变量)、操作数栈、动态链接、方法出口信息等。它的生命周期与线程相同。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
控制参数:
-Xss
控制每个线程栈的大小
Java虚拟机栈会出现两种异常:
-StackOverFlowError:异常线程请求的栈深度大于虚拟机所允许的深度时抛出;
-OutOfMemoryError:虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出。 - 本地方法栈(Native Method Stacks):线程私有
本地方法栈与虚拟机栈所发挥的作用非常相似,但本地方法区是本地方法运行的内存模型,为虚拟机使用到的Native方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。 - 程序计数器
是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。