JVM方法区与JVM常量池

JVM方法区

  • 方法区的用途: 主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

PermGen(永久代)

  • 什么是永久代

永久代是方法区的一种实现,只有Java1.7之前的HotSpot虚拟机才有永久代,对于其他类型的虚拟机并没有永久代,方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。

在JDK1.8中HotSpot已经没有永久代这个区间,取而代之的是一个叫做Metaspace(元空间)的东西。

Metaspace(元空间)

  • 什么是元空间

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

Java8之后元数据区还保存着什么

在jdk7之后方法区四分五裂,不再是单一的在一个区域内进行存储。其中,符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,这个影响了String的intern()方法的行为。
java8依照java7的划分方法,在元数据区只存储类和类加载器的元数据信息。暂时理解为静态常量池。

Java常量池

常量池的分类

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

静态常量池

所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

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

运行时常量池

  • 运行时常量池简介

运行时常量池指jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

  • 常量池的优点

    • 常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
    • 在编译阶段就把所有的字符串文字放到一个常量池中。节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。(这个基本没用)
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
          
System.out.println(s1 == s2);  // true    1
System.out.println(s1 == s3);  // true    2
System.out.println(s1 == s4);  // false    3
System.out.println(s1 == s9);  // false    4
System.out.println(s4 == s5);  // false    5
System.out.println(s1 == s6);  // true    6
  1. 两个变量均在运行时常量池中。
  2. 编译器会将"Hel"和"lo"优化为"Hello"。转化成1的情况
  3. new String(“lo”)在堆中创建所以地址不同。
  4. s7与s8为变量,编译器不知道他们具体是什么,所以不会像2一样优化
  5. s4与s5均为在堆中创建。
  6. s5.intern()会将该字符串转化为常量池中的地址。

除了字符串常量池之外的其他运行时常量池

整型常量池、浮点型常量池(java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;两种浮点数类型的包装类Float,Double并没有实现常量池技术) 等等,但都大同小异,只不过数值类型的常量池不可以手动添加常量,程序启动时常量池中的常量就已经确定了,比如整型常量池中的常量范围:-128~127,(Byte,Short,Integer,Long,Character,Boolean)这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。

### JVM 方法区中运行时常量池的工作机制作用 #### 1. 运行时常量池的概念 运行时常量池(Runtime Constant Pool)是方法区的一部分,它用于存储类文件中的常量信息。这些信息包括但不限于字面量(literals)、符号引用(symbolic references),以及通过某些方式动态生成的常量[^2]。 #### 2. 运行时常量池的作用 运行时常量池的主要作用在于支持程序运行时所需的各类常量资源。具体来说: - 它保存了由编译器生成的各种字面量和符号引用。 - 提供了一个可扩展的空间,允许在运行期间动态加入新创建的常量,比如通过 `String.intern()` 方法产生的字符串实例[^3]。 #### 3. 动态性特点 其他类型的常量池相比,运行时常量池的一个显著特点是其 **动态性**。这意味着不仅限于预先定义好的常量可以存入其中,在程序执行过程中还可以新增加一些未预料到的数据项。例如当调用 `String.intern()` 函数时,如果该字符串尚未存在字符串常量池,则会将其添加进去。 #### 4. 存储内容 运行时常量池所包含的具体内容有以下几个方面: - 字面量:如整数、浮点数等基本数据类型值; - 符号引用:指向其他对象或者方法的名字及其描述符; - 动态生成的新常量:像上述提到过的经由`intern()`操作引入的字符串实例。 #### 5. 可能引发的问题——内存溢出 由于运行时常量池位于方法区内,因此它的大小受到整个方法区域容量的影响。一旦超出设定界限就会抛出OutOfMemoryError错误提示。这种情况可能发生在频繁加载卸载大量临时性的class文件或是滥用 intern() 方法导致过多独一无二的字符串驻留等情况之下[^4]。 ```java // 示例代码展示如何可能导致 OOM 错误 public classOOMExample { public static void main(String[] args){ Set<String> set = new HashSet<>(); try{ while(true){ String str = UUID.randomUUID().toString(); set.add(str.intern()); } }catch(OutOfMemoryError e){ System.err.println("Out of Memory Error!"); } } } ``` 此段代码不断生成随机UUID并将它们转换成interned形式存放到HashSet里头,最终因为持续增加的独特字符串填满运行时常量池而导致 OutOfMemoryError 发生。 #### 总结 综上所述,JVM 中的方法区内的运行时常量池是一个非常重要的组件,负责管理应用程序运行所需的一系列静态及动态生成的常量资料。了解其工作机制对于优化性能、预防潜在风险具有重要意义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值