美文网首页
2020-04-24

2020-04-24

作者: 一盘好书 | 来源:发表于2020-07-08 00:40 被阅读0次

    1 简述

    Java运行时内存分布

    • 线程共享:方法区,堆;
    • 线程独有:虚拟机栈,程序计数器,本地方法栈;

    java文件通过编译生成class文件,class文件通过类加载器加载进入虚拟机,并且可以生成相应的Class对象。

    java加载过程.png

    2 程序计数器

    简而言之,记录某个线程程序执行位置。

    3 虚拟机栈

    虚拟机栈的初衷是为了描述java方法的内存模型。

    每个方法被执行的时,JVM都会在虚拟机栈中创建一个栈帧,而程序计数器则会纪录方法执行的位置。

    每一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址。

    image.png

    3.1 局部变量表

    我们写一个JVMDemo文件,然后定义如下一个方法:

    public int add() {
        int i = 1;
        int j = 3;
        int result = i + j;
        return result + 10;
    }
    

    通过javap命令编译上面的代码:

    javac ./src/JVMDemo.java
    javap -c ./src/JVMDemo.class // 可看到class字节码文件
    
    javap -v ./src/JVMDemo.class // 可看到常量池信息
    

    字节码如下:

    0: istore_1        // 把常量 1 压入操作数栈栈顶
    1: istore_1        // 把操作数栈栈顶的出栈元素放入局部变量表索引为 1 的位置
    2: iconst_3
    3: istore_2
    4: iload_1
    5: iload_2
    6: iadd
    7: istore_3
    8: iload_3
    9: bipush        10
    11: iadd
    12: ireturn
    

    以下信息引用至Android工程师进阶34讲

    • const 和 bipush,这两个指令都是将常量压入操作数栈顶,区别就是:当 int 取值 -1~5 采用 iconst 指令,取值 -128~127 采用 bipush 指令。
    • istore 将操作数栈顶的元素放入局部变量表的某索引位置,比如 istore_5 代表将操作数栈顶元素放入局部变量表下标为 5 的位置。
    • iload 将局部变量表中某下标上的值加载到操作数栈顶中,比如 iload_2 代表将局部变量表索引为 2 上的值压入操作数栈顶。
    • iadd 代表加法运算,具体是将操作数栈最上方的两个元素进行相加操作,然后将结果重新压入栈顶。

    指定内存大小运行一个JVM:第一,需要先将主java文件进行编译生成class文件。第二,再运行如下命令:

    java -Xms200m GCRootLocalVariable
    

    可达性分析来查看对象是否可被回收,通过一组名为“GC Root”的起始点开始往下回收,搜索走过的路径称为引用链,通过引用链判断对象是否可被回收。

    哪些对象可作为GC Root

    • 虚拟机栈中局部变量表的引用对象;
    • 方法区中静态引用指向的对象;
    • 仍处于运行状态的线程;

    java虚拟机(JVM)根据对象存活周期的不同,把堆内存划分为几个区域,一般是新生代和老年代。

    其中新生代又被划分为:Eden区域,Survivor0区,Survivor1区。

    注意设置jvm参数的顺序,我本人电脑上的java版本是1.8.0_121,此时需要先设置新生代内存,再设置总内存才会成功。

    JVM虚拟机存储对象的基本规则

    绝大多数被创建的对象存在于Eden区,当Eden区对象满了之后,进行一次垃圾回收,把存活的对象移动至Survivor0区,当Eden区对象再次满时,再次触发垃圾回收,将Eden区和Survicor0区存活的对象复制到Survivor1区,如此循环往复15次左右,还存活的对象将进入老年代中。

    java -Xmn10M -Xmx20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 MinorGCTest
    
    Heap
      // 新生代总大小 6144K,已经使用3622K 
     PSYoungGen      total 6144K, used 3622K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
      // 新生代分为3个区:eden区,survivor0区,survivor1区
      eden space 5632K, 57% used [0x00000007bf980000,0x00000007bfca9a78,0x00000007bff00000)
      // survivor0
      from space 512K, 75% used [0x00000007bff00000,0x00000007bff60020,0x00000007bff80000)
      // survivor1
      to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
     ParOldGen       total 13824K, used 4104K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
      object space 13824K, 29% used [0x00000007bec00000,0x00000007bf002020,0x00000007bf980000)
     Metaspace       used 2630K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 286K, capacity 386K, committed 512K, reserved 1048576K
    
    

    编译器负责将java文件转换为class文件字节码,类加载器对class字节码进行加载并生成相应的Class对象供外部调用

    在编译打包时期,dx命令可以让class文件优化为 dex文件,但是如果没有全局配置,需要进入到dx命令所在目录/Users/....../sdk/build-tools/28.0.3,然后使用

    ./dx --dex --output={文件目录} {需要打包的文件}
    

    ./dx --dex --output=say_something_hotfix.jar say_something.jar
    

    记住:未配置成全局的命令,执行时都得增加./的前缀。

    java内存模型 — JMM

    虚拟机栈和线程的工作内存并不是一个概念,线程的工作内存只是对CPU寄存器和高速缓存的一个抽象描述。

    由于CPU的运算速度远高于CPU对主内存的操作速度,所以出现了高速缓存来缓冲数据,从而达到提高运算效率的目的。

    但由此产生了多线程访问共享数据的安全性问题。安全性问题围绕着三个方面:原子性,可见性,排序性。

    各个线程都有自己的工作内存,将有可能导致共享数据的拷贝副本出现不一致的情况,这就是缓冲一致性问题。

    java内存模型中遵守的行为规范中有一条非常重要的规则:happens-before 先行发生原则。

    如果A happens before B成立,先发生动作的结果将对后续动作是可见。比如如下代码:如果满足SetColor happens before getColor,那么setColor中的值始终对getColor可见。

    public class Car {
        private String color;
    
        public String getColor() {
            return color;
        }
    
        public void setColor() {
            this.color = "black";
        }
    }
    

    JMM定义如下几种情况是自动符合happens before 的

    程序次序原则

    前后代码的逻辑顺序中有依赖关系的,不会发生指令重排。

    锁定规则

    无论是在单线程环境还是多线程环境,一个锁如果处于被锁定状态,那么必须先执行 unlock 操作后才能进行 lock 操作。

    变量规则

    volatile 保证了线程可见性。通俗讲就是如果一个线程先写了一个 volatile 变量,然后另外一个线程去读这个变量,那么这个写操作一定是 happens-before 读操作的。

    线程启动规则

    Thread 对象的 start() 方法先行发生于此线程的每一个动作。假定线程 A 在执行过程中,通过执行 ThreadB.start() 来启动线程 B,那么线程 A 对共享变量的修改在线程 B 开始执行后确保对线程 B 可见。

    线程中断规则

    对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测,直到中断事件的发生。

    相关文章

      网友评论

          本文标题:2020-04-24

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