Java堆、栈、方法区、常量池

时间:2022-12-27 07:57:29

1 堆与栈

Java的数据根据不同的使用情况,有不同的分类,接下来先简单概括一下各种数据类别(不是类型)的内存分配情况,首先帮助区分一下java堆和java栈:

  • 基础数据类型(Value type)直接在栈(stack)空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。

  • 引用数据类型,需要用new来创建,既在栈空间分配一个地址空间(reference),又在堆空间分配对象的类变量(object)。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。

  • 局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。

  • 方法调用时传入的 literal 参数,先在栈空间分配,在方法调用完成后从栈空间回收。字符串常量在 DATA 区域分配 ,this 在堆空间分配。数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小!
    哦 对了,补充一下static在DATA区域分配。

从Java的这种分配机制来看,堆栈又可以这样理解:栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享。跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这些对象的引用却是在栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。


1.1 堆区

(1)存储的全部是对象的实例,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)。
(2)每个jvm实例只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和引用型变量,只存放对象实例本身。


1.2 栈区

(1)每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用型数据,对象都存放在堆区中。
(2)每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
(3)栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
(4)stack的区域很小,只有1M,特点是存取速度很快,所以在stack中存放的都是快速执行的任务,比如static变量,static方法,基本数据类型的数据,和对象的引用(reference).



2 方法区

方法区涉及到JVM类的装载,以及然后把类的信息装载到运行中的JVM让代码可以执行?要知道,Java是面向对象的语言,出了几个基本数据类型,其他的都是类啊。通常这些类表示是由编译器从 Java 语言源代码生成的,而且它们通常存储在扩展名为 .class 的文件中。那么class文件有哪几部分组成呢?

Hello.java 的源代码和(部分)二进制类文件:

Java堆、栈、方法区、常量池

显示的二进制类表示中首先是“cafe babe”特征符,它标识 Java 二进制类格式(并顺便作为一个永久的 ― 但在很大程度上未被认识到的 ― 礼物送给努力工作的 barista,他们本着开发人员所具备的精神构建 Java 平台)。这个特征符恰好是一种验证一个数据块 确实声明成 Java 类格式的一个实例的简单方法。任何 Java 二进制类(甚至是文件系统中没有出现的类)都需要以这四个字节作为开始。

该数据的其余部分不太吸引人。该特征符之后是一对类格式版本号(本例中,是由 1.4.1 javac 生成的次版本 0 和主版本 46 ― 用十六进制表示就是 0x2e),接着是常量池中项的总数。项总数(本例中,是 26,或 0x001a)后面是实际的常量池数据。这里放着类定义所用的所有常量。它包括类名和方法名、特征符以及字符串(您可以在十六进制转储右侧的文本解释中识别它们),还有各种二进制值。

常量池中各项的长度是可变的,每项的第一个字节标识项的类型以及对它解码的方式。这里我不详细探究所有这些内容的细节,如果感兴趣,有许多可用的的参考资料,从实际的 JVM 规范开始。关键之处在于常量池包含对该类所用的其它类和方法的所有引用,还包含了该类及其方法的实际定义。常量池往往占到二进制类大小的一半或更多,但平均下来可能要少一些。

常量池后面还有几项,它们引用了类本身、其超类以及接口的常量池项。这些项后面是有关字段和方法的信息,它们本身用复杂结构表示。方法的可执行代码以包含在方法定义中的 代码属性的形式出现。用 JVM 的指令形式表示该代码,一般称为 字节码。

method(方法区)又叫静态区,存放所有的class和静态变量,方法区存放的是整个程序中唯一的元素,如class和static变量。method区可以被所有的线程共享,这一点和heap一样。


2.1 字节码与堆栈

构成类文件可执行部分的字节码实际上是针对特定类型的计算机 ― JVM ― 的机器码。它被称为 虚拟机,因为它被设计成用软件来实现,而不是用硬件来实现。每个用于运行 Java 平台应用程序的 JVM 都是围绕该机器的实现而被构建的。

这个虚拟机实际上相当简单。它使用堆栈体系结构,这意味着在使用指令操作数之前要先将它们装入内部堆栈。指令集包含所有的常规算术和逻辑运算,以及条件转移和无条件转移、装入/存储、调用/返回、堆栈操作和几种特殊类型的指令。有些指令包含立即操作数值,它们被直接编码到指令中。其它指令直接引用常量池中的值。


2.2 常量池

常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据,这一部分就是上面所说的【常量池数据】,当这个class文件被载入内存,那么保存常量池数据的地方也就叫做常量池。很多人会把方法区和常量池混为一谈,实际上,可以理解为方法区中有很多常量池,或者把它理解为一个大的常量池。

它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = “java”这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是JVM的一块特殊的内存空间。

Java是一种动态链接的语言,常量池的作用非常重要,常量池中除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值外,还包含一些以文本形式出现的符号引用,比如:

  • 类和接口的全限定名;
  • 字段的名称和描述符;
  • 方法和名称和描述符。

