二、JVM运行机制

一、JVM启动流程

1.JVM初始化
  • 编写java程序:JVMTest.java(需含有main方法);
  • 通过 javac JVMTest.java 命令编译成class文件;
  • 打包成jar文件;
  • java -jar JVMTest:启动程序;
  • JVM根据java运行环境信息(jre路径)寻找jvm.cfg配置文件;


  • 根据配置文件寻找JVM.dll文件,JVM.dll文件为JVM主要实现;


  • 初始化JVM,获取JNIEnv接口,JNIEnv接口实现查找类操作;
  • 找到main方法,执行程序。

二、JVM结构

  • PC寄存器(程序计数器)

    • 存放线程当前正在执行的JVM指令地址,并始终指向下一条指令的地址;
    • 线程私有,创建线程时创建,因为多线程本质上同一个时间节点只会执行一个程序(单核),当前线程需要知道当前线程的下一条指令,pc寄存器必须独立;

  • 方法区(Method Area、jdk1.8之后也叫元空间)

    • 存放装载类信息(即类名、直接父类名、类型修饰符、直接接口的有序列表)、方法信息(名称、返回类型、参数、修饰符、操作数栈等等)、常量(String常量1.7之后移到堆)、静态变量;
    • 线程共享;
    • JVM启动时被创建,方法区大小可设置;
    • 大小决定可存放多少类,当类过多导致溢出时报错:OutOfMemoryError:Metaspace(1.8之后),OutOfMemoryError:PermGen space(1.8之前)
    • jdk1.8前常与永久代(Perm)关联,1.8之后改为元空间,舍弃永久代概念,元空间内存不在虚拟机中,使用本地内存;
    • 方法区回收
      • 回收原则:内容不再被使用才可被回收,老年代、永久代内存不足时促发full gc才会回收方法区;
      • 由于方法区回收效率低(full gc频率低),而程序中一般会有大量String,1.7之后将String存放到堆中;
      • 常量池中的常量没有被任何地方使用就可被回收;
      • 类回收(类卸载):
        • 堆中所有实例被回收,且不存在任何派生子类实例;
        • 加载该类的类加载器被回收;
        • 该类的java.lang.Class对象未在任何地方被引用,且无法在任何地方通过反射访问该类;
        • 满足以上条件的类仅仅允许被回收;
  • java堆

    • 存放类实例、数组信息;
    • 线程共享;
    • 垃圾回收的主要区域,JVM启动时创建;
    • 大小决定存放多少对象实例,溢出时报错:OutOfMemoryError:java heap space;
    • java堆分代(大部分收集器)


    • 年轻代:Eden、From Surivor(s1)、To Surivor(s0,始终为空),默认比例:8:1:1;
      • Eden:绝大部分对象都在Eden区new出来(有些对象会进行栈上分配),绝大多数的对象回收也在该区域,当一个对象大到Eden区存不下的时候会直接放到老年区;
      • 当Eden区内存不足时,进行MinorGC(年轻代垃圾回收),将Eden和From Surivor中存活的对象复制到To Surivor,然后调换To Surivor和From Surivor指针,保证下次To Surivor依旧为空;
      • 当一个对象在Survivor区被来回调换15次(次数可设置)依旧存活,则该对象会进入老年代;
      • 当Survivor区内存被占50%,较高复制次数依旧存活的对象也会进入老年代;
      • 过早提升:进行MinorGC时,如果Survivor区内存不足以存放存活对象,则会将多于对象直接放入老年代;
      • 提升失败:进行MinorGC时,Survivor和老年代内存均不足以存放存活对象,则会进行fullGC;
    • 老年代:Tenure,新生代与老年代默认比例:1:2
      • 老年代内存不足时进行OldGC(Major GC,有时Major GC也被指为FullGC);
    • 永久代(1.8后为元空间):方法区;
    • FullGC
      • 当准备触发MinorGC时,发现之前平均晋升的对象所占内存比当前老年代内存大,此时不在出发MinorGC,改为FullGC;
      • 如果存在永久代,程序需要在永久代分配内存,而永久代内存不足时,出发FullGC;
      • System.gc()触发FullGC
  • java栈

    • 线程私有;
    • 由一系列帧组成,也叫栈帧;
    • JVM只对栈进行存储、压栈、出栈操作;
    • 调用一个方法时,创建一个栈帧,并将栈帧压入栈,方法结束时将当前方法的栈帧弹出栈;
    • 栈是先进后出结构,由此可知线程当前运行的方法对应的栈帧必然在栈顶部;
    • 栈上分配:在栈上直接分配内存,方法运行完会直接回收;(后续JVM优化补充)
    • 存放内容(主要为引用)
      • 局部变量表:函数参数列表(参数中有对象时存放对象引用)、函数类局部变量,当方法为实例方法时会存放一个当前对象的引用;


    • 栈帧:函数的调用组成栈帧


    • 操作数栈:java中没有寄存器,使用操作数栈传递参数


    • 常量池指针、返回地址
  • 方法区、堆、栈交互

  • 启动程序是将JVMTest、JVMDemo类信息加载到方法区;
  • 调用main()时,调用main方法栈,栈中存放demo,指向堆中JVMDemo实例;

