JVM系列之HotSpot虚拟机对象、字节码执行引擎、StringTable

1、对象的实例化、内存布局与对象的访问定位

1.1、对象的实例化

1.1.1、创建对象的方式
  • new :最常见的方式,变形:Xxx的静态方法,XxxBuilder/XxxFactory的静态方法
  • Class的newInstance:JDK9标记过时,反射的方式,只能调用空参的构造器,权限必须是public
  • Constructor的newInstance:反射的方式,可以调用空参,带参的构造器,权限没有要求
  • 使用clone:不调用任何构造器,当前类需要实现Cloneable接口,实现clone()方法
  • 使用反序列化:从文件、网络等获取一个对象的二进制流
  • 第三方库Objenesis
1.1.2、创建对象的步骤
1.1.2.1、判断对象对应的类是否加载、链接、初始化

当虚拟机遇到一条字节码new等创建实例对象的指令时。首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、连接、初始化过,如果没有初始化,在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为key值进行查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常。如果找到了.class文件,进行类的初始化过程

1.1.2.1、为对象分配内存

首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小

1.1.2.1.1、分配内存的两种方式(指针碰撞、空闲列表)
  • 空闲列表(内存不规整)
    1. 如果Java堆内存不规整,JVM虚拟机就必须维护一个列表,记录哪些内存可用,哪些不可用
    2. 分配的时候在列表中找一个足够大的空间分配,然后更新列表
    3. 这种分配方式叫空闲列表(Free List)
  • 指针碰撞(内存规整)
    1. 假设Java 堆中内存时绝对规整的,所有被使用过的内存放在一边,空闲的内存放在另一边,中间放一个指针作为分界点指示器
    2. 那么内存分配就是指针指向空闲的方向,挪动一段与对象大小相等的距离
    3. 这种分配方式成为指针碰撞(Bump The Pointer)

选择哪种分配方式由Java堆是否规整决定,Java堆是否规整由所采用的的垃圾收集器是否带有空间压缩整理(Compact)的能力决定,当使用Serial、ParNew等带有压缩整理过程的收集器,指针碰撞简单高效,当使用CMS基于清除(Sweep)算法收集器时,只能采用空闲列表来分配内存,CMS为了能在多数情况下分配内存更快,设计了一个Linear Allocatioin Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区后,在它里面仍可使用指针碰撞方式分配

1.1.2.2、处理并发安全问题

对象创建是非常频繁的行为,还需要考虑并发情况下,仅仅修改一个指针所指向的位置也是不安全的,例如正在给对象A分配内存,指针还未修改,对象B又使用原来的指针分配内存。解决问题有两种可选方案:

  • 对分配内存空间的动作进行同步处理。实际上虚拟机采取CAS配上失败重试的方式保证更新操作的原子性
  • 把内存分配的动作按照线程划分到不同的空间中进行,每个线程在Java堆中,预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX: +|-UseTLAB参数来设定
1.1.2.3、初始化分配到的空间

内存分配完成后,虚拟机将分配到的内存空间(不包括对象头)都初始化为零值。如果使用了TLAB,这个工作可以提前到TLAB分配时进,这步操作保证对象的实例字段在Java代码中,可以不赋初始值就直接使用,程序可以访问到字段对应数据类型所对应的零值。

1.1.2.4、设置对象的对象头

接下来Java虚拟机还要对对象进行必要的设置,例如对象是哪个类的实例、如何才能找到类的元数据信息,对象的哈希码(实际上对象的HashCode会延后真正调用Object的hashCode()方法时才计算)、对象的GC分代年龄等信息,这些信息存放到对象的对象头(Object Header)。

1.1.2.5、执行类的构造器()方法进行初始化

上面工作完成后,从虚拟机角度来说,一个新的对象已经产生了,但是从Java程序的视角来说,对象创建才刚刚开始,对象的构造方法(Class文件中()方法)还未执行,所有字段都是默认的零值,new指令之后接着执行()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全构造出来。

1.2、对象的内存布局

在这里插入图片描述
对象的内存布局(对象头、实例数据、对齐填充)

1.2.1、对象头

包含三部分(运行时元数据、指向方法区中的KClass对象、如果为数组需要有数组的长度(数据类型为int))

1.2.1.1、运行时元数据(Mark Word对象标记)

在这里插入图片描述
这部分数据的长度在32位和64位的虚拟机分别是4字节和8字节,官方称为Mark Word运行时元数据

  • 哈希值
  • GC分代年龄
  • 锁状态标志
  • 线程持有的锁
  • 偏向线程ID
  • 偏向时间戳

