JVM 相关知识

来源:互联网 发布:java 对话框 编辑:程序博客网 时间:2024/06/07 07:23

  • JVM运行机制
    • 1 JVM启动流程
    • 2 JVM基本结构
      • 21 PC寄存器
      • 22 方法区
      • 23 Java堆
      • 24 Java栈
      • 25 栈堆方法区交互
    • 3 内存模型
      • 31 volatile关键字
      • 32 几个概念
    • 4 字节码执行的两种方式编译运行与解释运行

JVM运行机制

1.1 JVM启动流程

java *(启动类,含有main方法)命令或javaw命令来启动。启动过程如下:

1、装载配置:在当前路径中寻找配置文件(根据当前路径和系统版本寻找jvm.cfg);

2、根据配置寻找JVM.dll文件(JVM的主要实现);

3、初始化JVM,获得JNIEnv接口(JNIEnv接口为JVM接口,findClass等操作通过它实现);

4、找到main方法,开始运行。

1.2 JVM基本结构

这里写图片描述

1.2.1 PC寄存器

  • 每个线程拥有一个PC寄存器
  • 在线程创建时创建
  • 指向下一条指令的地址
  • 执行本地方法时,PC的值为undefined

1.2.2 方法区

  • 保存装载的类信息(保存类的元信息,对类进行描述)
    • 类型的常量池(JDK6时,String等常量信息置于方法区,JDK7时,已经移动到了堆区)
    • 字段,方法信息
    • 方法字节码
  • 通常和永久区(Perm)关联在一起

1.2.3 Java堆

  • 和程序开发密切相关
  • 应用系统对象都保存在Java堆中
  • 所有线程共享Java堆
  • 对分代GC来说,堆也是分代的
  • GC的主要工作区间

1.2.4 Java栈

  • 1、线程私有
  • 2、栈由一系列帧组成(因此Java栈也叫作帧栈)
  • 3、帧保存一个方法的局部变量、操作数栈、常量池指针
  • 4、每一次方法调用都会创建一个帧,并压栈
  • 5、局部变量表——包含函数的参数局部变量
public class StackDemo{  //每个槽位最大能容纳32位的数据类型,long是64位,引用类型相当于是指针,占32位  public static int runStatic(int i, long l, float f,Object o,byte b){    return 0;  }  //实例方法与静态方法有一个区别是:第一个槽位是当前对象的一个引用(this)  public int runInstance(char c,short s,boolean b){    return 0;  } }

这里写图片描述

这里写图片描述
- 6、函数调用组成帧栈

public static int runStatic(int i, long l, float,Object o,byte b){  return runStatic(i,l,f,o,b);}

这里写图片描述

​ 每一块是一个帧,方法调用结束,帧就移调(这里省略了操作数栈、返回地址等)。

  • 7、操作数栈
    • Java没有寄存器,所有参数传递使用操作数栈
public static int add(int a, int b ){  int c = 0;  c = a+b;  return c;}//0:iconst_0 //0压栈//1:istore_2 //弹出int,存放于局部变量2(c变量)//2:iload_0 //把局部变量0(a变量)压栈//3:iload_1 //把局部变量1(b变量)压栈//4:iadd    //弹出2个变量,求和,结果压栈//5:istore_2    //弹出结果,存放于局部变量2//6:iload_2 //局部变量2压栈//7:ireturn //返回
  • 栈上分配
    • 堆上分配,用完之后需要手动删除(否则易导致内存泄漏);Java中局部变量在栈上分配,函数调用完成自动清理
    • 小对象(一般几十个bytes),在没有逃逸(对象分配出来除了在该线程上用,还要在别的线程上用)的情况下,可以直接分配在栈上
    • 直接分配在栈上,可以自动回收,减轻GC压力
    • 大对象或者逃逸对象无法栈上分配(因为栈是线程私有的,有逃逸的时候,对象会被别的线程共享,所以该对象不能分配在栈上)
public class OnStackTest{  public static void alloc(){    byte[] b = new byte[2];    b[0] = 1;  }  public static void main(String[] args){    long b = System.currentTimeMillis();    for(int i = 0;i<100000000;i++){      alloc();    }    long e = System.currentTimeMillis();    System.out.println(e-b);  }}//以下参数运行不会有GC//-server -Xmx10m -Xms10m//-XX:+DoescapeAnalysis -XX:+PrintGC//以下参数运行会发生GC,且是在堆上分配//-server -Xmx10m -Xms10m//-XX:-DoescapeAnalysis -XX:+PrintGC 

1.2.5 栈、堆、方法区交互

这里写图片描述

public   class  AppMain {         //运行时, jvm 把appmain的信息都放入方法区    public   static   void  main(String[] args){      //main 方法本身放入方法区。      Sample test1 = new  Sample( " 测试1 " );        //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面       Sample test2 = new  Sample( " 测试2 " );      test1.printName(); test2.printName();    } }public   class  Sample {       //运行时, jvm 把appmain的信息都放入方法区   private  name;      //new Sample实例后, name 引用放入栈区里,  name 对象放入堆里  public  Sample(String name) {    this .name = name;   }   //print方法本身放入 方法区里。  public   void  printName(){    System.out.println(name);   } }

1.3 内存模型

这里写图片描述

