各种变量在内存中的存储

代码如下,非常简单
public class Main {
        int i1 = 100;
        String str1 = new String("abc");
        public static void main(String[] args) {
                int i2 = 200;
                String str2 = new String("efg");
    }
}


反编译后如下:
Classfile /D:/Main.class
  Last modified 2019-12-18; size 513 bytes
  MD5 checksum e058e0e4b1a6a397bd8b0abf8b7b41f1
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#27         // java/lang/Object."<init>":()V
   #2 = Fieldref           #6.#28         // Main.i1:I
   #3 = String             #29            // abc
   #4 = Fieldref           #6.#30         // Main.str1:Ljava/lang/String;
   #5 = String             #31            // efg
   #6 = Class              #32            // Main
   #7 = Class              #33            // java/lang/Object
   #8 = Utf8               i1
   #9 = Utf8               I
  #10 = Utf8               str1
  #11 = Utf8               Ljava/lang/String;
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               LMain;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               i2
  #24 = Utf8               str2
  #25 = Utf8               SourceFile
  #26 = Utf8               Main.java
  #27 = NameAndType        #12:#13        // "<init>":()V
  #28 = NameAndType        #8:#9          // i1:I
  #29 = Utf8               abc
  #30 = NameAndType        #10:#11        // str1:Ljava/lang/String;
  #31 = Utf8               efg
  #32 = Utf8               Main
  #33 = Utf8               java/lang/Object
{
  int i1;
    descriptor: I
    flags:

  java.lang.String str1;
    descriptor: Ljava/lang/String;
    flags:

  public Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        100
         7: putfield      #2                  // Field i1:I
        10: aload_0
        11: ldc           #3                  // String abc
        13: putfield      #4                  // Field str1:Ljava/lang/String;
        16: return
      LineNumberTable:
        line 2: 0
        line 3: 4
        line 4: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  this   LMain;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: sipush        200
         3: istore_1
         4: ldc           #5                  // String efg
         6: astore_2
         7: return
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 8: 7
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  args   [Ljava/lang/String;
            4       4     1    i2   I
            7       1     2  str2   Ljava/lang/String;
}
SourceFile: "Main.java"


主要探讨以下几个方面:
1、类变量中的i1在哪
2、类变量中的引用str1在哪,引用指向的String实例在哪,具体的"abc"字符串在哪
3、局部变量中的i2在哪
4、局部变量变量中的引用str2 在哪,引用指向的String实例在哪,具体的"efg"字符串在哪

原则:代码方法体中的引用变量和基本类型的变量都在栈上,其他都在堆上

先看int i1 = 100;
可以确定,基础数据类型i1=100是在堆上
那么i1=100是如何存放到内存中的?
根据指令:
         5: bipush        100
         7: putfield      #2                  // Field i1:I
这两个指令执行结束后,将100这个整数赋值给常量池中#2的实例字段,就是i1

高级语言中,变量名这个东西和内存中的地址是一对一映射的,所以也就是说i1对应的堆内存的地址中存的就是100,这个也就是基础数据类型的存放方式。

String str1 = new String("abc");

引用类型的实例变量可以分为以下几个地方分析:
1、str1这个引用存在哪?
2、new出的String对象存在哪?
3、"abc"这个字符串存在哪?

个人分析:
1、str1这个引用,根据原则来看(代码方法体中的引用类型和基本数据类 型的变量都在栈上,其他情况都在堆上),作为实例变量,必定存放于堆上,但是存放的内容不同于基础数据类型,str1作为一个变量名,必定也是与内存地址 具有一对一映射关系,只不过str1映射的地址内存放的是new出的String对象所处的内存中的地址(怎么看都像C的指针,声明的指针的变量的值是一 个地址),这个可能就是“引用”这个名字的由来;
2、new出的String对象存在堆内,这个没有异议,无论是在类层级下还是方法层级下,new出的对象都是在堆内,原则上说的方法体内的是引用类型和基本数据类型在栈上,并不是说的new出来的对象;

1、类变量中的i1在哪
哪都不在,因为你没有new Main对象,只是在代码区里有个指令
public Main(); //构造函数代码区
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V //如果调用构造方法,则在这个对象的属性里,这个对象在堆上,所以它也在堆上
         4: aload_0
         5: bipush        100
         7: putfield      #2                  // Field i1:I //存入对象属性


2、类变量中的引用str1在哪,引用指向的String实例在哪,具体的"abc"字符串在哪
str1和上面的i1一样

String实例在堆上,因为指令是aXX指令都是引用类型,也就是栈内的变量里存的是堆对象的引用,所以String实例在堆上
public Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        100
         7: putfield      #2                  // Field i1:I
        10: aload_0  //aXX指令,把引用变量压入栈顶
        11: ldc           #3                  // String abc //从常量池推送至栈顶,而栈顶是个引用,所以实际把abc存入了堆(引用)里
        13: putfield      #4                  // Field str1:Ljava/lang/String;


“abc”在常量池里
Constant pool: //常量池
   #1 = Methodref          #7.#27         // java/lang/Object."<init>":()V
   #2 = Fieldref           #6.#28         // Main.i1:I
   #3 = String             #29            // abc 


3、局部变量中的i2在哪
在main方法栈里
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: sipush        200
         3: istore_1 //下标为1的栈变量

4、局部变量变量中的引用str2 在哪,引用指向的String实例在哪,具体的"efg"字符串在哪
str2也是在main方法栈里
Code:
      stack=1, locals=3, args_size=1
         0: sipush        200
         3: istore_1
         4: ldc           #5                  // String efg
         6: astore_2 //下标为2的栈变量

String实例在堆上,因为也是aXX指令
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: sipush        200
         3: istore_1
         4: ldc           #5                  // String efg //从常量池推送至栈顶
         6: astore_2 //把栈顶对象存入栈的下标为2的变量,因为是aXX指令,是个引用,所以实际存到了堆(引用)里

“efg”也是在常量池
Constant pool:
   #1 = Methodref          #7.#27         // java/lang/Object."<init>":()V
   #2 = Fieldref           #6.#28         // Main.i1:I
   #3 = String             #29            // abc
   #4 = Fieldref           #6.#30         // Main.str1:Ljava/lang/String;
   #5 = String             #31            // efg

 

那如果
public class Main {
        int static i1 = 100;
}
这个i1呢,应该就在堆上了吧

i1在静态区,也是堆内存

果然还是得有讨论才有进步 非常感谢

还有一个疑问,堆区还在JVM里还有静态区这种区域么?没有吧应该

严格来说,方法区,静态区,常量池区,都属于堆内存。但是内存管理机制上有区别,所以从概念和功能上又可以划分这些内存区域。
所以这个问题,你说没有,那它就没有,因为它属于堆内存;你说有,那它就有,因为它和一般的堆内存不一样。

还有一个比较疑惑的地方,
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #5                  // String abc
         2: putstatic     #6                  // Field str1:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 4: 0
这个是反编译后的一段,是个静态代码块,就是一个静态变量的定义,我这里想不通的是,既然静态变量都在逻辑方法区(实现在堆区),为什么指令要用ldc,ldc不是先把常量推到栈顶么?难道static{}这个代码块也会有栈帧?

方法区只是存代码指令,代码执行时还是会开启一个方法栈的,所以我们经常说,调用方法的时候会新开一个栈,就是这个意思

看我捋一下思路
首先jvm中虚拟机栈就是线程独立,也就是说线程每调用一个方法,那么就会在虚拟机栈中创建一个栈帧(我感觉你说的“调用方法的时候会新开一个栈”应该指 的新开一个栈帧吧),然后把要用的变量常量这些数据放到栈顶再用指令操作(大概我理解的流程就是这个)。现在其实疑问在static{}代码块里面的 ldc这个指令,这个指令应该就是先把"abc"推到操作栈栈顶然后用putfield赋值,那么也就是说static{}也被当成是一个方法,在虚拟机 栈内生成了一个栈帧?当类加载的时候会专门有一个线程,进行处理static相关的数据么?

必须的呀,类加载线程就是干这事用的,你可以参考jvm类加载机制的相关资料。
另外,不是说调用方法才会创建栈帧(但是有方法调用就会创建栈帧),每个{}区域就会一个栈帧创建,否则变量的作用域如何控制,比如你在{}里定义了一个变量,离开{}该变量就消亡了,为什么?就是栈帧被撤销了。

大佬  太感谢你了  已关注

在深入理解Java虚拟机书中的类加载过程章节讲到,静态类成员变量是在类加载过程中的“准备”过程将类static成员变量初始化然后放到方法区。你想问的成员变量应该指的是非static成员变量,那么会在方法初始化它的时候才分配到Java堆中。

我们讨论java的内存区分,不牵扯到实现,只牵扯到规范,因为jvm只有规范,实现是千变万化的,方法区内存可以和堆放在一起,也可以不放在一起,这取 决于具体的jvm实现,但是虚拟机规范中有规定,类、常量池、方法和静态成员变量都在方法区,对象和对象的属性在堆内存。

怎么个误法?希望能明确指出
方法区的实现,可以参考一下帖子,有几个版本jvm的比较

https://aijishu.com/a/1060000000003867
第二章节的5里有句话【方法区是堆的一个逻辑部分,为了区分Java堆,它还有一个别名Non-Heap(非堆)。】
在第三章节的2里,有各个版本的内存模型的比较,更直观看到堆和方法区的关系

正如你所说,虚拟机有规范,但是该规范没有定义方法区用什么实现,所以可以是堆,也可以不是堆。对于Hotspot虚拟机来说是在堆实现的,所有静态区也可以看作堆内存。
至于对象的属性,你一上来就说它在堆里,那我问你,对象没创建,对象本身在内存都不存在,哪来的对象属性在堆里之说?是我误导还是你误导?

另外,该楼的回答是针对LZ在4楼和6楼追问的回答,是建立在之前回答的共识的基础上的回答,希望你上下文都看了以后再作出回答,否则就是片面的。

 

对象的属性是指的成员变量么?
是的

我很明确的指出了,方法区, 静态区,不属于堆内存,虚拟机规范没有指出方法区属于堆内存,虽然可以这么实现,但是并不代表有虚拟机这么实现了你就可以这 么看,虚拟机实现完全可以吧堆内存和非堆内存也放在一起,虚拟机还可以不实现垃圾回收,那你是不是可以当做JAVA不存在垃圾回收?讨论虚拟机内存结构, 是一个逻辑上的问题,不是一个具体实现上的问题,这个逻辑问题依赖于虚拟机规范。

对象的属性没实例化,当然没有对象属性一说,但是这和对象与对象的属性都放在堆里面有矛盾么?创建对象以后,把它放在堆里,然后初始化对象的属性再把它放在堆里,这个过程你有疑问?

你这属于巧妙的问题转移,拿着虚拟机的规范当令牌,答非所问。
如果一种虚拟机用堆来实现方法区,那么说方法区也在堆里,有问题吗?
如果一种虚拟机实现没垃圾回收,那它就是没有垃圾回收,你能因为规范有垃圾回收而说它就有垃圾回收吗?
LZ的原题列出源码伪指令,然后问i1在内存哪里,你一上来拿着规范就说它在堆里,你自己觉得正确吗?对象初始化后在堆里,我没疑问,但是LZ的原题里有初始化的代码吗?你觉得有了规范这就可以忽略实际只讲理论吗?

反编译出来就是为了方便看汇编指令、本地变量表、异常表和代码行偏移量映射表这些东西,要说语法我也没看出来有什么语法。。。

严格来说,方法区,静态区,常量池区,都属于堆内存。但是内存管理机制上有区别,所以从概念和功能上又可以划分这些内存区域。
所以这个问题,你说没有,那它就没有,因为它属于堆内存;你说有,那它就有,因为它和一般的堆内存不一样。

 

大佬  
我看了一下  如果是
static final i1 = 10; 
static int i2 = 11;

这两个还不太一样,i1会直接在静态常量池中声明出一个Integer_info的常量项,我自己猜测了一下,是由于基础数据类型的 ConstantValue的特殊性,那么是不是说这个常量加载到内存中就是在运行时常量池里存放了101这个值?i1这个映射的地址就是直接映射到了运 行时常量池里,所以i1也不需要在静态常量池内有Field_info这个常量项。

而i2这个类变量,在准备阶段分配到“静态区”,然后在初始化过程中,用static{}里面的指令再给他赋值到这个对应的字段内/

Integer_info是常量池整形字面量信息,你的10被放到常量池,必然有该信息

常量池不会有field_info,这是字段表,和常量池不是一个概念,常量池有fielder_info,用于保存字段的符号引用,也就相当于一个描述符信息。

ConstantValue是属性表的一个属性,对于用final声明的类字段就会生成该属性

你把i1,i2的反编译代码贴出来看看,没反编译代码想象不出你的问题点。

我的描述不是很严谨,抱歉,我重新整理了一下措辞,然后贴上了代码和反编译代码:
代码如下:
class A{
static int i1 = 100;
static final i2 = 101;
static final f1 = 11.0f
public static void main(String args[]){
float f2 = f1+1.0f;
}
}


反编译后常量池、字段表、以及使用常量的指令的情况如下:
Constant pool:
   #1 = Methodref          #5.#29         // java/lang/Object."<init>":()V
   #2 = Class              #30            // A
   #3 = Float              12.0f
   #4 = Fieldref           #2.#31         // A.i1:I
   #5 = Class              #32            // java/lang/Object
   #6 = Utf8               i1
   #7 = Utf8               I
   #8 = Utf8               i2
   #9 = Utf8               ConstantValue
  #10 = Integer            101
  #11 = Utf8               f1
  #12 = Utf8               F
  #13 = Float              11.0f
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               LA;
  #21 = Utf8               main
  #22 = Utf8               ([Ljava/lang/String;)V
  #23 = Utf8               args
  #24 = Utf8               [Ljava/lang/String;
  #25 = Utf8               f2
  #26 = Utf8               <clinit>
  #27 = Utf8               SourceFile
  #28 = Utf8               A.java
  #29 = NameAndType        #14:#15        // "<init>":()V
  #30 = Utf8               A
  #31 = NameAndType        #6:#7          // i1:I
  #32 = Utf8               java/lang/Object
{
  static int i1;
    descriptor: I
    flags: ACC_STATIC

  static final int i2;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 101

  static final float f1;
    descriptor: F
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: float 11.0f

.....
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #3                  // float 12.0f
         2: fstore_1
         3: return

第一个问题就是:
i1作为类变量,会在常量池中有对应的CONSTANT_Fieldref_info(之前说错了,我说的field_info实际上指的就是 CONSTANT_Fieldref_info符号引用),而i2、f1作为常量则CONSTANT_Fieldref_info,从main的方法表里 能能看出来,调用f1常量的时候,直接是ldc #3 ,从常量池里拿到的float11.0f;
那这个我理解成“常量的值就是存在于静态常量池中,然后加载后在运行时常量池里,指令用到的时候直接从常量池中定位到常量并获取到其的数值”,这样理解是否正确?? 【!!这里只针对常量并且值为基础数据类型分析!!】

第二个问题:
i1是个类变量,然后在常量池里有对应的CONSTANT_Fieldref_info,就是加载常量池的时候 #4肯定也会加载到运行时常量池,然后再看初始化的过程:
static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        100
         2: putstatic     #4                  // Field i1:I
         5: return

putstatic #4 把100这个值放到对应字段地址内
我个人理解就是:在解析阶段,会把i1的CONSTANT_Fieldref_info符号引用,变成直接引用(准备阶段给static字段分配的空 间),然后初始化的时候putstatic #4就是通过常量池的#4,拿到真实的字段地址,然后把100放进去,这个理解的对不对?

所以从上面两个来看,类变量和常量(针对基础数据类型)虽然都是直接存的值,但是存的地方还是有一定的区别,常量直接在常量池里,类变量是在静态区(之前问过你 不过不知道这个静态区逻辑上是不是属于方法区)?

还有一个疑问就是字段表有什么用处,是不是字段存储就是通过字段表中的Field_info加载的呢?是把常量也会加载么?但是按照之前自己的分析常量直接在常量池啊,这里又会又矛盾,大概就这些 现在感觉非常的混乱,越看越迷糊。///

 

而i2、f1作为常量则在常量池中没有CONSTANT_Fieldref_info


上个回复少了几个字。。

首先Hotspot的常量池有3种,Class文件常量池,运行时常量池和全局字符串常量池
你反编译看到的常量池信息是Class文件常量池(毕竟没有运行,所以后两者是不会生成也就是看不到信息的),在编译期就生成,也就是class文件会有Class文件常量池的信息描述,基本是一些常量的字面量和符号引用(字段/方法名描述符,类/接口全限定名)等。

问题1,为什么final修饰的变量没有CONSTANT_Fieldref_info?
这是编译优化,不需要展开符号引用才去找该字段,而是直接通过ConstantValue属性取值
你可以参考一下的文章,会发现更有意思,用final修饰的类字段,类本身不需要加载就可以使用。也就是说,final修饰的类字段不需要加载类就可以使用,所有不需要符号引用。

https://blog.csdn.net/tiantiandjava/article/details/86505855
另外,ldc #3取的是12.0,也是编译优化,自动把计算结果保存到常量池,就好像String s = "a" + "b"; 常量池会自动生成"ab"一样

问题2,基本如你所理解,类加载时会展开符号引用保存到运行时常量池,指令putstatic #4会通过符号引用找到直接引用

类变量和常量(针对基础数据类型)的区别参见问题1的解释,该常量不需要符号引用。类变量在静态区(常量池保存该符号引用),静态区逻辑上属于代码区。

Field_info是class文件的一个数据结构,和CONSTANT_Fieldref_info的区别

首先非常感谢解答!!!!!!

再引申一个问题:还是上面的代码,CONSTANT_Fieldref_info这个符号引用转换成直接引用之后,这个指向的直接引用是什么?是不是就是字段表里面的东西加载到内存后的地址?看了一下知乎的回答,也没有特别明确的回复

还有就是,我记着之前看书,说的是加载的时候会把静态常量池(字节码文件的常量池)全部加载到内存里,也就是运行时常量池,这个应该是没问题的吧

直接引用直接替换符号引用。也就是第一次解析符号引用时通过它找到加载到内存中的相应字段/方法的具体偏移地址,然后用该偏移地址覆盖符号引用。
在链接的字段决议那部分大概有描述。具体你可以网上搜一下符号引用解析为直接引用的帖子,一般都有解释符号引用的信息由怎样怎样变成怎样怎样。

看了一下您给发的两个url,但是发现还是没有我需要的答案,可能还得麻烦您这给解惑一下
现在的疑问其实挺简单的:
.class文件中有字段表合集,我网上搜了一圈,基本上都是复制粘贴,所有对字段表合集作用的解释都是:对字段的相关描述。我其实好奇的是这个字段表合集对字段描述结束后,JVM会在加载的时候处理字段表合集内的Field_info么?
就比如说:static int i1 = 100;
这个字段i1:
1、是根据字段表合集中的i1的这个field_info来进行加载的么?
2、被JVM记载完以后是不是放到方法区了?
3、i1对应的常量池中的CONSTANT_Fieldref_info进行符号引用转直接引用的时候是指向了第一个问题中i1被加载后在方法区的地址么?

看了一晚上,也没想明白这三个问题的答案。

其实按照我上个回复的思路来推理,感觉是正确的
关联起来类的加载,在准备阶段,不需要直接对类变量赋值代码中的确切的值,所以在field_info中也不需要明确出准确的值,所以只在准备阶段分配了 i1的内存;而在static{}中,会对i1对应的CONSTANT_Fieldref_info常量项作为操作数并且putstatic,初始化之前 已经进行了解析,也就是符号引用转直接,那么这个推理看起来好像是合理的

1 你主要是没把文件结构和内存结构捋清楚。class被文件结构解析后会生成相应的内存数据结构保存信息。field_info与其是从i1来,不如说是从你的反编译代码的
static int i1;
  descriptor I;
  flags ACC_STATIC
这部分内容而来的,也就是field_info运行时会生成对应的Field内存结构(也就是反编译的通过Class对象拿到的Field对象)

2 加载后的field_info对应于内存结构的Field对象,是Class信息的一个属性,
lrc下载所以跟随Class也在在静态区。

3 不是指向同一个地址。field_info存字段的描述信息,也就相当于用反射得到的Field对象,所以不是同一个地址。 Fielderef_info在Class文件的常量池,运行期会生成一个ConstantPool与之对应,它像个数组,通过下标也就是你看到的#3这 类符号来定位数据。该符号引用被替换为直接引用后直接存于ConstantPool内存结构。它的值是一个相对内存偏移地址(因为每次运行加载到内存的地 址不是固定的,所以是个相对Class对象首地址的偏移地址,毕竟它是Class数据结构的一个成员),和field_info不是一回事。

类加载分5个阶段,加载,验证,准备,解析,初始化。在准备阶段并不会赋值(当然常量会直接赋值,而静态变量会赋初始默认值),解析阶段只是把符号用替换为直接引用,初始化阶段才会真正赋值。

我觉得你不是搞理论或搞底层研发的话,没必要研究那么深,没有意义,对你做项目有帮助吗?建议有时间多学点有用的东西估计更好。你要一心扎下去想研究,就去网上搜个jvm源代码自己好好琢磨。否则钻这种牛角尖纯属浪费时间和精力。

好的  非常感谢!

发布了79 篇原创文章 · 获赞 2 · 访问量 2251

猜你喜欢

转载自blog.csdn.net/liuji0517/article/details/104892988
今日推荐