1 简述
Java运行时内存分布
- 线程共享:方法区,堆;
- 线程独有:虚拟机栈,程序计数器,本地方法栈;
java文件通过编译生成class文件,class文件通过类加载器加载进入虚拟机,并且可以生成相应的Class对象。
java加载过程.png2 程序计数器
简而言之,记录某个线程程序执行位置。
3 虚拟机栈
虚拟机栈的初衷是为了描述java方法的内存模型。
每个方法被执行的时,JVM都会在虚拟机栈中创建一个栈帧,而程序计数器则会纪录方法执行的位置。
每一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址。
image.png3.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() 方法的调用先行发生于被中断线程的代码检测,直到中断事件的发生。
网友评论