三、内存模型

  • 每一个线程都有一个工作内存,和主内存独立;
  • 工作内存中存放主内存的拷贝;
  • 操作原子性:
    • lock(锁):将主内存中共享变量加锁,标记为线程独占状态;
    • read(读):将主内存中变量读出;
    • load(载入):将从主内存中读出的变量载入工作线程;
    • use(使用):执行引擎使用工作内存中的变量;
    • assign(赋值):执行引擎将变量计算结果返回给工作内存;
    • store(存储):将工作线程中的变量返回给主内存;
    • write(写):将返回的变量写入主内存中的变量;
    • unlock(解锁):将主内存中的变量标记为解锁状态,解锁状态的变量可以被线程上锁;
  • 由于主内存中的共享变量未及时更新到线程中,多线程之间的共享变量值可能不一致(线程安全);
  • 可见性(共享变量)
    • 一个线程修改共享变量,其它线程能立即知道;
    • 缓存一致性协议(mesi):线程会读取变量到各自的高速缓存中,当共享变量被改变时,会马上写回主内存,其余线程会通过总线嗅探机制感知改变,并立即让缓存中的变量失效重新读取主内存中的数据;
    • volatile修饰:开启总线中的mesi,因此volatile变量每次使用时都从主内存重新读取,实际汇编实现时,会锁定缓存以保证取到的变量值是最新值;
    • synchronized修饰:会上锁变量,并在解锁前将变量写回;
    • final修饰:final修饰的为常量,初始化后不可改变,其它线程可见;
  • 指令重排
    • 在不影响单线程执行结果的前提下,为了优化性能,编译器会对机器指令优化排序;
    • as-if-serial:重排后的指令不会影响单线程执行的结果,但不能保证多线程之间的结果(线程间不可见);
    • happens-before:单线程中语义串行,比如解锁操作必须在上锁操作后;
    • 例:int a = 1,int b =2; 可重排;int a = 1,int b = a 不可重排;
  • 有序性
    • 单线程内按顺序执行,多线程间无法保证,线程a执行方法testMethodA,线程b执行testMethodB,此时可能出现b = true,但是a = 0;
 * @author xiaomu
 * @title: JVMTest
 * @description: JVM 测试
 * @date 2022/8/514:59
 */
public class JVMTest {
 
    int a = 0;
    boolean b = false;
 
    public void testMethodA(){
        a = 1;
        b = true;
    }
 
    public void testMethodB(){
        if (b){
            a = a+1;
        }
    }
}
  • 加锁(synchronized)、volatile可保证多线程间的有序性;

四、运行方式

  • 解释运行:读一句执行一句;
  • 编译运行:将字节码编译成机器码之后执行,性能与解释运行比有数量级提升。

你可能感兴趣的:(二、JVM运行机制)