在C语言中,如果一个程序要调用其它库中的函数,在链接时,该函数在库中的位置(即相对于库文件开头的偏移量)会被写在程序中,在运行时,直接去这个地址调用函数。而在Java语言中不是这样,一切都是动态的。编译时,如果发现对其它类方法的调用或者对其它类字段的引用的语句,记录进class文件中的只能是一个文本形式的符号引用,在连接过程中,虚拟机根据这个文本信息去查找对应的方法或字段。

所以,与Java语言中的所谓“常量”不同,class文件中的“常量”内容很非富,这些常量集中在class中的一个区域存放,一个紧接着一个,这里就称为“常量池”。

在Java程序中,有很多的东西是永恒的,不会在运行过程中变化。比如一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属类型,一个常量,还有在程序中出现的大量的字面值。而这些在JVM解释执行程序的时候是非常重要的。那么编译器将源程序编译成class文件后,会用一部分字节分类存储这些代码。而这些字节我们就称为常量池。事实上,只有JVM加载class后,在方法区中为它们开辟了空间才更像一个“池”。正如上面所示,一个程序中有很多永恒的类似粗体代码显示的部分。每一个都是常量池中的一个常量表(常量项)。而这些常量表之间又有不同,class文件共有11种常量表,如下所示:

Java堆、栈、方法区、常量池

(1) CONSTANT_Utf8 用UTF-8编码方式来表示程序中所有的重要常量字符串。这些字符串包括: ①类或接口的全限定名, ②超类的全限定名,③父接口的全限定名, ④类字段名和所属类型名,⑤类方法名和返回类型名、以及参数名和所属类型名。⑥字符串字面值
表格式: tag(标志1:占1byte) length(字符串所占字节的长度,占2byte) bytes(字符串字节序列)

(2) CONSTANT_Integer、 CONSTANT_Float、 CONSTANT_Long、 CONSTANT_Double 所有基本数据类型的字面值。比如在程序中出现的1用CONSTANT_Integer表示。3.1415926F用 CONSTANT_Float表示。
表格式: tag bytes(基本数据类型所需使用的字节序列)

(3) CONSTANT_Class 使用符号引用来表示类或接口。我们知道所有类名都以 CONSTANT_Utf8表的形式存储。但是我们并不知道 CONSTANT_Utf8表中哪些字符串是类名,那些是方法名。因此我们必须用一个指向类名字符串的符号引用常量来表明。
表格式: tag name_index(给出表示类或接口名的CONSTANT_Utf8表的索引)

(4) CONSTANT_String 同 CONSTANT_Class,指向包含字符串字面值的 CONSTANT_Utf8表。
表格式: tag string_index(给出表示字符串字面值的CONSTANT_Utf8表的索引)

(5) CONSTANT_Fieldref 、 CONSTANT_Methodref、 CONSTANT_InterfaceMethodref 指向包含该字段或方法所属类名的 CONSTANT_Utf8表,以及指向包含该字段或方法的名字和描述符的CONSTANT_NameAndType 表
表格式: tag class _index(给出包含所属类名的CONSTANT_Utf8表的索引) name_and_type_index(包含字段名或方法名以及描述符的 CONSTANT_NameAndType表 的索引)

(6) CONSTANT_NameAndType 指向包含字段名或方法名以及描述符的 CONSTANT_Utf8表。
表格式: tag name_index(给出表示字段名或方法名的CONSTANT_Utf8表的索引) type_index(给出表示描述符的CONSTANT_Utf8表的索引)

在Java源代码中的每一个字面值字符串,都会在编译成class文件阶段,形成标志号为8(CONSTANT_String_info)的常量表 。 当JVM加载 class文件的时候,会为对应的常量池建立一个内存数据结构,并存放在方法区中。同时JVM会自动为CONSTANT_String_info常量表中的字符串常量的字面值 在堆中创建新的String对象(intern字符串对象 ,又叫拘留字符串对象)。然后把CONSTANT_String_info常量表的入口地址转变成这个堆中String对象的直接地址(常量池解析)。

2.3 拘留字符串对象

源代码中所有相同字面值的字符串常量只可能建立唯一 一个拘留字符串对象。 实际上JVM是通过一个记录了拘留字符串引用的内部数据结构来维持这一特性的。在Java程序中,可以调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象。

(1)String s=new String(“Hello world”); 编译成class文件后的指令(在myeclipse中查看):
事实上,在运行这段指令之前,JVM就已经为”Hello world”在堆中创建了一个拘留字符串( 值得注意的是:如果源程序中还有一个”Hello world”字符串常量,那么他们都对应了同一个堆中的拘留字符串)。然后用这个拘留字符串的值来初始化堆中用new指令创建出来的新的String对象,局部变量s实际上存储的是new出来的堆对象地址。

(2)String s=”Hello world”;
这跟(1)中创建指令有很大的不同,此时局部变量s存储的是早已创建好的拘留字符串的堆地址。

这也是博主在另一篇文章中提到的,如果是new String(“Hello world”),”Hello world”又是第一次出现,那么,实际上在内存中会新建两个”Hello world”。