对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,根据对象状态的不同,MarkWord可以复用自己的空间

1.2.1.2、类型指针(开启指针压缩4字节,未开启8字节)

即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确认该对象属于哪个类的实例,该指针指向方法区中类的KClass对象,对象头已经精心设计为8字节的整数倍,1倍或者2倍

说明:如果是数组,还需要记录数组的长度,4字节

1.2.2、实例数据

对象的实例数据部分,是对象的真正存储的有效信息,即我们在程序代码中定义的各种类型的字段,无论是父类继承下来,还是子类中定义的字段都要记录下来,这里不包括静态字段,因为静态字段属于类,而不属于类的实例对象。

为字段分配内存的策略:

  • 这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响
  • 分配策略参数-XX:FieldsAllocationStyle=1
  • HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
  • 从默认的分配策略中可以看出,相同宽度的字段总被分配到一起存放
  • 在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前
  • 如果HotSpot虚拟机的+XX:+CompactFields参数(默认开启),那么子类中较窄的变量也允许插入父类变量的空隙之间,以节省一点点空间
1.2.3、对齐填充

对其填充,这并不是必然存在,没有特别的意义,它仅仅起着占位符的作用,因为HotSpot虚拟机自动内存管理系统,要对对象的起始地址必须是8字节的整数倍,换句话就是任何对象的大小都必须是8字节的整数倍,对象实例数据部分如果没有对齐的话,就需要通过对其填充来补全。

1.2.4、Java对象指针压缩

什么是对象指针压缩(Ordinary Object Pointers Compress)

  1. jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
  2. JVM配置参数:UseCompressedOops,compressed–压缩、oop–对象指针
  3. 启用指针压缩:-XX:+UseCompressedOops,禁止指针压缩:-XX:-UseCompressedOops
  • 如果开启了指针压缩,那么引用数据类型的指针大小就是4个字节共32位。如果进行内存寻址,最大只能访问4G(2的(4 * 8)次方 / 1024 / 1024 / 1024 )的内存。堆内存如果大于4G,那么如何访问4G之外的内存呢?
  • Java对象的大小必须是8字节的整数倍,如果是2进制表示,最后三位必然都是000,那么就可以将32位进行无符号左移3位,变成35位。那么可以最大内存寻址的范围就是32G
  • 举例一下,假如对象实际偏移量为24字节,那么指针只存储3字节,实际内存寻址的时候,进行无符号左移3位,其实就是乘以8,得到24,达到节约内存的目的。其实这也是时间换空间思想的体现,利用CPU的计算资源,减少内存消耗
  • 如果堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对Java对象寻址
  • 如果堆内存小于4G,就没必要使用指针压缩了

1.3、对象的访问定位

对象的访问定位(句柄和直接指针)

1.3.1、使用句柄

使用句柄,Java堆中将划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄包含对象实例数据与类型数据各自的具体信息
在这里插入图片描述

1.3.2、直接指针

使用指针,reference中存储的直接就是对象地址,如果访问对象本身,不需要多一次的间接访问的开销
在这里插入图片描述

1.3.3、两种方式各有优势
  • 使用句柄最大好处是reference中存放的是稳定句柄地址,在对象被移动(垃圾回收时会产生)时只改变句柄中实例数据指针,reference本身不用改变
  • 使用指针最大好处就是速度快,节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,所以积少成多也是一项可观的执行成本
  • HotSpot主要是用指针,进行对象访问(例外情况,如果使用Shenandoah收集器的话,也会有一次额外的转发)

2、字节码执行引擎

在这里插入图片描述
执行引擎是Java虚拟机核心的组成部分之一,虚拟机的执行引擎由各大厂商自行实现,物理机的执行引擎是操作系统层面上,能够执行不被硬件直接支持的指令格式

在这里插入图片描述

  1. 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
  2. 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址
  3. 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息

2.1、Java代码编译和执行过程

大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过下图中的各个步骤
在这里插入图片描述

2.1.1、为什么说Java是半编译半解释型语言?

JVM在执行Java代码的时候,通常会将解释执行与编译执行二者结合起来进行。

2.1.2、Javac编译器的执行过程(Javac编译器完全使用Java语言编写,这种用自己的语言写的编译器来编译自己的方式叫自举)

在这里插入图片描述

2.1.3、Java字节码的执行流程

在这里插入图片描述

