浅谈JVM(1)

一、JVM的内存模型

JDK 1.8 中,JVM的内存模型主要分为五个部分:堆、原空间、虚拟机栈、本地方法栈和程序计数器
JVM的内存模型图
这几个部分可以分成线程共享线程私有 两类,线程共享的内存可以供所有线程共同使用且访问;线程私有的内存则会随着线程的创建而分配,随着线程的销毁而回收。

  • 线程共享
    • 堆 (Heap):
      ​ 堆是JVM中最大的一块内存,在JVM启动的时候创建,专门用来存放数组对象实例 。堆中还包含了字符串常量池 ,用于存储字符串字面量和常量。
      ​ 从内存回收的角度,堆中又分为新生代(占1/3) 老年代(占2/3),新生代又分为1个Eden区 和2个Survivor区 (From & To)。
      ​ 当堆中没有内存分配给对象实例 ,并且堆再也无法拓展内存 的时候,就会出现OOM异常 (OutOfMemoryError)。
    • 元空间 (MetaSpace):
      ​ 元空间用来存储类元信息常量方法字节码 等。元空间包含运行时常量池 ,用于存储编译时期生成的各种字面量和符号引用。
      ​ 在JDK 1.7及之前,元空间被称为方法区 ,并且它的内存分配在JVM内部;在JDK 1.8之后,元空间在本地内存 中分配。
      ​ 元空间可以不实现垃圾收集,内存不足 时会抛出OOM异常。
  • 线程私有
    • 虚拟机栈
      每个线程都会有一个虚拟机栈,生命周期与线程相同
      它存储方法调用产生的栈帧 ,包括局部变量操作数栈方法返回地址 等。
      递归层数过多时可能会产生StackOverflowError ,空间不足时会产生OOM异常
    • 本地方法栈
      ​ 与虚拟机栈类似,专门用于存储本地方法 (Native Method)的调用信息。
      ​ 可能会抛出 StackOverflowErrorOutOfMemoryError
    • 程序计数器
      ​ 用于存储当前线程正在执行的字节码指令地址 ,如果是Native方法,则计数器值为null
      ​ 是JVM中最小的一块内存区域,不会出现OOM

二、对象创建过程

在Java中创建对象主要分为五步:

  1. 类加载检查
    当虚拟机遇到new指令时,会检查这个指令的参数是否能在常量池 中找到对到一个类的符号引用 ,并且检查这个类是否被加载、解析和初始化过 。如果没有,则会执行相应的类加载过程。
  2. 内存分配
    ​ JVM会为对象分配内存空间。对象的所需内存大小在类加载过程中就可以确定,因此分配内存就是在虚拟机划定一块连续的空间,主要有两种方式:
    • 指针碰撞:如果堆中的空闲内存是规整的 (空闲的内存都紧密排列),JVM就会通过移动指针 来分配内存。
    • 空闲列表:如果堆中的空闲内存是碎片化的 ,JVM就会维护一个空闲列表,记录可用的内存块,并分配合适的区域。
  3. 零值初始化
    ​ 分配完空间之后,JVM会对分配的内存进行初始化,将所有的字段都设为零值 (引用值为null,boolean为false,数字为0)。这一步保证了对象的字段赋值之前 具有默认值,避免未初始化的变量被访问
  4. 设置对象头
    ​ 对象头包括Mark Wordklass pointer数组长度 等。Mark Word用于存储对象的哈希码 、分代GC年龄、锁的占有情况 等。klass pointer指向对象所属类的元数据的地址
  5. 执行构造方法
    ​ 用方法来进行对象的初始化。构造方法会根据代码逻辑对对象进行赋值 ,同时调用父类的构造方法完成继承链的初始化

三、类加载过程

JVM进行类加载的过程主要分5步:加载链接验证准备解析 )、初始化使用卸载
类加载过程

  1. 加载
    加载过程也分为三步:
    • 首先会根据被加载类的全限定名称 (包名、类名)去获取相应的.class文件的二进制字节流
    • 然后会将二进制文件中的静态数据结构转化成方法区(元空间)的运行时数据结构,即将类的元信息 (字段、方法、父类等)加载到方法区
    • 最后会在堆中创建一个Class类的对象 ,作为改类的访问入口,也可以供反射调用
  2. 验证
    验证需要验证四个方面:
    • 文件格式:验证字节码文件 符合.class文件 的格式规范
    • 类元信息:验证该类的元信息是否符合语法要求,如是否继承了final修饰的类父类是否存在
    • 字节码:验证字节码指令是否符合语法要求,如访问越界类型转化错误
    • 符号引用:验证符号引用 是否能转化为直接引用
  3. 准备
    JVM会为该类的静态变量开辟内存空间,并赋上零值
  4. 解析
    将类中符号引用 转化成直接引用
  5. 初始化
    JVM会为类中的静态变量赋上具体的值,同时执行静态代码块中的内容。
  6. 使用
    使用类或者对象。
  7. 卸载
    类的卸载非常苛刻,需要同时满足三个条件:
    • 该类的所有实例 都已经被回收
    • 该类的类加载器 也被回收
    • 该类的java.lang.Class对象没有被引用

四、类加载的双亲委派原则

​ 类加载的双亲委派原则,就是指当一个类加载器需要加载某个类时,首先会请求自己的父类加载器加载此类,判断缓存是否命中。如果委托到启动类加载器还没有命中,那么加载该类的请求就会逐层向子类加载器委托,知道该类属于某个子类加载器的加载范畴,才会开始启动类加载过程。
双亲委派示意图

类加载器的层次结构

  1. 启动类加载器 :它负责加载JVM中的核心类库,位于最顶层,通常由本地方法实现
  2. 拓展类加载器 :它负责加载/lib/ext路径下的类
  3. 应用程序类加载器 :它负责加载ClassPath路径下的类
  4. 自定义类加载器 :由用户自行实现.

双亲委派的优势

  1. 唯一性 :避免多个类加载器加载同一个类
  2. 安全性 :避免恶性篡改的类替代核心库中的类
  3. 模块化管理 :不同的模块可用交给不同的类加载器管理