【JVM】JVM常量池

目录

JVM常量池

什么是字面量?

什么是符号引用?

字符串常量池

静态常量池与运行时常量池

静态常量池

运行时常量池

元空间

​编辑

元空间与永久代有什么不同?

为什么要废弃永久代,引入元空间?

方法区大小设置和OOM

如何解决OOM

引用:


JVM常量池

什么是字面量?

字面量是指由字母、数字等构成的字符串或者数值常量。

字面量就是给变量赋予的一个常量值。

比如下面的例子,等号右边的值都是字面量

  • 数值字面量:int num = 10;, double pi = 3.14;
  • 字符串字面量:String message = "Hello, World!";
  • 布尔字面量:boolean flag = true;

什么是符号引用?

符号引用是编译原理中的概念,是相对于直接引用来说的。

主要包括了以下三类常量:

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

类符号引用的示例:

比如Test类中的instance、num都是字段名称,就是一种符号引用,还有常量池中的com/lizhi/Test就是类的全限定名,main和test是方法名,()V是一种UTF8格式的描述符,这些统统都是符号引用。

// 引用java.util.List类

import java.util.List;

在上述代码中,java.util.List是类符号引用。它们表示对应的类,并在编译和运行时用于加载和操作这些类。

字段符号引用的示例:

// 引用自定义类的字段

public class MyClass {

        private int myField;

}

MyClass myObject = new MyClass(); i

nt fieldValue = myObject.myField;

方法符号引用的示例:

// 调用String类的toUpperCase方法

String str = "hello";

String upperCaseStr = str.toUpperCase();

这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载或运行时会被转变为被加载到内存区域的代码的直接引用,也就是动态链接。例如,test()这个符号引用在运行时就会被转变为test()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。

字符串常量池

在JKD1.6及以前,字符串常量池是方法区的一部分,jdk1.7及以后,字符串常量池放在java堆中。

字符串常量池是Java运行时常量池比较特殊的一块空间,在JDK7以后,字符串常量池的内存区域从之前的永久代就移动了堆内存,也就是逻辑上字符串常量池是运行时常量池,属于方法区的一部分,但实际的内存区域是堆空间。

到了JDK8以后取消了永久代,取而代之的是元空间(方法区),元空间使用的直接内存(堆外内存),里面包含了运行时数据区,但运行时数据区的常量以及字符串常量池是在堆中的,这一块需要区分清楚。

运行时与class常量池一样,运行时常量池也是每个类都有一个,但是字符串常量池在jvm中只有一个。为了提高匹配的速度,即更快的查找某字符串是否存在常量池中,java在设计字符串常量池时,设计了stringtable结构,stringtable类似hashtable,保存字符串的引用。

存放字符串到stringtable原理:hash函数计算出字符串的hashcode,生成字符串Node节点(包含hash值,字符串(key)),hash值根据路由算法放入stringtable表的数组中,如果hash冲突则相同的hash值的字符串变成链表结构,字符串放入链表中。

根据字符串hash映射成hashcode,通过路由算法查找stringtable表,如果没有hash冲突,直接返回查找到的Node节点引用;如果冲突,查找node节点链表,然后遍历链表查找到对应的node。

字符串常量池的设计思想

字符串属于引用类型,但是在Java中字符串的使用频率与基本数据类型差不多,甚至更高。如果与其他的引用类型一样,每个字符串都分配大量的额外空间,会导致时间和空间的双重浪费,同时也会极大地影响程序的性能。

JVM为了提供性能和减少字符串引用类型的内存开销,在实例化字符串常量的时候做了大量的优化:

为字符串开启了一块独立的内存空间,字符串常量池,类似于缓冲区。

创建字符串常量时,首先查询字符串常量池中是否存在该字符串(equal)

如果存在该字符串,直接返回引用实例;如果不存在,实例化该字符串并放入常量池中。

字符串常量池(string table,字符串字面量)为什么要调整到堆空间?

