java内存结构分析
java内存结构
我们根据线程是否共享将java内存结构分成两部分:
线程共享区域
堆
方法区(1.8成为元区间)
线程独占区域
栈
本地方法栈
PC寄存器(程序执行到的位置)
java栈结构分析:
我们先看一下栈的结构图
接下来我们详细看一下每一个部分具体作用
栈帧
每一个方法的执行就是一个栈帧,而且在栈内存中遵循先进后出的原理。听到这里,是不是感觉不是很懂(大佬直接忽略)?
我们来看一个示例:
这里先提一个小的概念:
每一个方法就是一个栈帧
入栈:方法执行的时候就会入栈,放的栈的底部。
出栈:方法执行结束就会出栈。
1.,当main方法开始执行,就会进行入栈(压栈)操作,main方法就在整个栈结构的最底部
2. main方法里调用add方法,add方法也是一个栈帧,进行了入栈操作
3. 当add方法执行结束,add方法会执行出栈(弹栈)操作。
4. add方法执行结束,main方法也会执行完毕
5. 这样就可以印证了栈的先进后出原理
局部变量表
用户存放方法参数和方法运行途中生成的变量
操作数栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
返回地址
当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
这里我们运用反汇编指令查看目录结构
类文件进行编译
javac StackStructure.java
类文件进行反汇编编译
javap -c StackStructure
然后截图看下反汇编后的add方法
public static void add();
Code:
0: bipush 100
2: istore_0
3: bipush 100
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: istore_2
10: return
我们将相应的汇编指令放在这里
bipush 将一个8位带符号整数压入栈 (这里的栈指的是操作数栈)
istore_0 将int类型值存入局部变量0
istore_1 将int类型值存入局部变量1
iload_0 从局部变量0中装载int类型值
iload_1 从局部变量1中装载int类型值
iadd 执行int类型的加法
istore_2 将int类型值存入局部变量2
我们通过反汇编指令来分析一下栈的各个结构的作用,我们对比上面的汇编指令进行相应的翻译
- 将100整数压入操作数栈
0: bipush 100
2. 将int类型的100存入局部变量表的a中
2: istore_0
3. 将100整数压入操作数栈
3: bipush 100
4. 将int类型的100存入局部变量表的b中
5: istore_1
5. 从局部变量表a中装载int类型值100到操作数栈
6: iload_0
6. 从局部变量表b中装载int类型值100到操作数栈
7: iload_1
7. 在操作数栈中执行加法操作
8: iadd
8. 将计算的结果200存入局部变量表c中
9: istore_2
9. 最后将结果给返回即可
10: return
运行时常量池
public class Test2 {
public static void main(String[] args) {
String s1 ="abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1 == s2); // true
System.out.println(s3 == s1); // false
System.out.println(s3.intern() == s1); //true
}
}
我们先分析前两个比较结果
String s1 = "abc"是存放在字符串常量池中,而new出来的对象是存放在堆中,所以前两个结果成立
但是为s3.intern() == s1的结果也是为true呢?我们再来看下一张图解
调用intern()方法,会把堆中的"abc"转移到方法区的字符串常量池中,并且覆盖原来的“abc”(字符串常量池类似于一个hashSet,转移的值会覆盖原来的值)。所以,三个对象此时都指向同一个常量“abc”。
对象的创建过程
类加载的执行流程图
对象创建的过程:
- new对象
- 根据参数在常量池中定位类符号的引用
- 判断类引用是否存在,存在则说明类已经加载,可以直接使用
- 找不到的情况下说明类还未加载,需要在堆内存中开辟内存空间
- 然后是类的属性初始化
- 类的构造方式初始化
对象内存分配方式
整个过程中,我们详细看如何在堆内存中开辟空间
有两种方案:
指针碰撞
空闲列表
指针碰撞
我们先看指针碰撞的情况
假设现在的堆内存是一块连续的空间,我们new了一个obj1。obj1加入到堆内存中,且会有一个指针指向obj1,obj2加入的时候也是同理,指针指向obj2
我们再看下一种情况,当多线程情况下new 出obj3和obj4,如何开辟内存空间呢?
这里会采用CAS算法,obj3和obj4的线程争抢锁,谁能拿到,谁就先执行并且再堆内存中开辟相应的内存空间。
空闲列表
堆内部有一个列表来存储我们堆中空闲的地方。我们创建对象则去找列表中对应的空闲区域去创建我们的对象。
堆是否规整有我们垃圾回收器来决定的 ,如果垃圾回收器使用的是标记压缩算法,那么他会规整的分配我们的对象
多线程的情况下:
空闲列表则采用我们的本地线程分配缓存,线程占满则采用我们的cas加锁方式,再去分配本地缓存分配一部分区域。
我们这里抛出一个问题,对象创建以后除了在堆上还会在哪里?
public class StackStructure {
public void a() {
StackStructure stackStructure = new StackStructure();
}
public StackStructure b() {
StackStructure stackStructure = new StackStructure();
return stackStructure;
}
}
栈上分配:
a()方法里面声明的这个对象并没有返回给外部,或者给外部使用,所以会存在栈里面。即成为栈上分配。当声明的对象太大了以后也会造成内存逃逸,被分配到堆里面去
内存逃逸:
b()方法里面的对象返回出去了,就会从栈上逃逸,分配到堆内存里面。就造成了内存逃逸
对象结构分析
对象头
hash值、gc分代年龄、持有锁信息、 类型指针:方法区存储class对象(这个唯一的);例如:new Test().getClass() == new Test().getClass();
对象实例数据
主要存放我们自身的 属性变量,包括父类属性等。
对象填充数据
使用数据填充,没有实际的意义 HotStop 虚拟机指定对象大小必须是8个字节的整数倍。如果不是8个字节则,使用此进行填充
对象的内存引用分析
对象的内存引用有两种方式:
直接引用
句柄引用
直接引用图解
对象的直接引用,当obj对象更改时,速度较快,但是每次都需要更换对象的引用地址
句柄池引用
obj对象更改以后,不会更改A的引用,只需要把句柄池里面的引用更改就好了,效率比直接引用低
具体选择那种引用方式,是根据不同的虚拟机来选择的