  • 每一个线程有一个工作内存,和主存独立
  • 工作内存存放主存中变量的值的拷贝

1、当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。

2、每一个操作都是原子的,即执行期间不会被中断。

3、对于普通变量,一个线程中更新的值,不能马上反应在其他线程中。(线程直接读取和存储的是线程的工作内存,工作内存到主存是有时差的)

4、如果需要在其他线程中立即可见,需要使用 volatile 关键字。(直接到主存中拿数据)

这里写图片描述

1.3.1 volatile关键字

volatile不能代替锁;

一般认为volatile比锁性能好(不绝对);

选择使用volatile的条件是:语义是否满足应用。

public class VolatileStopThread extends Thread{    private volatile boolean stop = false;    public void stopMe(){        stop=true;    }    public void run(){        int i=0;        while(!stop){            i++;        }        System.out.println("Stop thread");    }    public static void main(String args[]) throws InterruptedException{        VolatileStopThread t=new VolatileStopThread();        t.start();        Thread.sleep(1000);        t.stopMe();        Thread.sleep(1000);    }}

1.3.2 几个概念

  • 可见性:一个线程修改了变量,其他线程可以立即知道
  • 保证可见性的方法
    • volatile
    • synchronized (unlock之前,写变量值回主存)
    • final(一旦初始化完成,其他线程就可见)
  • 有序性
    • 在本线程内,操作都是有序的
    • 在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)
  • 指令重排
    • 线程内串行语义
    • 写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
    • 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
    • 读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
    • 以上语句不可重排
    • 编译器不考虑多线程间的语义
    • 可重排: a=1;b=2;

Eg.

1、指令重排——破坏线程间的有序性

/*    线程A首先执行writer()方法    线程B线程接着执行reader()方法    线程B在int i=a+1 时不一定能看到a已经被赋值为1,因为在writer中,两句话顺序可能打乱    线程A    flag=true    a=1    线程B    flag=true(此时a=0)*/class OrderExample {    int a = 0;    boolean flag = false;    public void writer() {        a = 1;                           flag = true;               }    public void reader() {        if (flag) {                            int i =  a +1;                  //……        }    }}

2、指令重排——保证有序性的方法

/*    同步后,即使做了writer重排,因为互斥的缘故,reader 线程看writer线程也是顺序执行的。    线程A    flag=true    a=1    线程B    flag=true(此时a=1)*/class OrderExample {    int a = 0;    boolean flag = false;    public synchronized void writer() {        a = 1;                           flag = true;               }    public synchronized void reader() {        if (flag) {                            int i =  a +1;                  //……        }    }}
  • 指令重排的基本原则
    • 程序顺序原则:一个线程内保证语义的串行性
    • volatile规则:volatile变量的写,先发生于读
    • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
    • 传递性:A先于B,B先于C 那么A必然先于C
    • 线程的start方法先于它的每一个动作
    • 线程的所有操作先于线程的终结(Thread.join())
    • 线程的中断(interrupt())先于被中断线程的代码
    • 对象的构造函数执行结束先于finalize()方法

1.4 字节码执行的两种方式——编译运行与解释运行

  • 解释执行
    • 解释执行以解释方式运行字节码
    • 解释执行的意思是:读一句执行一句
  • 编译运行(JIT)
    • 将字节码编译成机器码
    • 直接执行机器码
    • 运行时编译
    • 编译后性能有数量级的提升