乐趣区

方法区到底是个什么鬼

一、方法区与永久代

这两个是非常容易混淆的概念,永久代的对象放在方法区中,就会想当然地认为,方法区就等同于持久代的内存区域。事实上两者是这样的关系:

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。同时大多数用的 JVM 都是 Sun 公司的 HotSpot。在 HotSpot 上把 GC 分代收集扩展至方法区,或者说使用永久代来实现方法区。换句话说:方法区是一种规范,永久代是 Hotspot 针对这一规范的一种实现。而永久代本身也在迭代中:

在 Java 6 中,方法区中包含的数据,除了 JIT 编译生成的代码存放在 native memory 的 CodeCache 区域,其他都存放在永久代;
在 Java 7 中,Symbol 的存储从 PermGen 移动到了 native memory,并且把静态变量从 instanceKlass 末尾(位于 PermGen 内)移动到了 java.lang.Class 对象的末尾(位于普通 Java heap 内);
在 Java 8 中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize 参数失去了意义,取而代之的是 -XX:MaxMetaspaceSize。

对于 Java8,HotSpots 取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它与永久代有什么不同的?

存储位置不同,永久代是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;

存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。

二、方法区里存着什么?

既然永久代是方法区的一种实现,那么在 Hotspot 下,方法区就等于永久代,也被称为非堆。那方法区里都存着什么呢?先抛结论:

静态变量 + 常量 + 类信息(构造方法 / 接口定义) + 运行时常量池存在方法区中。

类信息与类常量池

方法区里的 class 文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。
在 Class 文件结构中,最头的 4 个字节用于存储魔数 Magic Number,用于确定一个文件是否能被 JVM 接受,再接着 4 个字节用于存储版本号,前 2 个字节存储次版本号,后 2 个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个 U2 类型的数据 (constant_pool_count) 存储常量池容量计数值,参见下图。

因此 class 文件信息和 class 文件常量池的关系如下图:

图中还包含一个运行时常量池,这个稍后会介绍。

class 文件常量池中存储了哪些内部呢?

我们写的每一个 Java 类被编译后,就会形成一份 class 文件;class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池 (constant pool table),用于存放编译器生成的各种字面量(Literal) 和符号引用(Symbolic References);

每个 class 文件都有一个 class 常量池。

动态常量池

运行时常量池是方法区的一部分,是一块内存区域。Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。

静态常量池和动态常量池的关系以及区别

静态常量池存储的是当 class 文件被 java 虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量,符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引。

动态常量池是当 class 文件被加载完成后,java 虚拟机会将静态常量池里的内容转移到动态常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

本节总结

方法区里存储着 class 文件信息和动态常量池,class 文件的信息包括类信息和静态常量池。可以将类的信息是对 class 文件内容的一个框架,里面具体的内容通过常量池来存储。

动态常量池里的内容除了是静态常量池里的内容外,还将静态常量池里的符号引用转变为直接引用,而且动态常量池里的内容是能动态添加的。例如调用 String 的 intern 方法就能将 string 的值添加到 String 常量池中,这里 String 常量池是包含在动态常量池里的,但在 jdk1.8 后,将 String 常量池放到了堆中。

三、jvm 中的常量池

在 Java 的内存分配中,总共 3 种常量池:

字符串常量池(String Constant Pool):

字符串常量池在 Java 内存区域的哪个位置?
在 JDK6.0 及之前版本,字符串常量池是放在 Perm Gen 区 (也就是方法区) 中;

在 JDK7.0 版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。

字符串常量池是什么?

在 HotSpot VM 里实现的 string pool 功能的是一个 StringTable 类,它是一个 Hash 表,默认值大小长度是 1009;这个 StringTable 在每个 HotSpot VM 的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了 StringTable 上。

在 JDK6.0 中,StringTable 的长度是固定的,长度就是 1009,因此如果放入 String Pool 中的 String 非常多,就会造成 hash 冲突,导致链表过长,当调用 String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;

在 JDK7.0 中,StringTable 的长度可以通过参数指定:

-XX:StringTableSize=66666

字符串常量池里放的是什么?

在 JDK6.0 及之前版本中,String Pool 里放的都是字符串常量;

在 JDK7.0 中,由于 String#intern()发生了改变,因此 String Pool 中也可以存放放于堆内的字符串对象的引用。关于 String 在内存中的存储和 String#intern()方法的说明,可以参考我的另外一篇博客:

需要说明的是:字符串常量池中的字符串只存在一份!

如:

String s1 = "hello,world!";
String s2 = "hello,world!";

即执行完第一行代码后,常量池中已存在“hello,world!”,那么 s2 不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给 s2.

class 常量池(Class Constant Pool):

我们写的每一个 Java 类被编译后,就会形成一份 class 文件;class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池 (constant pool table),用于存放编译器生成的各种字面量(Literal) 和符号引用(Symbolic References);

每个 class 文件都有一个 class 常量池。

运行时常量池(Runtime Constant Pool):

运行时常量池存在于内存中,也就是 class 常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()), 符号引用可以被解析为直接引用

JVM 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm 就会将 class 常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的 StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

常量池的好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。

(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。

(2)节省运行时间:比较字符串时,== 比 equals()快。对于两个引用变量,只用 == 判断引用是否相等,也就可以判断实际值是否相等。

参考文档

https://www.zhihu.com/questio…

http://blog.csdn.net/vegetabl…

https://www.cnblogs.com/holos…

https://blog.csdn.net/vegetab…

https://blog.csdn.net/u011635…

退出移动版