美文网首页技术专题JVM
深入理解JVM 系列(一)JVM运行机制 JVM 内存模型(v

深入理解JVM 系列(一)JVM运行机制 JVM 内存模型(v

作者: Gxgeek | 来源:发表于2017-09-05 22:18 被阅读126次

    为了 接下去 更好理解 JAVA 并发,多线程 JUC 包的原理 特此写下前置学习文章 深入学习 java 虚拟机

    本文目录

    • JVM启动流程
    • JVM基本结构
    • 内存模型
    • 编译和解释运行的概念

    一、java 程序 启动流程

    启动流程

    java 命令开始
    寻找 配置文件 定位需要的 .dll
    .ddl 初始化 JVM 虚拟机
    获得 native 接口
    找到main 方法运行

    二、JVM结构(运行时数据区)

    JVM结构(运行时数据区)

    - 二.一、线程私有的区域

    1.程序计数器(PC寄存器)

    • 记录正在执行的字节码地址,可辨别当前字节码解析到了什么位置,引导字节码解析顺序,并控制程序的流程。(当前程序执行到哪然后下一步该执行什么操作。)

      • 执行java 方法的时候世纪路字节码的地址,执行Native方法这个计数器为空(Undefined)
      • 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
    • 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。

    2.栈(VM Stack)

    • 栈由一系列帧组成(因此Java栈也叫做帧栈)
    • 线程私有------->方法执行时候的内存模型
      • 每个方法执行都会创建一个栈帧 用于存储局部变量表(基本数据类型和对象引用),操作数栈,动态链表,方法出口等信息,值得一提的是long,double长度为64为会占据两个局部变量空间其余为一个。
        局部变量表所需内存实在编译器完成分配的,方法在帧中所需的分配多大的局部变量空间是完全确定的,方法运行不会改变局部变量表大小。
    • 当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值。
    mark

    mark

    • 栈上分配
      • 小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上
      • 直接分配在栈上,可以自动回收,减轻GC压力
      • 大对象或者逃逸对象无法栈上分配
    mark
    每个线程包含一个栈区,栈中只保存基础数据类型和自定义对象的引用(不是对象),对象都存放在堆区中
    每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
    栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

    3.本地方法栈(Native Method Stack)

    • 和虚拟机栈所发挥的效果非常相似, 区别在于 是为执行Native 方法 所服务的,HotSpot 直接把本地方法栈和虚拟机栈合二为一

    -二.二、线程共享区域

    Method Area(方法区) 方法区是堆的逻辑部分。

    (这个只是JVM 中的 一个规范设计 每个厂商可能实现不同 本文会尽可能的 使用JDK1.8 的HotSpot 作为讲解)
    在 HotSpot中 我们有了新的东西 就是 Metaspace(元空间) 是对 JVM规范中方法区的实现
    元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过设置参数来指定元空间的大小。所有线程共享的。

    • 保存装置的类信息
      • 类的常量池()
      • 类的字段,方法信息
      • 方法字节码
    • 永久区 ---> (java 8 HotSpot 中已经废除)
      • 因为容易出现永久代的内存溢出

    JAVA 堆 (Heap )全局共享

    • 程序开发程序最为密切
    • 目的就是存放对象实例
    • 根据垃圾回收算法 分为(Minor GC、Full GC)
      • 新生代(Eden ,Survivor from ,Survivor To )
      • 老年代
      • GC的主要工作区间
      • 所有线程共享Java堆

    直接内存

    • 1.4 中加入NIO 后 基于通道(channel) 与缓存 (Buffer) 使用Native 函数 操作内存 通过一个存储在java堆中的DirectByteBuffer对象作为这块内存引用这样能显著提升性能 避免java堆 与Native 堆 来回复制数据

    三、java 的内存模型

    • 每一个线程有一个工作内存和主存独立
    • 工作内存存放主存中变量的值的拷贝
    对于普通变量,一个线程中更新的值,不能马上反应在其他变量中 如果需要在其他线程中立即可见,需要使用 volatile 关键字
    mark

    3.1、多线程操作变量模型

    mark

    3.2、关键字volatile

    • 保证内存可见性
    • 防止指令重拍(有序性)
    • volatile保证操作原子性(volatile 不能保证 原子性 感谢嗯哼kaka指出 当初可能脑残了=。=)

    volatile 不能代替锁
    一般认为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);
        }
    }
    //根本停不下来
    
    // 类似效果
    @Slf4j
    public class VolatileExample extends Thread{
        //设置类静态变量,各线程访问这同一共享变量
        private  static  boolean flag = false;
        //无限循环,等待flag变为true时才跳出循环
        public void run() {
            while (!flag){
            };
            log.info("停止了");
        }
    
        public static void main(String[] args) throws Exception {
            new VolatileExample().start();
            //sleep的目的是等待线程启动完毕,也就是说进入run的无限循环体了
            Thread.sleep(100);
            flag = true;
        }
    }
    
    

    synchronized (unlock之前,写变量值回主存)

    final(一旦初始化完成,其他线程就可见)

    3.3 指令重拍

    指令重拍就是编译器按照理解的优化代码
    可能会不按照代码顺序来 不会进行对象依赖的重拍
    会重拍对象之间不依赖的进行重拍
    最后 保证 整个线程的语义不发生改变

    • 线程内串行语义
        写后读 a = 1;b = a;    写一个变量之后,再读这个位置。
        写后写 a = 1;a = 2;    写一个变量之后,再写这个变量。
        读后写 a = b;b = 1;    读一个变量之后,再写这个变量。
        以上语句不可重排
    
    • 编译器不考虑多线程间的语义

      可重排: a=1;b=2;

    mark

    例子 -----> 正确方法多线程 指令重拍的错 synchronized 锁住对象


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

    编译运行(JIT)

    • 将字节码编译成机器码
    • 直接执行机器码
    • 运行时编译
    • 编译后性能有数量级的提升

    本文参考文献

    我的公众号

    微信公众号

    相关文章

      网友评论

      本文标题:深入理解JVM 系列(一)JVM运行机制 JVM 内存模型(v

      本文链接:https://www.haomeiwen.com/subject/lwipjxtx.html