jvm-内存
前言
首先了解一下什么是虚拟机(Java Virtual Machine),java虚拟机是一台执行java字节码的虚拟计算机,拥有独立的运行机制,java虚拟机就是二进制字节码的运行环境,负责装在字节码到其内部,解释/编译为对应平台上的机器指令执行。
特点:
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收
目前一般所说的jvm是HotSpot Vm,毕竟是官方所出,它采用的是解释器与及时编译器并存的架构。不同的厂商可能内存结构不尽相同,取决于实现。
内存结构与内存模型非一个意思:
内存模型:Java Memory Model(JMM),他定义了一套在多进程读写共享数据时,对数据可见性,有序性和原子性的规则和保障。
内存结构:就是下文中讨论的一些结构。
java的执行程序就是:java程序 通过 java编译器 编译为 字节码文件,然后通过类加载器加载,校验并通过翻译字节码(解析执行),JIT编译器(编译执行)进行执行。
JVM的生命周期是跟随程序开始以及结束的。执行一个java程序,其实是执行一个java虚拟机的进行。
类加载子系统
类加载子系统(Class Loader SubSystem)负责从文件系统或者网络中加载class文件,class文件开头有特定的文件标志,cafe babe
ClassLoader只负责class文件的加载,加载完成之后,会交给执行引擎(Execution Engine)执行
加载之后的类的信息存放在一个成为方法区(Method Area)的内存空间。不仅包含类的信息,方法区中还后悔存在运行时常量池信息,可能也会包含字符串字面量与数字常量
加载
过程:
- 通过类的全限定名获取定义此类的二进制流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表该类的java.lang.Class对象,作为方法区该类的访问入口
类加载器分为:
- 引导类加载器(Bootstrap ClassLoader):C/C++实现,嵌套在jvm内部,加载java的核心库。扩展类加载器以及应用程序加载器就是它创建的,无父类加载器,不继承在java.lang.ClassLoader
- 扩展类加载器(Extension ClassLoader):派生于ClassLoader,父类为启动类加载器
- 应用程序类加载器(AppClassLoader):派生于ClassLoader,父类加载器为扩展类加载器,该类加载器是程序中默认的类加载器
- 用户自定义加载器:如果必要可以自定义类加载器
链接
过程:
- 验证:确保Class文件的字节流中包含西悉尼符合虚拟机要求,保证被加载类的正确性,主要包含四种验证:文件验证,元数据验证,字节码验证,符号引用验证
- 准备:为类变量分配内存并设置默认初始值(final修饰的static变量在编译的时候就会分配)。这里不会对类的实例变量初始化,类变量在方法区,而实例变量会随着对象分配到java堆中
- 解析:将常量池内的符号转化为直接引用的过程(符号引用是一组符号描述所引用的目标,直接引用就是直接指向目标的指针,相对偏移量或间接定位到目标的句柄),解析动作主要针对类或接口,属性,类方法,接口方法,方法类型等。
初始化
过程:
- 执行类构造器方法<cinit>()的过程,此方法不需要定义,是javac编译器自动收集类中的类变量的复制动作和静态代码块中的语句合并而来,与<init>()不一样。
- 如果该类具有父类,jvm会保证在执行子类的cinit方法之前,先执行父类的
- 虚拟机必须要保证一个类的cinit方法再多线程下同步加锁
双亲委派机制:
Java虚拟机对class文件采取按需加载,但加载文件时,java虚拟机采用的是双亲委派模式,把请求交给父类处理,具体过程:
- 类加载器收到类加载请求,将请求交给父类加载器去执行
- 如果父类加载器还存在父类加载器,则进一步委托,直到到达引导类加载器
- 如果父类可以完成类加载,则成功返回,否则由子类自己尝试加载
优势:
- 这样可以避免类的重复加载,可以保证该类只有一种类型的加载器加载(jvm判断两个类是否相同,一个是类的完整类名必须一致,一个是类的加载器实例对象必须一致)
- 可以保护程序安全,防止核心API被篡改,比如我们自定义一个string类,那么如果不是双亲委派机制,则这个类就会被加载为String,导致核心api被污染
运行时数据区
内存是重要的系统资源,是硬盘与cpu的桥梁。jvm内存结构规定java在运行时内存申请、分配、管理的策略,保证jvm的高效稳定运行,不同jvm对于内存划分和管理机制存在部分差异
java虚拟机定义若干种程序运行期间会使用到的运行时数据区,其中一些生命周期与虚拟机一致,另外一些则与线程保持一致
- 与线程生命周期一致:本地方法栈(Native Method Stack)、程序计数器(PC Registers)、虚拟机栈(Stack Area)
- 与虚拟机生命周期一致:堆区(Heap Area)、方法区(Method Area)
每个jvm只有一个Runtime的实例(Runtime采用的是饿汉式单例模式),即运行时环境。
在Hotspot jvm里,每个线程都与操作系统的本地线程直接映射。当java的线程准备好时,操作系统的本地线程也会对象创建,java线程终止时,本地线程回收。jvm出去我们的程序线程,还包含一些后台线程:
- 虚拟机线程:线程操作需要JVM到达安全点才会出现。这些操作必须在不同线程的原因是因为他们都需要jvm达到安全点,堆才不会发生变化。执行类型包含STW(stop-the-world)的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
- 周期性任务线程:用于周期性操作的调度执行,比如中断
- GC线程:对jvm里不同种类的垃圾收集行为提供支持
- 编译线程:运行时将字节码编译成本地代码
- 信号调度线程:接收信号并发送给jvm,在其内部通过调用适当方法进行处理
程序计数器(PC Register)
register是寄存器的意思,但一般直接翻译为程序计数器或者指令计数器,在jvm这个是对物理pc积存的的一种抽象模拟,作用就是记住下一条指令地址。
特征:
- 一块很小的空间,也是运行速度最快的存储区域
- 线程私有,生命周期与线程保持一致
- 任何时间一个县城只有一个方法在执行,又称当前方法。程序计数器会存储当前线程正在执行的java方法的jvm指令地址,如果是native方法,则是undefined。
- 程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖计数器
- 字节码解释器工作时就是通过程序计数器选取吓一跳需要执行的字节码指令
- 唯一一个没有规定OOM(OutOfMemoryError)
虚拟机栈(Stack Area)
由于跨平台特性,java指令都是根据栈来设计,因为寄存器并非每个平台都有的。优点就是跨平台,指令集少,编译器容易实现,缺点就是性能下降,对比基于寄存器实现同样功能会需要更多地指令。
栈与堆的通俗解释:
- 栈:是解决程序运行问题,程序应当如何运行以或者处理数据
- 堆:是解决数据存储问题,数据如何放以及放在那里。形象比喻就是仓库
特点:
- 每个线程创建时,都会创建一个虚拟机栈,其内部保存一个个栈针(Stack Frame),对应每一次方法调用
- 生命周期与线程保持一致
- 主管方法的运行,保存方法的局部变量,部分结果,并参与方法的调用和返回
- 一种快速有效的分配存储方式,访问速度仅次于程序计数器
- 没有GC
- 当方法的个数超出栈的深度会报异常,因为虚拟机规范允许栈的大小可以是动态的或者固定不变的,如果采用固定大小,如果请求分配的栈容量超过虚拟机栈允许最大容量会抛出StackOverflowError,如果可以动态扩展,并且尝试申请没有足够内存去创建虚拟机栈,会抛出OutOfMemoryError异常。
原理:
- 栈的操作只有两个,对栈帧的压栈以及出栈,遵循先进后出原则
- 同一个时间点只有一个活动栈帧,即当前栈帧(Current Frame),对象的方法就是当前方法(Current Method),定义这个类就是当前类(Current Class)
- 执行引擎运行的所有字节码指令都只针对当前栈帧
- 如果调用其他方法,对应新的栈帧会被创建,放在栈的顶端,成为当前栈帧
- 不同线程的栈帧是不能相互引用的,因为是线程私有
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会将结果传给前一个栈帧,虚拟机会丢弃当前栈帧,是前一个栈帧成为当前栈帧
- 两种返回函数方式,一种是正常返回,使用return指令(对于void方法,最后一张指令也会是return),另一种就是抛出异常,两种方式都会导致栈帧被弹出。
栈帧的内部结构
局部变量表(Local Variables):
- 局部变量数组或者本地变量表。定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括:基本数据类型,对象引用(reference),方法返回地址
- 局部变量表是线程私有的,不存在数据安全问题
- 局部变量表所需容量大小在编译期就会确定,并保存在方法的code属性的locals(Maximum local variables)数据项中,运行期间是不会改变其大小的
- 如果方法的参数和局部变量增多会使局部变量膨胀,那么对应栈帧就会越大,相对应当前栈所能入站的栈帧数量就会减少
- 局部变量只在当前方法调用中有效,方法执行时,虚拟机通过局部变量表完成参数值到参数列表的传递过程。当方法调用结束之后,方法栈桢销毁,局部变量表也会随之销毁
- 局部变量表的基本存储单元就是Slot(变量槽),并且参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
- 局部变量表中,32位以内的类型只占用一个slot,64位的类型(long与double)占用两个slot,如下述变量表中j就占用2-3的槽
- 当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被复制局部变量表中的每一个slot中
- 如果当前栈帧是由构造方法或者实例方法创建,那么该对象引用的this会放在index为0的slot处
栈帧中的局部变量表中的槽位是可以服用的。当一个局部变量过了其作用域,那么在之后声名的新的便有就有可能会覆盖过期的局部变量,从而达到节省资源目的
对于下述代码
public void test2(){ { int a = 0; System.out.println(a); } int b = 1; }
对应的局部变量表,可以看出,新声明的变量覆盖了过期变量a:
- 之前类加载的过程中有声明,类变量表有两次初始化计划,一个是准备阶段,赋予零值,一个是初始化阶段,进行赋值,但是局部变量表没有准备阶段的赋值过程,如果不赋值则无法使用
- 栈帧中与性能关系最密切的就是局部变量,同时,局部变量表也是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用对象都不会被回收
操作数栈(Operand Area):
- 又称表达式栈,在方法执行过程中,根据字节码指令,在栈中写入数据或提取数据,入栈、出栈
- 用于保存计算过程中的中间结果,以及为计算过程中变量的临时储存空间
- 方法开始执行时,一个新的栈帧随之被创建,这个方法的操作数栈是空的
- 每一个操作数栈都拥有一个明确栈深度,所需最大深度在编译其就会确定,保存在code属性中stack(max_stack)
- 栈中任何一个元素都可以是java的任意数据类型,32位占用一个栈的深度,64位占用两个栈的深度
- 如果被调用的方法带有返回值,其返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需执行的字节码指令
- java虚拟机的解释引擎是基于栈的执行引擎,其中的栈就是操作数栈
动态链接(Dynamic Link):
- 每一个栈帧内部都包含一个指向运行时常量池中该站真所属方法的引用,目的就是为了支持当前方法的代码能够实现动态链接
- java源文件编译到字节码文件,所有的变量以及方法引用都是符号引用,保存在class文件的常量池中。如果描述一个方法调用了另外一个方法就是通过常量池中符号引用来进行表示,二动态链接就是讲这些符号引用变为调用方法的直接引用
说明:
- 静态链接:当一个字节码文件被装载到jvm内部时,如果被调用的目标方法在编译期克制,并且运行期保持不变,则这种情况下可以直接将调用方法转为直接引用。
- 动态链接:被调用的方法无法在编译期确定下来,需要动态解析出需要调用的方法,然后执行
public LocalVariables test(){ int i = 1; test3(i); Runnable runnable = () -> {}; return localVariables; }
对应的字节码
方法返回地址(Return Address):
- 存放调用该方法的程序计数器的值
- 一个方法的结束有两种方式:方法正常退出或者出现未处理异常,异常退出
- 方法退出之后,都会返回该方法被调用的地方,当正常退出时,调用这个的程序计数器的值最为返回地址,即调用方法的指令下一条指令地址,异常退出则需要查询异常表来确定
- 返回指令包含return(void),ireturn(boolean,byte,char,short,int),lreturn(long),freturn(float),dreturn(double),areturn(any)
- 方法的退出就是当前栈帧出站,此时需要恢复上册方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作舒展,设置程序计数器,让调用者方法据需执行,但异常退出不会给上层调用者返回值
一些附加信息:
与java虚拟机实现相关的附加信息,不重要
本地方法栈
本地方法是一个Native Method,就是java代码调用非java方法的接口,定义一个native method,并不提供实现体,由非java语言在外部实现
- 本地方法栈就是用于管理本地方法的调用
- 线程私有
- 允许实现为固定或者可动态扩展内存大小(内存溢出与虚拟机栈一样)
- 本地方法是使用c实现的,具体做法就是在方法上标记native方法,执行引擎执行时就是加载本地方法库
- 当某个线程调用本地方法是,就会进入一个全新的且不受虚拟机限制的世界,拥有与虚拟机同样的权限
堆
核心概念:
- 一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域
- 堆区在jvm启动实施后机会被创建,是jvm管理的最大一块空间,堆可以处在不连续的内存空间,但逻辑上它应当被视为连续的。
- 几乎所有的对象实例以及数组都在堆上分配内存
- 数组和对象永远不会存储在栈上,只是保存引用,引用指向对象或者数组在堆中的位置
- 方法结束之后,堆中内存对象不会被马上被删除,仅在垃圾收集时才会被移除
- 堆就是GC的重点回收区域
- java8之后的堆空间分为:年轻代(伊甸园,幸存区)和老年代
- 一旦堆中的内存大小超过指定的最大内存,将会抛出OutOfMemoryError异常
- 关于堆内存可以使用下述两个参数进行设置:
- “-Xms”用于表示堆区的起始内存,等价于-XX:InitialHeapSize,默认值=物理电脑内存大小/64
- “-Xmx”用于表示堆区的最大内存,等价于-XX:MaxHeapSize,默认值=物理电脑内存大小/4
- 通常会将-Xms与-Xmx两个参数配置相同的值,目的就是为了能够在java垃圾回收机制清理完堆区之后不需要重新分隔计算队取得大小,从而提高性能
public class OOMTest { public static void main(String[] args) { List<File> list = new ArrayList<>(); while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } list.add(new File(new Random().nextInt(1024 * 1024))); } } } class File { private byte[] file; public File(int length) { this.file = new byte[length]; } }
使用 -Xms100m -Xmx100m,会发现异常信息:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.yang.heap.OOMTest.main(OOMTest.java:37)
年轻代与老年代:
- 年轻代是指生命周期较短的瞬间对象的存储区域,可以划分为伊甸园区(Eden),幸存者区(Survivor0,Survivor1 有时也称作from区,to区)
- 老年代指的是生命周期非常长的对象的存储区域
- 默认的年轻代老年代的比例为1:2,可以通过jvm参数 -XX:NewRatio=2 来进行调节
- 年轻代中伊甸园区与两个幸存者区的比例为8:1:1,可以通过参数 -XX:SurvivorRatio=8进行设置
- 几乎所有的java对象都是在伊甸园区被new出来
- 绝大部分java的对象销毁都是在年轻代进行
- 新建一个对象先放入伊甸园区,如果伊甸园区内存不足,则触发Minor GC(Minor GC过程见下文),再尝试放入伊甸园区,如果伊甸园区空间不足,则会将对象放入老年代,如果老年代空间不足,触发Major GC,如果老年代的空间还不足放下,则会抛出OOM异常。
GC过程
jvm在进行GC时,并非每次都对所有区域一起回首,大部分时候的回收都是指年轻代
按照回收区域可以分为部分收集以及整堆收集:
- 部分收集(Partial GC):
- 年轻代收集(Minor GC/Young GC):只是对年轻代(Eden\S0,S1)垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代收集,目前之后CMS垃圾收集器有单独收集老年代行为,大部分时候Major GC会与Full GC混淆使用
- 混合收集(Mixed GC):收集整个新生代以及部分老年代垃圾,目前只有G1 GC有这种行为,因为他是按照region来进行垃圾回收的
- 整堆收集(Full GC):收集整个java堆和方法区
年轻代GC(Minor GC/Young GC):
- 当年轻代空间(伊甸园区(Eden区))空间不足时,就会触发Minor GC,过程就是会收集伊甸园区垃圾,而幸存对象对象,会放入幸存0区(from区),并且将其年龄加1,同时会扫描幸存者1区(to 区),如果满足晋升条件,则放入老年代,否则进入幸存者0区(from区),同时年龄加1,然后幸存者0区域(from)和幸存者1区(to 区)逻辑互换
- 年轻代向老年代晋升的条件默认是年龄到达15次之后,可以通过 -XX:MaxTenuringThreshold=15进行设置,对于超大对象,则会进入老年区,如果伊甸园区或者幸存者0区向幸存者1区复制对象时,如果空间不足,就出直接放入老年代。还有如果幸存0区中相同年龄对象的大小总和大于其空间的一半,那么年龄大于或等于该年龄的对象直接进入老年代
- java对象大多具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快
- Minor GC会引发STW,暂停其他用户线程,等垃圾回收结束后,用户用户线程恢复运行
- 在进行Minor GC之前,虚拟机会检查老年代的最大可用的连续空间是否大于新生代所有对象的总空间或者是否大于历次晋升到老年代的对象的平均大小,是则会进行Minor GC,否则进行Full GC。
老年代GC(Major GC/Old GC):
- 出现Major GC一般至少会进行一次Minor GC,但并非绝对,取决于垃圾收集器。当老年代空间不足时,会尝试先触发Minor GC,如果之后空间仍不足,则会触发Major GC
- Major GC速度一般比Major GC速度慢很多,因为STW时间更长
- 如果Major GC之后,空间仍然不足,就会报OOM异常。
Full GC:
- 调用System.gc(),系统建议执行Full GC,但是不必然执行,一般生产环境都会关闭这个功能。java中解析excel的第三方库,有一个方法就会调用这个方法
- 老年代或者方法区空间不足时会触发Full GC
- Full GC时开发或者调优需要尽量减少,这样会减少暂停时长
TLAB(Thread Local Allocation Buffer):
- 堆区是线程共享的,对象创建在JVM非常频繁,为防止多线程划分内存,这时候需要加锁来保证不会分配到同一内存,但这样势必会影响分配速度
- jvm为每一个线程分配了一个私有缓存区域,包含在伊甸园区(Eden),默认情况仅占伊甸园区的1%,非常小
- 多线程同时分配内存时,会优先在TLAB上分配内存,如果分配失败,就会尝试通过加锁机制,在伊甸园区分配内存
- 可以通过 -XX:TLABWasteTargetPercent=1 设置TLAB空间所占用伊甸园区的百分比
逃逸分析:
是一种可以有效减少java程序见同步负载和内存堆分配压力的跨函数全局数据流分析算法。经过逃逸分析,编译器能够分析出一个新的对象引用的使用范围是否将这个对象分配到腿上,纠纷分析这个对象的动态作用域。对于没有发生逃逸的对象,可以直接分配到栈上,随着栈空间的一扩而消失,这样可以减少GC的次数,从而提升性能。但是目前技术没有非常成熟,因为无法保证逃逸分析的性能消耗会高于之前不做性能分析的小号。因为本身逃逸分析也是一个相对耗时的操作,但思想总是好的。
使用逃逸分析,编译器主要做下述优化:
- 栈上分配:如果对象没有发生逃逸,将对象由对分配转为栈分配
- 同步省略(锁消除):如果一个对象只会被一个线程访问,对这个对象的操作可以不加锁
- 分离对象或者标量替换:有的对象可能不需要作为一个连续的内存结构存在也可被访问,那么对象的部分或全部可以不放在内存中,放在cpu寄存器中
方法区(Method Area)
尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择进行垃圾收集或者压缩。方法区还有一个别名Non-Heap非堆,方法区一颗看作是一块独立于java堆的内存空间。
- 方法区是是各线程共享区域
- jvm启动时创建,实际的物理空间可以是不连续的,jvm关闭则会释放这个空间
- 方法区的大小界定系统可以存放类的个数,如果方法区溢出,则也会抛出OOM错误
- 大小可以选择固定大小或者可以扩展,使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置。在window下方法区默认大小时21M,最大就是没有限制,会耗尽所有可用系统内存。方法区到达21M之后就会触发Full GC,卸载无用的类,如果还不够则会在不超过最大方法区的限制之前适当提高该值。因此可以将MetaspaceSize设置为一个相对较高的值
- 方法区又称元空间,不在虚拟机设置的内存中,使用的是本地内存
- 方法区主要存储已经被虚拟机加载的类型信息、常量、静态常量、即时编译器编译后的代码缓存、域信息、方法信息等
- 类型信息主要包含该类型的完整有效名称,直接父类的完整有效名称,类的修饰符,直接接口的一个有序列表
- 域信息包含域名称,域类型、域修饰符
- 方法信息:方法名称,方法返回类型,方法参数的数量和类型,方法的修饰符,方法字节码,操作数栈,局部变量表大小,异常表
- 方法区内部包含了运行时常量池,与字节码文件中的常量池不用
- 常量池存储的主要是数据量,字符串值,类引用,字段引用,方法引用,可以看作是一张表,虚拟机指令根据这个常量表查询待执行的类名,方法名,参数类型,字面量
- 运行时常量池在加载了和接口到虚拟机后,就会创建对应的运行时常量池,池中的数据是通过索引访问。常量不在是常量池中那个的符号地址,换成真实地址,具备动态性
- 垃圾收集主要回收运行时常量池中废弃常量和不再使用类型
执行引擎(Execution Engine)
将字节码指令解释/编译为对应平台上的机器指令。就是高级语言转化为机器语言
- 解释器(Interpreter):jvm启动时根据预定义的规范对字节码逐行解释的方式执行。将每条字节码文件的内容转化为对应平台的本地机器码
- JIT(Just In Time Compiler)编译器:jvm直接将源代码转为为本地机器平台相关的机器语言
String Table
- 字符串常量池不会存储相同内容的字符串
- 常量与常量的拼接结果在常量池
- 只要凭借过程中,只要有一个是变量,结果就在堆中,但是如果该变量是被final修饰的话,因为final修饰这个值在加载的时候接确定,属于常量引用,这种情况下不会使用StringBuilder的方法进行创建,会在编译期优化,因此如果可以声明final,尽量去声明
- 如果拼接的结果调用intern()方法,则注定将常量池中还没有的字符串对象放入池中,并返回此对象地址,如果池中已有该对象,则不会放入
public static void test1(){ String s1 = "1"; String s2 = "2"; String s3 = "12"; String s4 = s1 + "2"; String s5 = "1" + s2; String s6 = s1 + s2; String s7 = "1" + "2"; String s8 = new StringBuilder().append(s1).append(s2).toString(); String s9 = new String("12"); String s10 = "12"; System.out.println(s3 == s4); // false System.out.println(s3 == s5); // false System.out.println(s3 == s6); // false System.out.println(s3 == s7); // true System.out.println(s3 == s8); // false System.out.println(s3 == s9); // false System.out.println(s3 == s10); // true System.out.println(s4 == s6); // false System.out.println(s5 == s6); // false } public static void test2(){ final String s1 = "1"; final String s2 = "2"; String s3 = "12"; String s4 = s1 + "2"; String s5 = "1" + s2; String s6 = s1 + s2; String s7 = "1" + "2"; System.out.println(s3 == s4); // true System.out.println(s3 == s5); // true System.out.println(s3 == s6); // true System.out.println(s3 == s7); // true System.out.println(s4 == s6); // true System.out.println(s5 == s6); // true }
String intern的使用
如果不是常量或者常量引用的对象,可以使用String 的intern方法
在查证intern之前,先确认一下new String("12")会执行的操作,这个操作会创建两个对象,一个是new关键字在堆空间创建爱你的,另外一个就是在字符串常量池中创建的字符串常量,可以看出ldc就是在字符串常量池中加载对象
public static void main(String[] args) { String s = new String("12"); s.intern(); System.out.println(s == "12"); // false }
intern方法会查询字符串常量池中产寻当前字符串是否存在,如果不存在,会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址,存在就会返回已有对象的地址
public static void main(String[] args) { String s1 = "1"; String s2 = "2"; String s3 = s1 + s2; String s4 = s3.intern(); String s5 = "12"; String s6 = new String("12"); String s7 = s6.intern(); System.out.println(s3 == s4); // true,调用intern之后会将12的引用地址存入常量池中,并返回,因此s3余s4形同 System.out.println(s3 == s5); // true,字符串常量池中已经有12,因此获取的就是引用地址 System.out.println(s4 == s5); // true System.out.println(s3 == s6); // false new的一个对象,这个不会相等,地址执行的是堆new对象的地址 System.out.println(s3 == s7); // true 尝试将12放入字符串常量池中,但是已存在,返回引用地址 System.out.println(s6 == s7); // false 不是一个对象 }