JVM的运行机制

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/l18637220680/article/details/68489040

JVM的启动流程

jvm的启动流程

JVM的基本结构

jvm的基本结构

pc寄存器

  • PC(Program Couneter)寄存器是每个线程私有的,Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,如果是本地方法,则PC寄存器值为Underfined,寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。

方法区

  • 保存装载的信息、常量信息、常量池信息、方法信息,通常和永久区(Perm)关联在一起。

Java堆

  • Java堆是和Java应用程序关系最密切的内存空间,所有线程共享Java堆,并且Java堆完全是自动化管理,通过垃圾收集机制,垃圾对象会自动清理,不需自己去释放。
  • 根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为新生代和老年代。其中新声带存放新生的对象或者年龄不大的对象,老年代则存放老年对象。绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。
    这里写图片描述

Java栈

  • Java栈是由一系列帧组成(因此Java栈也叫做帧栈),每一次方法调用创建一个帧,并压栈。
  • Java栈一块线程私有的空间,一个栈,一般由三部分组成:局部变量表、操作数据栈和帧数据区。
  • 局部变量表:包含函数的参数及局部变量。局部变量表有很多槽位,每个槽位最多可以容纳32位的数据类型,所以int占用一个槽位,float占用2个槽位,对象是一个引用,占用一个槽位。
    实例方法和静态方法有一点不同,局部变量表的第一个槽位占用的是this,代表当前对象的引用。
public class StackDemo {//静态方法
    public static int runStatic(int i,long l,float  f,Object o ,byte b){
        return null;
    }
    public int runInstance(char c,short s,boolean b){//实例方法
        return null;
    }
}

静态方法的局部变量表实例方法的局部变量表

  • 操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
    下图是一个两数相加的操作数栈的过程:
    操作数栈的过程
  • 帧数据区:除了局部变量表和操作数据栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着访问常量池的指针,方便计程序访问常量池,另外当函数返回或出现异常时卖虚拟机子必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。
  • 直接内存:JavaNio库允许Java程序直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
  • 栈、堆、方法区交互
    栈、堆、方法区交互
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; 
    }
    public void printName(){//printName方法本身放入方法区里。
        System.out.println(name);
    } 
}

内存模型

  • 每个线程有一个工作内存并和主存独立,工作内存中存放主存中变量的值得拷贝
    这里写图片描述
  • 当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。每一个操作都是原子的,即执行期间不会被中断。
  • 对于普通变量,一个线程中更新的值,不能马上反应在其他变量中,如果需要在其他线程中立即可见,需要使用volatile关键字,但是volatile不可以代替锁。在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
    在JVM被设置成-server时运行下面代码,发现没有volatile时程序不会停止。因为在-server的模式时为了提高线程的运行效率,线程一直在本地的工作内存中取值,当加上volatile关键字时,就不会出现死循环。
public class VolatileThread extends Thread{
    private static boolean flag = true;
    //private static volatile boolean flag = true;
    public static void main(String args[]) throws InterruptedException {
        new Thread( new  Runnable() {
            @Override 
            public void run() {
                while(true){
                    if (!flag) {  
                        System.out.println(">>>>>>"); 
                        System.exit(0);  
                    } 
                }
            }
        }
                ).start();
        Thread.sleep(10); //让前面的线程先进行
        new Thread( new  Runnable() {
            @Override 
            public void run() {
                flag = false; 
                System.out.println(flag);
            }
        }
                ).start();
    }
}
  • 可见性:一个线程修改了变量,其他线程立即知道,保证内存可见行的方法:
    ①使用volatile定义变量。②使用锁。③使用final定义变量,一旦初始化完成,其他线程就可见。
  • 有序性,在此线程内,操作都是有序的,在线程外操作都是无序的(指令重排和主内存同步延时)
  • 指令重排:写后读,写后写,读后写等不可重排,可重排: a=1;b=2,编译器不考虑多线程间的语义。
/**
    假如有两个线程A和B
    线程A首先执行writer()方法
    线程B线程接着执行reader()方法
    线程B在int i=a+1 是不一定能看到a已经被赋值为1
    因为在writer中,两句话顺序可能打乱
    */
class OrderExample {
    int a = 0;
    boolean flag = false;
    public void writer() {
        a = 1;                   
        flag = true;           
    }
    public void reader() {
        if (flag) {                
            int i =  a +1;      
        }
    }
}
  • 指令重排的基本原则
    程序顺序原则:一个线程内保证语义的串行性。
    volatile规则:volatile变量的写,先发生于读。
    锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。
    传递性:A先于B,B先于C 那么A必然先于C。
    线程的start方法先于它的每一个动作。
    线程的所有操作先于线程的终结(Thread.join())。
    线程的中断(interrupt())先于被中断线程的代码。
    对象的构造函数执行结束先于finalize()方法。

编译和解释运行的概念

解释运行

  • 解释执行以解释方式(读一句执行一句)运行字节码。

编译运行(JIT)

  • 运行时编译,将字节码编译成机器码,直接执行机器码,编译后性能有数量级的提升。
展开阅读全文

没有更多推荐了,返回首页