一、
1. 基于JDK1.8的String intern()方法解析
intern方法 1.8:
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,则放入成功
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
1. 示例一
String s2 = s.intern();
当执行
String s2 = s.intern();
时,会遵循以下逻辑:
- 如果字符串
s
已经在常量池中存在,那么s2
会被设置为指向常量池中已存在的相同字符串的引用。- 如果字符串
s
不在常量池中,它的内容会被复制到常量池,并且s2
被设置为指向这个新添加到常量池的字符串的引用。这段代码的主要目的是确保
s2
是常量池中与s
相同内容的字符串的一个引用,这样可以优化内存使用,特别是当你有一个可变的对象但希望它在后续的操作中像一个不可变的常量一样行为时。总结:不管放入成功还是失败,s2始终是字符串常量池中的对象。
2. 示例二
String s = new String("a")+new String("b"); String s2 = s.intern(); System.out.println(s == "ab");//true System.out.println(s2 == "ab");//true
执行第一行时,创建的s对象是堆中的。
执行第二行时,字符串常量池中还没有与s相同内容的字符串对象,即还没有“ab”对象,则放入成功,于是第三行输出为true,因为已经放入成功了。(注意:这里的“放入成功”是指将堆中的s对象放入到了字符串常量池中了,堆中不再有s对象了)
3. 示例三
String s1 = "ab"; String s = new String("a")+new String("b"); String s2 = s.intern(); System.out.println(s == "ab");//false System.out.println(s2 == "ab");//true
执行第三行时,字符串常量池中已经存在与s相同内容的字符串对象,即已有“ab”对象,则放入失败,于是第四行输出为false,因为放入失败了。至于s2,不管放入成功还是失败,都会是字符串常量池中的常量“ab”指向它的一个引用。
注意:JDK1.6的intern()方法与JDK1.8有不同哦。不同之处在于,1.6中放入时,如果串池中没有,则拷贝一份放入串池,相当于此时堆中和串池各有一份。
三、问GitCode
1. JVM 方法区
JVM 方法区,也被称为非堆(Non-Heap),是Java虚拟机的内存区域之一,主要用于存储已由虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它是线程共享的,这意味着所有线程都会访问同一个方法区。
主要特点:
- 存储内容包括类型信息、域(Field)信息、方法(Method)信息、运行时常量池等。
- 方法区在JVM启动时创建,其物理内存空间可以是不连续的。
- 大小可固定或可扩展,但过度填充会导致内存溢出,如
java.lang.OutOfMemoryError: PermGen space
(JDK 7及之前)或java.lang.OutOfMemoryError: Metaspace
(JDK 8及之后)。- 类只能被加载一次,保证了类的全局唯一性。
发展历程:
- 在早期的HotSpot JVM中,方法区被称为永久代(Permanent Generation),使用特定的空间进行管理。
- JDK 8开始,永久代被元空间(Metaspace)取代,元空间主要利用操作系统的native memory,允许更大的灵活性和更好的性能。
内存分配:
- 字符串常量池在JDK 7之前位于永久代,JDK 8后移到了Java堆中。
- 静态变量存储在方法区内,对于
static final
类型的变量,如果其值是编译期常量,则会放入运行时常量池。垃圾回收:
- 方法区中的一些内容,如不再使用的类、无引用的常量,会被垃圾收集器清理,这称为类卸载。
- 类卸载条件较为苛刻,通常发生在系统资源紧张时。
相关问题:
- 方法区和Java堆的主要区别是什么?
- 为什么在JDK 8中要将字符串常量池从永久代移出来?
- 什么是运行时常量池,它与字面量池有何联系?
- 系统如何判断一个类是否可以被卸载?
- 如何通过配置JVM参数调整方法区(元空间)的大小?
2. JDK1.7和JDK1.8对应的方法区分别存哪些内容?★★★★★
在JDK 1.7中,方法区(也称作永久代)主要存储以下内容:
- 类的信息:如类名、超类名、接口实现等。
- 域(Field)信息:类的成员变量及其相关信息。
- 方法信息:类的方法签名、访问权限等。
- 运行时常量池:存储各种常量,包括字符串常量、数字常量以及符号引用等。
- 类装载器和权限验证相关数据。
- 静态变量:类级别的变量。
- 编译后的字节码指令。
而在JDK 1.8中,由于永久代被元空间(Metaspace)替代,存储的内容大致相同,但有以下几个变化:
- 字符串常量池被移出方法区,现在存储在Java堆中。
- 元空间主要利用操作系统内存,而不是JVM堆的一部分,因此避免了永久代可能导致的内存溢出问题。
- 其他如类信息、字段信息、方法信息、运行时常量池(不含字符串常量)等仍然存储在元空间中。
JVM JDK1.8中静态变量保存在哪里?
在JDK 1.8中,静态变量属于类的一部分,因此它们存储在方法区域(Metaspace)中,这个区域也被称为元空间。元空间主要存储类的元数据,包括类的结构信息、方法信息以及静态变量。当类加载时,这些静态变量会被加载到元空间,并且在所有实例化对象之前就可以访问。
JVM JDK1.8对应的方法区分别存哪些内容?
在JDK 1.8中,原本的方法区(Method Area)被概念上替换为元空间(Metaspace)。以下是JDK 1.8中元空间的主要内容:
- 类的版本信息(major version number)
- 类名、接口名
- 父类的全限定名
- 接口的直接父接口
- 常量池(包括字符串常量池,但现在字符串常量池已被移动至堆中)
- 字段的名称、类型、访问权限
- 方法的名称、返回类型、参数类型、访问权限、是否是桥接方法、是否是默认方法等
- 访问标志,用于判断某个类或方法是否可被访问
- 类装载器信息
综上,方法区存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译出来的代码等数据。只是JDK1.8及之后,字符串常量池不再放在方法区了,而是放到了堆中。
3. 字符串常量池和运行时常量池有什么区别?
字符串常量池和运行时常量池都是JVM内存模型的一部分,它们的区别在于作用范围和包含的内容:
作用范围:
- 字符串常量池:主要存储字面量字符串(比如
"hello"
)以及用String.intern()
方法添加的字符串。- 运行时常量池:属于方法区的一部分,存储编译期生成的各种字面量和符号引用,这些字面量包括但不限于数字、字符串、方法签名等。
引入时间:
- 字符串常量池:在程序执行前就可能存在,如静态初始化块中的字符串。
- 运行时常量池:随着类加载到JVM中而创建,随着类的卸载而销毁。
内容类型:
- 字符串常量池:仅包含字符串。
- 运行时常量池:包含各种字面量和符号引用,不只是字符串。
优化行为:
- 字符串常量池:通过
intern()
方法可以保证同一字符序列的字符串只存在于池中的一次,提高内存效率。- 运行时常量池:解析符号引用为直接引用,优化方法调用速度。
4. 字符串常量池在JDK 1.8中的位置变化带来什么影响?
在JDK 1.8中,字符串常量池的位置从永久代移动到了堆内存中,这一改变带来了以下影响:
- 内存分配更加灵活:使用堆内存来存储字符串常量使得内存管理更为灵活,可以根据程序需要动态调整大小,降低了永久代溢出的风险。
- 更少的垃圾回收压力:在元空间中,主要是存放类的元信息,这些信息在类加载时创建且生命周期与类相同,不会频繁变动。而字符串常量在堆中则会随着对象的创建和销毁进行GC,减少了对元空间的垃圾回收压力。
- 提高性能:对于字符串拼接操作,JDK 1.8会更早地将它们放入堆中,当字符串连接的结果是常量时,可以直接指向常量池,提升了性能。
- 优化内存使用:由于元空间主要使用的是 Native Memory,移动字符串常量池到堆中可以节省这部分系统内存资源。
- 减少了 PermGen OutOfMemoryError 的出现:在JDK 1.7中,如果字符串常量池占用的空间过大,可能会导致永久代溢出,而在JDK 1.8中这种问题得到了缓解。
5. 为什么说JDK 1.8的字符串常量池优化了内存使用?
在JDK 1.8中,字符串常量池位于堆内存而不是元数据区域(元空间),这优化了内存使用的几个方面:
按需分配:堆内存是Java应用程序的主要内存区域,其大小可以根据应用需求动态扩展。这意味着字符串常量池也可以随着程序运行按需分配空间,避免一次性分配过多内存。
减少元空间压力:元空间主要用于存储类的元数据,如方法和字段的信息。将字符串常量池移出元空间减轻了这个区域的压力,减少了因元空间满而引发的内存溢出风险。
更有效的垃圾回收:堆内存中的对象更容易被垃圾收集器处理。一旦不再有引用指向某个字符串实例,垃圾收集器就会回收这部分内存,提高了内存利用率。
更佳的内存碎片管理:堆内存在垃圾回收过程中会进行整理,降低内存碎片,从而提高整体的内存使用效率。
与对象共享:JDK 1.8中,如果两个不同的字符串变量具有相同的值,那么它们在堆中只会有一个实例,这是通过 intern() 方法实现的。这样可以有效地避免重复的对象创建,节约内存。