2020-04-24

2020-07-08  本文已影响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() 方法的调用先行发生于被中断线程的代码检测,直到中断事件的发生。

上一篇下一篇

猜你喜欢

热点阅读