2.1.4、各种JVM语言源代码、编译器、字节码、解释器、JIT、机器码的关系

在这里插入图片描述

2.1.5、机器码、指令、汇编语言、字节码
2.1.5.1、机器码
  • 各种采用二进制编码方式表示的指令,叫做机器指令码,机器指令与CPU紧密相关,不同种类的CPU所对应的机器指令也就不同
2.1.5.2、指令
  • 由于机器码由01组成,可读性太差。
  • 于是人们发明了指令,指令就是把机器码特定的0和1序列,简化成对应的指令,一般为英文编写如mov、inc等,可读性稍好。
  • 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同。所以不同的硬件平台的同一种指令,对应的机器码也可能不同
2.1.5.3、指令集
  • 不同硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集
  • x86指令集,对应的x86架构的平台
  • ARM指令集,对应的是ARM架构的平台
2.1.5.4、汇编
  • 由于指令的可读性太差,于是又有了汇编语言
  • 汇编语言用助记符代替机器指令的操作码,用地址符号或标号,代替指令或操作数的地址
  • 汇编语言要翻译成机器指令码,计算机才能识别和执行
2.1.5.5、字节码
  • 字节码是一种中间状态的二进制代码,它比机器码更加抽象,需要直译器转义后才能完成机器码
  • 字节码主要为了实现特定软件运行和软件环境,与硬件环境无关
  • 字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以执行的指令。典型的应用为Java bytecode

在这里插入图片描述

2.1.6、解释器
  • 当Java虚拟机启动时,会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行
  • 解析器真正意义上所承担的角色就是一个运行时翻译者,将字节码文件中的内容翻译为对应的平台的本地机器指令执行
  • 当一条字节码指令被解释执行完成后,接着在根据PC寄存器中的记录下一条需要被执行的字节码执行解释执行
  • 现在普遍使用的模板解释器,模板解释器将每一条字节码和一个模板函数相关联,模板函数直接产生这条字节码执行时的机器码,提高解释器的性能
  • HotSpot中
    1. Interpreter模块,实现了解释器的核心功能
    2. Code模块,用于管理HotSpot在运行时生成的本地机器指令
2.1.7、JIT编译器(Just In Time)

就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言,JVM平台支持一种叫做即时编译的技术,目的是避免解释执行,而是将整个函数体编译成机器码,每次函数执行时,只执行编译后的机器码即可,使执行效率大幅提升

2.1.8、JVM 为什么既使用解释器又使用JIT编译器
  • 首先程序启动后,解释器可以马上发挥作用,省去编译时间,立即执行
  • 编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地机器码后执行效率更高
  • 对于服务端应用,启动时间并非关注重点,但是对于看重启动时间的应用场景,就需要找到一个平衡点
  • 当Java虚拟机启动时,解释器可以首先发挥作用,而不是等待即时编译器全部编译完成后再执行,这样可以省去很多不必要的编译时间,随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
2.1.9、编译器的分类
  • 前端编译器:把.java文件转换为.class文件的过程,sun的Javac编译器
  • 后端运行期编译器:把字节码转为机器码的过程,JIT编译器:HotSpot的C1、C2编译器
  • 静态提前编译器:Ahead of Time Compliler AOT,直接把.java文件编译器本地机器代码(可直接运行的二进制文件)的过程,GNU Compiler for the Java(GCJ)
2.1.10、JIT简单介绍

需要根据代码被调用执行的频率而定,需要被编译为本地代码的字节码,也称之为热点代码,JIT编译器会在运行时针对频繁调用的热点代码做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能,一个被多次调用的方法,或者一个方法体内部循环次数较多的循环体,都可以被称之为热点代码,因此可以通过JIT编译器编译为本地机器指令,由于这种编译方法发生在方法的执行过程中,因此也被称之为栈上替换,OSR On Statck Replacement

2.1.10.1、一个方法调用都少次才能达到标准?

这个依靠热点探测功能,HotSpot采用的基于计数器的热点探测

2.1.10.1.1、方法调用计数器

在这里插入图片描述

  • 统计方法调用次数
    1. 默认阈值,Client模式下是1500次,Server模式下是10000次
    2. -XX:CompileThreshold
  • 热度衰减
    1. 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数
    2. 如果超多一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器,那么方法调用计数器的值就会减少一半,这个过程称之为方法调用计数器热度衰减,而这个时间值就是半衰周期
    3. 进行热度衰减的动作是在JVM进行垃圾收集时顺便执行的,可以使用-XX:-UseCounterDecay来关闭热度衰减
    4. 另外可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒
2.1.10.1.2、回边计数器

统计循环体执行的循环次数
在这里插入图片描述

2.1.10.2、HotSpot中JIT分类

内嵌两个JIT编译器,client ,server,大多情况下简称C1、C2

  • -client:指定Java虚拟机在Client模式下,并使用C1编译器,C1编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度
    1. 方法内联:将引用的函数代码编译到引用点处,减少栈帧的生成,减少参数传递以及跳转过程
    2. 去虚拟化:对唯一的实现类进行内联
    3. 冗余消除:在运行期把一些不会执行的代码折叠掉
  • -server:指定虚拟机在server模式下,并使用C2编译器,C2进行耗时较长的优化,以及激进优化,单优化后的代码执行效率更高,逃逸分析是优化的基础,基于逃逸分析在C2上有几种优化
    1. 标量替换:用标量值代替聚合对象的属性值
    2. 栈上分配:对于未逃逸的对象分配在栈而不是堆
    3. 同步消除:清除同步操作,通常指synchronized

随着JIT编译器的存在,可以在项目启动时,使用压测工具对程序进行代码预热,提高程序的运行效率,JDK9引入了AOT编译器,JDK10起,HotSpot又引入了个全新的即时编译器Graal编译器

2.1.11、HotSpot可以设置程序执行的方式
  • -Xint完全采用解释器模式执行
  • -Xcomp完全采用即时编译器模式执行,如果即时编译器出现问题,解释器会介入执行
  • -Xmixed采用解释器+即时编译器的混合模式共同执行
  • 默认使用混合模式Xmixed

3、StringTable

3.1、StringTable为什么要调整位置?

JDK7中将StringTable放到了堆空间中,因为永久代的回收效率很低。在FullGC的时候才触发,而FullGC是老年代空间不足,永久代不足时才触发,这就导致了StringTable回收效率不高,而我们开发中会创建大量的字符串,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存

3.2、String的基本特性

  • 字符串,用""引起来表示
  • 声明为final的不可被继承的
  • 实现了Serializable接口,表示支持序列化
  • 实现了Comparable接口,表示可以比较大小
  • JDK8及以前,内部定义了final char[] value用于存储字符串数据
  • JDK9时改为byte[]字节数组
    1. char数组一个char占2字节,String是堆空间的主要部分,大部分是latin-1字符,一个字节就够了,这样会有一半空间浪费
    2. 中文等UTF-16 的用两个字节存储。
    3. StringBuffer、StringBuilder同样做了修改
  • String代表不可变的字符序列,简称不可变性
    1. 当字符串重新赋值,需要重写指定内存区域赋值,不能使用原有的value进行赋值
    2. 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能对使用原有的value进行赋值
    3. 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
    4. 由于String的不可变性,因此String也是线程安全的对象
  • 通过字面量的方式,区别与new给一个字符串赋值,此时的字符串值声明在字符串常量池中
  • 字符串常量池中不会存储相同的字符串的
    1. String的String pool是一个固定大小的HashTable,默认大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了,直接影响就是调用String.intern()方法时性能会大幅下降
    2. -XX:StringTableSize可设置StringTable的大小
    3. JDK6固定1009,JDK7中StringTable默认的长度是60013,JDK8时默认是60013,1009是可设置的最小值

3.3、String的内存分配

Java语言中有8种基本数据类型和一种比较特殊的类型String,这些类型为了使他们再运行过程中速度更快,更节省内存,都提供了一种常量池的概念

3.3.1、String的常量池主要使用方法有两种
  • 直接使用双引号,声明出来的String对象会直接存储在常量池中
  • 如果不是双引号声明的String对象,可以使用String提供的intern()方法
3.3.2、String的常量池的存放位置
  • JDK6及之前,字符串常量池存在永久代
  • JDK7中,字符串常量池调整到Java堆中,调优时仅需调整堆大小就可以
  • JDK8中,字符串常量在堆中
  • 为什么要调整?永久代默认情况下比较小,永久代垃圾回收频率低,大量字符串容易导致OOM
3.3.3、String的基本操作

Java语言规范要求完全相同的字符串字面量,应该包含同样的Unicode字符序列,包含同一份码点序列的常量,并且必须指向同一个String类实例

3.3.3.1、字符串拼接操作
3.3.3.1.1、常量与常量的拼接结果在常量池,原理是编译期优化,字节码ldc # 2 ,是指加载常量池中索引为2的字面量abc压入操作数栈

