JVM运行机制

本博客为炼数成金 JVM教程的第二课的学习总结

博客大纲

  1. JVM启动流程
  2. JVM基本结构
  3. 内存模型
  4. 编译和解释运行的概念

JVM启动流程

JVM启动流程

JVM基本结构

JVM基本结构

PC寄存机

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

方法区(用来保存类的原信息)

  • 类型的常量池
  • 字段,方法信息
  • 方法字节码
  • 但也不是绝对的,JDK6时,String等常量信息至于方法区,但在JDK7时,已经移动到了堆
    通常和永久区(Perm:保存相对稳定的数据)关联在一起

Java堆

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

GC的主要工作区间
java堆的基本结构

Java栈

  • 线程私有
  • 栈由一系列帧组成(因此Java栈也叫作帧栈)
  • 帧保存一个方法的局部变量,操作数栈,常量池指针
  • 每一个方法调用创建一个帧,并压栈

局部变量表:包含参数和局部变量
局部变量表要很多的槽位,每一个槽位可以容纳32位的数据类型,所以int占一个槽位,long就要占两个槽位,对象存的是引用

public class StackDemo{
    public static int runStatic(int i, long l, float f, Object o, byte b){
        return 0;
    }
    public int runInstance(char c, short s, boolean b){
        return 0;
    }
}

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

实例方法的局部变量表的分配方法跟静态方法的区别在于,实例方法的第一个槽位放的是this,即当前对象的引用,其余是一致的

实例方法的局部变量表

java栈 - 函数调用组成帧栈、

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

每一次方法调用,就会有一个帧,往栈里压,一直压到帧栈满了溢出,如果方法结束了,那么就从这个栈里移掉
函数调用

Java栈 - 操作数栈
Java没有寄存器,所有参数传递使用操作数栈

public static int add(int a, int b){
    int c = 0;
    c = a + b;
    return c;
}

下面是上述代码的反编译代码
这里写图片描述

有三个变量,abc,所有局部变量有三个,分别为123
我们模拟100+98的加法运算,看看数据在局部变量和操作数栈中的变化
这里写图片描述

Java栈- 栈上分配
栈上分配和堆上分配的区别:
堆上分配:每次需要清理空间
栈上分配:函数调用完成自动清理

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 < 10000000000; i++){
            alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println(e-b);
    }
}

运行上述代码:
栈上分配

堆上分配

可见,第二次输出的时候有大量的GC日志,可见第二次运行时,这些byte数组是在堆上做分配,且因为内存不足,会进行GC,可见第二次分配在堆上,第一次分配在栈上。

栈上分配的好处:
小对象(一般几十个bytes),在没有逃逸(只被自己用,不被其余线程使用)的情况下,可以直接分配在栈上
直接分配在栈上,可以自动回收,减轻GC压力
大对象或者逃逸对象无法栈上分配

堆、栈、方法区的交互

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把Sample的信息都放入方法区
{
    private name;
    // new Sample 实例后,name引用放入栈区里,name对象放入栈里
    public Sample(String name){
        this.name = name;
    }
    // print方法本身放入方法区里
    public void printName(){
        System.out.println(name);
    }
}

堆栈方法区交互

可见,对象本身是在堆中的,栈中存放对象的引用
由栈指向堆,堆从方法区里获取方法等信息。

内存模型

  1. 每一个线程有一个工作内存和主存
  2. 每一个线程的工作内存存放主存中变量的值的拷贝

内存模型

当数据从主内存复制到工作存储时,必须出现两个动作:

  • 第一,由主内存执行的读(read)操作;
  • 第二, 由工作内存执行的相应的load操作;

当数据从工作内存拷贝到主内存时,也出现两个操作:

  • 第一个,由工作内存执行的存储(store)操作。
  • 第二:由主内存执行的相应的写(write)操作每一个操作都是原子的,即执行期间不会被中断。

例如在read中不能被中断,但 read 和load 之间是可以被中断的对于普通变量,一个线程中更新的值,不能马上反映到其他变量中,如果需要在其他线程中立即可见,需要使用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);
    }
}

使用volatile的例子,一个线程一直在给i做加1操作,另一个线程将stop的值设置为true让线程停止加1,加了volatile之后,可以让其立即停止。

volatile不能代替锁,一般认为volatile比锁性能好(不绝对)
选择使用volatile的条件是:当前场景的语义是否满足volatile的应用

一些跟内存模型相关的概念性的东西

1.可见性
一个线程修改了变量,其他线程立刻可以知道
保证可见性的方法

  • volatile
  • synchronized (unlock 之前, 写变量值回主存)
  • final (一旦初始化完成,其它线程就可见)

2.有序性
在本线程中,操作都是有序的
在线程外观察(多线程情况下),操作都是无序的(指令重排 或 主内存同步延时)

3.指令重排

线程内串行语义
写后读 a=1;b=a; 写一个变量之后,再读这个位置
写后写 a=1;a=2; 写一个变量之后,再写这个变量
读后写 a=b; b=1; 读一个变量之后,再写这个变量
以上语句不可重排,因为重排会导致定义出错,不符合开发要求
a=1;b=2;
这条语句可以重排,因为两个变量的赋值不会互相影响
与此同时,编译器不考虑多线程间的语义,因此指令重排会破坏线程间的有序性

class OrderExample{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }
    public void reader(){
        if(flag){
            int i = a + 1;
            ......
        }
    }
}

线程A首先执行writer() 方法
线程B接着执行reader() 方法
线程B在int i = a + 1 是不一定能看到a 已经被赋值为1
因为在writer中,两句话顺序可能打乱
OrderExample指令重排
可能当线程A执行完flag = true之后,线程B执行了reader操作,但此时其实a 仍然等于 0

这种有序性被破坏,就会导致线程运行的结果不符合预期

指令重排 - 保证有序性的方法

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;
            ......
        }
    }
}

同步后,即使用了writer重排,因为互斥的缘故,reader 线程看writer 线程也是顺序执行的
synchronized  控制的指令重排
可见线程A和线程B是顺序执行的,可以避免指令重排导致的问题

指令重排的基本原则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则: volatile变量的写,先发生于读
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt()) 先于被中断线程的代码
  • 对象的构造函数执行结束先于finalize()方法

JVM字节码运行的两种方法

  1. 解释运行
    解释执行以解释方式运行字节码
    解释执行的意思是:读一句执行一句
  2. 编译运行(JIT)
    将字节码编译成机器码
    直接执行机器码
    运行时编译
    编译后性能有数量级的提升
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值