jdk7中将StringTable放到了堆空间中,因为永久代的回收效率很低,在full gc的时候才会触发。而full GC是老年代的空间不足,永久代不足是时才会触发。

这就导致了StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

字符串创建与内存分配

1、直接赋值

String a = "hello";

最多创建一个字符串对象。

编译器会在字符串常量池中检查是否存在值为 "hello" 的字符串对象。如果不存在,它会在字符串常量池中创建一个新的字符串对象,并将其引用赋给变量 a。如果字符串常量池中已经存在值为 "hello" 的字符串对象,那么变量 a 将会引用该已存在的对象。

2、new String()

String a = new String("hello");

最多创建两个字符串对象。

第一个对象是字符串常量 "hello",它存储在字符串常量池中,因为在这里使用了字符串常量 "hello",所以它已经存在于常量池中,不需要再创建。

第二个对象是堆内存中的一个新的字符串对象,它是通过 new String() 构造函数创建的,其中传入的参数是字符串常量 "hello" 的引用。因此,这个新的字符串对象与常量池中的字符串对象内容相同,但它们在内存中是不同的两个对象。

3、intern()方法

String.intern()方法的作用如下:

  1. 查询字符串常量池:intern()方法会在字符串常量池中搜索具有相同内容的字符串。如果找到相同内容的字符串,则返回字符串常量池中该字符串的引用。
  2. 添加到字符串常量池:如果字符串常量池中不存在具有相同内容的字符串,intern()方法会将当前字符串添加到字符串常量池中,并返回字符串常量池中新添加的字符串的引用。

使用intern()方法可以实现字符串对象的共享,以节省内存并提高字符串比较的效率。通过共享字符串对象,可以避免创建多个相同内容的字符串对象,而是重复利用已存在的字符串对象。

String str_1 = new String("hello");
String str_2 = new String("hello");
String str_3 = "hello";
System.out.println(str_1 == str_2);                      //false
System.out.println(str_1 == str_2.intern());             //false
System.out.println(str_1.intern() == str_2.intern());    //true
System.out.println(str_1 == str_3);                      //false
System.out.println(str_1.intern() == str_3);             //true

总结:

1、new String() 创建(new)出来的对象地址肯定不同。

2、intern() 比较的是字符串常量池的值。

3、String str_3 = "hello" 与 str_1.intern() 同样直接引用常量池里的值。

包装类与对象池

Java中八种基本数据类型的包装类基本都实现了缓存技术,目的也是为了避免重复创建过于的引用,这其中就包含Byte、Short、Integer、Long、Character、Boolean,而另外两种包装类型Float和Double则没有实现常量池技术,原因在于这两种数据类型的数值是很随意的,就算有常量池命中率也不会高,还浪费额外的堆内存。

静态常量池与运行时常量池

静态常量池

静态常量池也可以称为Class常量池,也就是每个.java文件经过编译后生成的.class文件,每个.class文件里面都包含了一个常量池,因为这个常量池是在Class文件里面定义的,也就是.java文件编译后就不会在变了,也不能修改,所以称之为静态常量池。

静态变量存放在哪?

结论:静态引用对应的对象实体始终都是放在堆空间的。

静态变量(静态引用)在jdk7及其以后版本的Hotspot虚拟机选择把静态变量于类型在Java语言一端的映射class对象(静态变量应用的对象实例)存放在一起,存储在Java堆之中。

运行时常量池

方法区,内部包含了运行时常量池。字节码文件,内部包含了常量池。

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项常量池表(Constant Pool Table),包括各种字面量(通常是字符串)和对类型、域和方法符号的引用。

为什么需要一个常量池呢?