在这里插入图片描述

3.3.3.1.2、常量池中不存在相同内容的常量

在这里插入图片描述
执行了String a = “mysql”;
在这里插入图片描述
此后每执行一行代码,String类的实例个数一次增加1,直到执行String e = “mysql”;之前
在这里插入图片描述
再次执行String e = “mysql”;代码发现String类的实例个数并没有增加,这就间接证明了常量池中不存在相同内容的常量

3.3.3.1.3、只要其中有一个变量,拼接结果就在堆中(常量池以外的堆),变量的拼接原理是StringBuilder,

在这里插入图片描述
在这里插入图片描述

3.3.3.1.4、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
3.3.3.1.5、字符串拼接操作不一定使用的是StringBuilder如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式
3.3.3.1.6、针对final修饰类,方法,基本数据类型,引用数据类型变量的结构时,能使用final尽量使用上,由于使用了final修饰变量,在编译器就对其进行了优化,使之变成了常量

在这里插入图片描述

3.3.3.1.7、对比用+号拼接字符串和StringBuilder.append操作对比
  • 拼接10万次,+号4000毫秒,append用了7毫秒,原因是+号每次循环创建一个StringBuilder,还要通过toString创建一个String对象
  • 内存中由于创建了较多的对象,内存占用更大,如果需要GC需要花费额外的时间
  • 改进空间:StringBuilder默认是16长度的char型数组,不够的时候会扩容,可以一次建一个比较大长度的数组
3.3.4、String类的intern()方法
3.3.4.1、面试题
  • new String(“ab”)会创建几个对象?答案:2个

    1. 一个对象是:new关键字在堆空间创建的
    2. 另一个对象是:字符串常量池中的对象"ab"。 字节码指令:ldc
      在这里插入图片描述
  • JDK6执行结果,false false
    在这里插入图片描述
    如上图所示。首先说一下 jdk6中的情况,在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。

  • JDK7/8,false true
    在这里插入图片描述

    1. 在第一段代码中,先看 s3和s4字符串。String s3 = new String(“1”) + new String(“1”);,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String(“1”)我们不去讨论它们。此时s3引用对象内容是”11”,但此时常量池中是没有 “11”对象的。
    2. 接下来s3.intern();这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个 “11” 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
    3. 最后String s4 = “11”; 这句代码中”11”是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。

在这里插入图片描述

  1. 再看 s 和 s2 对象。 String s = new String(“1”); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。
  2. 接下来String s2 = “1”; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。图中画的很清晰。
3.3.4.2、面试题变形

在这里插入图片描述

3.3.4.3、new String(“a”) + new String(“b”)?
  • 对象1,new一个String
  • 对象2,常量池a
  • 对象3,new String
  • 对象4,常量池b
  • 对象5,有拼接操作就new StringBuilder()
  • 对象6,StringBuilder,toString方法会new String返回,此时字符串常量池中没有ab
3.3.4.4、String.intern()方法总结
  • JDK6中,将这个字符串对象放入串池,如果串池中有,则并不会放入,返回已有串池中的对象的地址,如果没有,会把对象复制一份,放入串池,并返回串池中的对象地址
  • JDK7起,将这个字符串对象尝试放入串池,如果串池中有,则并不会放入,返回已有的串池中的对象的地址,如果没有,则会把对象的引用地址复制一份,放入串池,并返回对象的引用地址

在这里插入图片描述

3.3.4.5、intern()的效率测试
  • 大的网站平台,需要内存中存储大量的字符串,比如社交网站,很多人存储:北京市、海淀区等信息,这时候如果字符串调用intern()方法,则会明显降低内存的大小
  • FastJson将json字符串反序列化成Java对象的时候,将key进行了使用了intern()方法,理由是key是经常出现的,放入String Table中可以大大减少内存消耗
3.3.5、Stringtable的垃圾回收

-XX:+PrintStringTableStatistics

3.3.6、G1中String去重操作

背景:对许多Java应用,做的测试结果如下,堆存活数据集合里面String对象占了25%,堆存活数据集合里面重复的String对象有13.5%,String对象的平均长度是45,所以一说许多大规模的Java应用的瓶颈在于内存

  • Java堆中存活的数据集合差不多25%是String对象,这里差不多一半的String对象是重复的, 重复是指equals方法=true,堆上重复的String对象必然是一种内存的浪费
  • G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样避免浪费
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值