JVM 相关知识

Posted Lee_Sung

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM 相关知识相关的知识,希望对你有一定的参考价值。

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)
    • 将字节码编译成机器码
    • 直接执行机器码
    • 运行时编译
    • 编译后性能有数量级的提升

以上是关于JVM 相关知识的主要内容,如果未能解决你的问题,请参考以下文章

并发相关基础知识 - MESI - JMM

jvm相关知识

JVM 相关知识

jvm相关知识详解

JVM 内存模型

五分钟理解主存储器的基本组成结构