一个Java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。总结起来,其实就是对真实数据的一个符号引用(#xxx),像图纸一下,运行时把符号动态替换成真实数据。

运行时常量池是方法区的一部分。常量池表示Class文件的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池(每个类都有一个运行时常量池)。

JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。

运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实的地址。

运行时常量池,相对于Class文件常量池的另一个重要特征是:具备动态性(存放的信息比字节码中的常量池信息更多)

java语言并不要求常量一定只有编译期才产生的,也就是并非预置入class文件中常量的内容才能进入方法区运行时常量池,运行期也可能将新常量放入池中,这种特性开发人员使用较多的是String类的intern()方法。

运行时常量池类似于传统编程语言中的符号表(symbol table),但是它所包含的数据却比符号表要更加丰富一些。

当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所提供的最大值,则JVM会抛出OutOfMemoryError异常。

方法区运行时常量池、程序计数器、虚拟机栈配置实例

总结:运行时常量池,在加载类和接口到虚拟机之后,就会创建对应的运行时常量池,每一个类都有一个运行时常量池。运行时常量池,相对与class文件常量池(静态常量池)另一个重要特征是:具备动态性(存放的信息比字节码中的常量池信息更多)

常量不一定只在编译的时候才产生的,运行期也可能将新的常量放入池中。

元空间

在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代(方法区的落地实现)来进行垃圾回收。

而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。

元空间与永久代有什么不同?

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

存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。

为什么要废弃永久代,引入元空间?

1、元空间的最大可分配空间就是系统可用内存空间,

在某些场景下,如果动态加载类过多,容易产生Perm 区的OOM。

在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。

2、对永久代进行调优是很困难的。full GC 严重影响性能。

永久代会为GC带来不必要的复杂度,并且回收效率偏低。

总结:元空间相比永久代,元空间使用的是本地内存,并将静态变量和常量池等并入堆中

废除永久代的好处

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。

将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。

将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。

永久代为什么要被元空间替换?

随着java8的到来,hotspot VM中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。

这项改动是很有必要的,原因有:为永久代设置空间大小是很难确定的。

在某些场景下,如果动态加载类过多,容易产生Perm 区的OOM。比如某个时间的Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误:

java.lang.OutOfMemoryError:PermGen space

而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制。对永久代进行调优是很困难的。full GC 严重影响性能。放在本地内存不会影响Jvm在运行时的性能,因为都是放在本地内存中,而不是放在本地磁盘上。

方法区是一种概念,永久代和元数据区是对应的实现。

在JDK1.7之前 JVM的方法区的实现是永久代,JDK1.7之后JVM废除永久代,使用元数据区作为方法区的实现,jdk8的方法区将静态变量和字符串常量池存放在堆中,类信息与静态常量池,运行时常量池存放在直接内存中,直接内存指的是服务器的内存。

元空间与永久代最大的区别在于:元空间不在JVM虚拟机内存中,而是使用本地内存。《java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做NoN-Heap(非堆),目的就是要和堆分开。所以,方法区看作是一块独立与Java堆的内存空间。

方法区大小设置和OOM

方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。

jdk7及以前:

通过-XX:PermSize来设置永久代初始化分配空间,默认值是20.75M。

-XX:MaxPermSize来设置永久代最大可分配空间。32位机器默认是64M,64为机器默认是82M。

当JVM加载的类信息容量超过了这个值,会报一次OutOfMemoryError:PermGen space

jdk8及以后:

元空间大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。

默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。

与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生异常,虚拟机一样会抛出异常OOM:Metaspace

-XX:MetaspaceSize设置初始的元空间大小,对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被处罚并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置,新的高水位线的值取决于GC后释放了多少元空间,如果释放的空间不足,那么在不超过MaxMetaspaceSize时,使得提高该值,如果释放空间过多,则适当降低该值。

如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收的日志可以观察到Full GC多次调用,为了避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。

如何解决OOM

要解决OOM异常或者heap space的异常,一般的手段是首先通过内存映像分析工具对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(memory leak)还是内存异常(memory overflow)。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确的定位出泄漏代码的位置。

如果不存在内存泄漏,换句话说就是内存中的对象确实还必须活着,那就应当检查虚拟机堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码检查是否存在某些生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

引用:

详解JVM的常量池_jvm 常量池-CSDN博客

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值