代码如下,非常简单
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源代码自己好好琢磨。否则钻这种牛角尖纯属浪费时间和精力。
好的 非常感谢!