【jvm】01- java内存结构分析

时间:2022-07-25
本文章向大家介绍【jvm】01- java内存结构分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

我们通过反汇编指令来分析一下栈的各个结构的作用,我们对比上面的汇编指令进行相应的翻译

  1. 将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”。

对象的创建过程

类加载的执行流程图

对象创建的过程:

  1. new对象
  2. 根据参数在常量池中定位类符号的引用
  3. 判断类引用是否存在,存在则说明类已经加载,可以直接使用
  4. 找不到的情况下说明类还未加载,需要在堆内存中开辟内存空间
  5. 然后是类的属性初始化
  6. 类的构造方式初始化

对象内存分配方式

整个过程中,我们详细看如何在堆内存中开辟空间 有两种方案: 指针碰撞 空闲列表

指针碰撞

我们先看指针碰撞的情况

假设现在的堆内存是一块连续的空间,我们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的引用,只需要把句柄池里面的引用更改就好了,效率比直接引用低

具体选择那种引用方式,是根据不同的虚拟机来选择的