JVM 三. 运行时内存结构详解

一. JVM 基础

  1. 什么是JVM : 运行 java 程序的虚拟机,不同系统安装不同的 JVM, java程序可以实现平台无关性运行,就是通过在不同的 JVM 上实现的
  2. JVM生命周期: 创建实例—> 运行----> 销毁
  • 创建实例: 当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点
  • 运行: main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。 http://www.cnblogs.com/super-d2/p/3348183.html
  • 销毁: 当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用java.lang.Runtime类或者java.lang.System.exit()来退出。
  1. JVM 结构: 堆,虚拟机栈,本地方法栈,方法区,运行常量池,程序计数器,垃圾回收系统,类装载系统
    在这里插入图片描述
  • 堆: 所有线程共享的区域,存放了引用类型变量实体(不包含常量),这些对象实体指向栈空间的对象引用,在虚拟机启动时创建,是垃圾收集器管理的主要区域,也称为gc堆,在内存回收角度来看堆可分为新生代与老生代,如果堆中没有内存完成实例分配,并且堆无法再扩展时,会抛出outofMemoryError异常
  • 虚拟机栈: 是线程私有的,生命周期和线程相同,指的是方法执行的内存模型, 每个方法在执行都会创建一个栈帧(Stack Frame)用于存储基本类型变量,对象的引用,实例方法, 常说的栈内存指的就是虚拟机栈, 这个区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常
  • 本地方法栈: 与虚拟机栈区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务(可以理解为通过这个栈调用 native,操作其它语言)
  • 虚拟机栈与本地方法栈统称为栈,线程私有, 存放了基本数据类型变量,和引用类型变量的引用(堆中对象实体指向栈中的引用),栈管运行,堆管存储,栈与队列刚好相反,先进后出
  • 方法区: 与java堆一样(又叫静态域,存放了被所有线程共享的静态数据),线程共享的内存区域,用于存储被虚拟机加载的类常量、静态变量、即时编译器编译后的代码等数据(也就是通过类加载器加载一个类时,加载的大Class,类的结构信息)。它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常
  • 运行时常量池: 方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在加载后进入方法区的运行时常量池中存放
  • 直接内存: 不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分区域也被频繁使用,而且也可能导至OutOfMemoryError异常
  • 程序计数器: 一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器, Java虚拟机的多线程是通过并处理器分配线程轮流切换的方式来实现的,一个处理器都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存, 程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域
  • 类装载系统: 负责执行虚拟机的字节码,将java代码转换为虚拟机指令生成.class结尾的文件,当需要某个类时,虚拟机加载指定的.calss文件,并创建对应的class对象, 将class文件加载到虚拟机内存的过程称为类的加载
  • 垃圾收集系统: Java的核心,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理
  1. JVM中哪些是线程私有的哪些是共享的: 程序计数器, 虚拟机栈, 本地方法栈是线程私有的,方法区跟堆是共享的

程序计数器

  1. 解释程序计数器是做什么的: 首先程序计数器是线程私有的,记录线程的执行位置,例如多核cpu上资源调度,线程切换到另一个cpu上由前面程序计数器记录的位置开始继续执行, 官方解释:存储指向吓一跳指令的地址,也就是即将执行的代码,执行引擎的字节码解释器工作时通过这个计数器的值来选取下一个需要执行的字节码指令
  2. 为什么执行native方法时,是undefined? 任何时间一个线程都是只有一个当前方法在执行,程序计数器会存储当前线程正在执行的java方法的JVM指令地址,native方法大多是c实现的,并未编译成需要执行的字节码指令,所以计数器中是undefined
  3. 程序计数器为什么被设定为线程私有的: 一个程序中通过cpu不停的切换可能运行了多个线程,为了隔离多个线程,每个线程都分配一个寄存器,做到互不干扰

虚拟机栈

  1. 先了解几个问题:
  1. 堆和栈的区别,谁的性能更高: 栈的性能更高,栈里面存放了基本数据类型变量,引用,实例方法,对中存放了对象实例
  2. 为什么要把堆和栈区分出来: 栈的内存空间较小,防止引用类型变量过大造成栈溢出,或影响栈的运算性能,并且在创建对象时基本类型变量大小是预先知道的,而引用类型变量在字节码文件时是确定不下来的,所以将栈跟堆区分出来
  3. 栈中可以存储数据吗: 可以存储数据
  4. 如何理解栈管运行,堆管存储
  1. 对栈的解释: 是线程私有的,生命周期和线程相同,指的是方法执行的内存模型, 每个方法在执行都会创建一个栈帧(Stack Frame)用于存储基本类型变量,对象的引用,实例方法, 常说的栈内存指的就是虚拟机栈, 这个区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常
  2. 栈存在内存溢出吗: 栈存在溢出情况,但是不存在垃圾回收,
  3. 栈的大小决定了方法调用的最大可达深度,压栈数
  4. 什么时候会造成栈溢出: 1递归调用层次太多,压栈次数太多时,2局部数组过大时,3动态扩展栈大小无法申请到足够内存时(OutOfMemoryError)
  5. 栈的默认大小是java1.5前256k,1.5后是1024k,通常为1m,
  6. 如何修改栈内存大小:“-Xss size”
  7. 调整栈的大小后能避免内存溢出吗:不能,只能减少,栈是不可以无限扩大的
  8. 栈是越大越好吗: 如果设置过大造成一定的栈空间下能够创建线程数量变小
  9. 方法中定义的局部变量是不是线程安全的:不是,虽然每个执行方法都存在一个栈帧,栈帧不存在多个线程共享的问题,但是栈帧的局部变量中是可以存储对象引用的,而对象示例时存在堆中的,假设一个实例被多个栈帧的引用就会造成数据安全问题,还可以说一下i++的问题

栈中的栈帧

  1. 先了解一下方法和栈帧之间的关系:每个方法在执行都会创建一个栈帧(Stack Frame)用于存储基本类型变量,对象的引用,实例方法,例如下图中方法1调用方法2—>方法4,栈是先进后出,当方法4执行完毕,栈帧出栈
    在这里插入图片描述
  2. 可以这样理解,栈就是用来存放栈帧的,而栈帧就是执行的每个方法,如果一个执行的方法中调用了其它方法,就会创建对应这个被调用的方法的栈帧放在栈的顶端,被称为当前帧,
  3. 什么情况下会导致栈帧弹出: 方法执行结束,return指令,抛出异常, 入栈出栈遵循先进后出,后进先出原则
  4. 栈帧的结构: 栈帧是由: 局部变量表, 操作数栈, 动态链接, 方法返回地址, 一些附加信息 主要这五部分构成(又被称为帧数据区)
  5. 局部变量表详解:是一个数字数组,用来存储定义在方法内的局部变量,包括基本数据类型(8种),对象引用,returnAddress返回地址,这些变量只在当前方法调用中有效,当方法调用结束后,随着栈帧的销毁而销毁
    在这里插入图片描述
  6. 操作数栈详解: 主要用于存储计算过程中的中间结果,计算过程中临时的存储空间,我们说的方法执行时参数压栈就是指压入操作数栈,还有就是如果被调用的方法有返回值,这个返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令,32bit类型占用一个栈单位深度,64bit占用两个栈单位深度,在编译期间字节码文件时操作数栈的大小就已经确定下来了
  7. 根据操作数栈解释一下"i=i++",第一次执行将i赋值给i的基础值压入操作数栈中,第二次执行i的累加后的值压入操作数栈中,然后基于后进先出出栈,累加后的值先出栈赋值给i,但是栈中还存储着前面读取的i的基础值,再次出栈,将累加后的值给覆盖了
    在这里插入图片描述
  8. 动态链接: java源文件编译到字节码时,所有的变量和方法引用都是作为符号引用的,保存在class文件的常量池中,而动态链接主要就是用来将这些符号引用转换为调用方法的直接引用,目的就是为了支持当前方法的代码能够实现动态链接invokedynamic指令,例如一个方法中调用了另外一个方法,被调用的方法就是通过常量池中指向该方法的符号引用来表示的,动态链接就是将这个引用转换为直接引用
  9. 方法的返回地址: 存放了该方法的程序计数器的值,是不是可以理解为该方法被调用时程序计数器中存储的值,也就是方法被调用的位置???为了继续执行返回下一个栈帧

本地方法接口与本地方法栈

本地方法接口与本地方法栈:其作用是为了融合不同编程语言为java使用,初衷是为了融合c/c++的

  1. 先简单解释一下什么是堆: 所有线程共享的区域,存放了引用类型变量实体(不包含常量),这些对象实体指向栈空间的对象引用,在虚拟机启动时创建,是垃圾收集器管理的主要区域,在方法结束后,堆中的对象不会马上移除,也称为gc堆,在内存回收角度来看堆可分为新生代与老生代,如果堆中没有内存完成实例分配,并且堆无法再扩展时,会抛出outofMemoryError异常(下图JDK7与JDK8堆的结构)
    在这里插入图片描述
    在这里插入图片描述
  2. 有两个问题:
  1. 对象都分配在堆上吗: 从java虚拟机规范上来说是正确的,但是考虑栈性能更快,有栈上分配这个东西,所以这句话实际来说也不绝对
  2. 所有线程都共享堆吗: 正常情况下是这样的,但是又有快速分配策略TLAB(下面会解释)
  1. 堆的结构是什么样子的: 由于现在的垃圾回收都是基于分代来收集的,所以堆被细分为: 新生代,老生代,永久代有被称为方法区, 新生代又被细分为伊甸园区,s0区,s1区(s0,s1也被称为from区跟to区)
  2. 堆中为什么要分新生代,老年代, 持久代,新生代中为什么又要分Eden跟Survivor,并且各自有什么特点
  1. 为什么要分代: 防止内存溢出需要进行垃圾回收释放内存,对象的生命周期有长有短,为了减少不必要的回收创建,所以进行分代处理,
  1. Eden跟Survivor常见比例是多少: 8:1:1
  2. 伊甸园区,from区,to区的比例是否可调: 默认情况下新生代是整个堆内存的1/3, 新生代占1,老年代占2(通常情况下不会修改,如果存活对象比较多的话可以适当增加老年代)
  3. 创建对象时如何分配内存空间的,什么时候进入老年代
  4. 为什么要15次后进入老年代
  5. 对象在堆内存的生命周期,
  6. 新生代和老年代的内存回收策略
  7. 对象什么时候可以被回收
  8. JVM关于堆的几个主要参数
  9. 新生代分为Eden区s0区s1区,其中s0区s1区又被成功from区跟to区,那么这两个s区哪个是from,哪个是to: 基于复制进行垃圾回收时,复制交换后,空的是to区

堆内存设置

  1. 如何设置堆内存大小,设置时分为堆初始化内存大小,跟最大内存大小,可以设置为相等
  1. “-Xms 大小”: 设置堆的初始化大小
  2. “-Xmx 大小”: 设置堆的最大值,一旦堆中存储数据超过这个值时会抛出OutOfMemoryError内存溢出
  1. 初始化堆大小和最大堆大小设置为相同值的好处: 好处是在垃圾回收执行清理堆后不需要动态计算重新计算堆区的大小,提高性能
  2. 堆中默认最大值跟最小值分别是多少
  1. 堆的最大值计算: 如果物理内存少于192M,推荐设置为物理内存的一半,如果物理内存大于等于1G,值设置为物理内存的1/4,
  2. 堆的最小值计算: 最小值不少于8m,如果物理内存大于等于1G,推荐最小值设置为物理内存的1/64既16m,最小内存在JVM启动时就会被初始化
  1. JVM中最大堆大小有没有限制: 对于32位虚拟机来说,如果物理内存为4G,最大堆内存可达1G,对于64位虚拟机来说,如果物理内存为128G,理论上堆内存最大可达32G
  2. 实际开发中设置堆最大内存为2G?
  3. 堆中新生代老年代内存比例是多少: 默认情况下新生代是整个堆内存的1/3, 新生代占1,老年代占2(通常情况下不会修改,如果存活对象比较多的话可以适当增加老年代)
  4. 如何设置这个比例:
  1. “-XX:NewRatio=4” 表示设置新生代是整个堆内存的1/5,新生代占1,老年代占4
  2. “-Xmn 新生代最大内存大小”: 一般情况下使用默认值
  1. 新生代中又分为Eden区跟s0区,s1区,默认比例是8:1:1,通过"-XX:SurvivorRation=8" 设置
  2. 什么是空间分配担保策略: 在发生MinorGC前,虚拟机会检查老年代最大可用的连续内存空间是否大于新生代所有的对象总空间
  1. 如果大于,则此次MinorGc是安全的
  2. 如果小于,虚拟机会查看"-XX:HandlePromotionFailure"设置值是否为true,是否运行担保失败"
  3. 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续内存空间是否大于历次晋升到老年代的对象的平均大小,如果大于则尝试进行MinorGc,如果为该值为false,则将执行的MinorGc该为一次FullGc
  4. 重点:上面说的是JDK6以前的,JDK6.24后,规则变为: 只要老年代连续空间大于新生代对象总大小或大于历次晋升对象的平均大小就进行MinorGc,否则就执行FullGc

对象分配

  1. 先简单了解一下垃圾回收与不同分代区域的回收规则:
  1. 频繁的在新生代进行垃圾回收
  2. 很少在老年代进行老家回收
  3. 几乎不再永久区/元空间进行垃圾回收
  1. 总结对象分配规则:
  1. 优先分配到Eden区
  2. 大对象直接分配到老年代
  3. 长期存活对象分配到老年代
  4. 动态判断对象年龄,如果Survivor区中相同年龄的所有对象大小总和大于Survivo空间的一半,那么大于等于该年龄的对象直接进入老年代无序等等
  5. 空间分配担保,只要老年代连续空间大于新生代对象总大小或大于历次晋升对象的平均大小就进行MinorGc,否则就执行FullGc
  1. 对象分配详解: 实际要结合使用的垃圾回收算法
  1. new处的对象首先存放在Eden伊甸园区,需要注意当创建的对象过大伊甸园存放不下时,会直接将这个对象放入老年代
  2. 当伊甸园区空间存满时,JVM的垃圾收集器将对伊甸园区进行垃圾回收执行MinorGC,将伊甸园区中不在被引用的对象销毁,再加载新的对象到伊甸园区
  3. 然后将伊甸园区中的对象移动到Survivor0幸存区
  4. 如果再出发垃圾回收,此时Survivor幸存区上次保存的对象如果没有被回收会被整个复制到Survivor1幸存者区
  5. 后续没执行异常垃圾回收会将幸存对象在两个Survivor中来回复制,并在幸存的对象头MarkWor中进行记录,默认15次(最大也是15,因为对象头中用来存储这个标记的是4bit,二进制转换为10机制最大就是15,可以通过"-Xx:MaxTenuringThreshold=几次来设置")
  6. 当执行了15次垃圾回收后,该对象还未被回收掉,这个对象就会被放到老年代
  7. 当老年代内存不足时会触发MajorGC
  8. 如果触发MajorGC老年代垃圾回收后发现对象依然无法保存是就会产生OOM

堆与GC

  1. 什么是 MinorGC, MajorGC, FullGC
  1. MinorGC: 针对新生代的垃圾回收,
  2. MajorGC: 针对老年代的垃圾回收
  3. FullGC: 是整个java堆和方法区的GC
  1. MinorGC, MajorGC, FullGC都有什么区别
  2. 什么时候触发新生代的垃圾回收: 当新生代空间不足时会触发MinorGC,这里的新生代只指伊甸园区,Survivor如果满了是不会触发的,因为Survivor区中相同年龄的所有对象大小总和大于Survivo空间的一半时会进入老年代
  3. MinorGC新生代GC有什么特点: 对象具有朝生夕灭的特性,MinorGC非常频繁,回收速度快,MinorGC会引发STW挂起线程,暂停其它用户的线程,等垃圾回收结束后再恢复运行
  4. 什么时候会触发老年代的垃圾回收,自动触发阈值是多少:
  1. 目前只有GMS收集器存在单独触发执行收集老年代的行为,
  2. 还有混合收集,对新生代老年代都执行的垃圾回收,目前G1垃圾回收期采用的就是这种行为
  3. 整堆回收:例如当触发FullGC时有MinorGC与MajorGC还有方法区的GC
  1. MajorGC老年代GC有什么特点: 通常发生MajorGC时会伴随着MinorGC不是绝对的,要考虑使用的哪种垃圾收集器,MajorGC速度比MinorGC慢10倍以上,STW时间更长,如果MajorGC执行后内存还是不足会报OOM
  2. 什么时候触发FullGC: 有5种情况会触发
  1. 老年代空间不足时
  2. 方法空间不足时
  3. 空间分配担保策略: 当发生MinorGC前,虚拟机检查老年代最大可用的连续内存空间是否大于新生代所有的对象总空间,或检查是否大于历次晋升对象平均值时,如果大于则执行MinorGC,如果不大于则执行FullGC
  4. 在执行垃圾回收,Eden区判断对象可达后,Survivor0(From)区向Survivor1(To)区复制时,如果FromS0区对象大小大于ToS1可用内存,则把这些对象转存到老年代,但是老年代可用内存又小于当前这些对象时,也会触发FullGC
  5. 另外还有调用System.gc()方法或Runtime.getRuntime().gc()时,但不是必然执行的
  1. JVM一次完整的GC流程,或一次FullGC流程: 也就是上面说的集中GC触发条件,新生代不足时会触发MinorGC, 老年代不足时会触发FullGC,

OOM堆内存溢出

  1. 当老年代执行了MajorGC后,发现对象过大依然保存不下时,会出现OutOfMemoryError 内存溢出,OOM如何解决
    在这里插入图片描述

方法区

  1. 堆, 栈, 方法区三者之间的关系: 当我们new创建一个对象时,new出来的对象实例存储在堆中,而这个对象的引用在栈中(栈的局部变量表中),指向堆中的这个对象实例, 而对应堆中这个对象实例的数据类型指针指向的就是方法区该对象类型数据,方法区可以理解为存放对象的Class类,堆中的对象实例通过一个指针指向方法区中这个对象实例的类,例如下图中创建Persion对象
    在这里插入图片描述
  2. 在逻辑上方法区看成堆的一部分,而实际上又被称为非堆,用来跟堆区分开,方法区是线程共享的
  3. 不同版本JDK方法区是不同的: 7及以前被称为永久代,JDK8开始使用元空间取代了永久代
  4. 方法区的默认大小与怎么设置方法区大小: 根据JDK版本不同不一样
  1. JDK7及以前永久代设置: “-XX:PermSize=初始化大小,默认20.75M”, “-XX:MaxPermSize=最大值,32位系统默认64,64位默认82”,
  2. JDK8元空间设置:“-XX:MetaspaceSize=初始化大小,默认21”, “-XX:MaxMetaspaceSize=最大值,默认-1表示没有限制跟随本地内存
  1. 方法区会不会内存溢出: 会,方法区的大小决定了系统中可以存放多少个类,例如加载jar包过多,大量动态生成反射类,例如Tomcat部署工程过多是(30-50个)
  1. JDK7及以前时方法区内存溢出会报"java.lang.OutOfMemoryError: PermGen space"
  2. JDK8方法区内存溢出会报"java.lang.OutOfMemoryError: Metaspace"
  1. 元空间跟永久代有什么区别,用元空间有什么好处: 永久代大小是固定的,但实际生产中这个大小是很难确定的,在某些场景下如果动态加载的类过多容易产生Perm区的内存溢出,而元空间不在虚拟机中而是使用本地内存,默认情况下元空间的大小只受限与本地内存的大小,并且永久代时如果初始化大小设置过低,FullGC时也会多次回收,为了避免频繁的垃圾回收需要设置一个相对较高的值"-XX:MetaspaceSize"
  2. 方法区中存在垃圾回收吗,回收的什么:存在FullGC时会对方法区中的垃圾回收,回收的是常量池中废弃的常量和不在使用的类型
  3. 方法区中存放了那些常量: 字面量和符号引用,字面量就是java语言层面的入文本字符串,被声明为final的常量值等,符号引用则属于编译原理方面的概念,包括: 类和接口的全限定名, 字段的名称和描述符, 方法的名称和描述符
  4. 方法区中的常量什么时候被回收: HotSpot虚拟机对常量池的回收策略是只要常量池中的常量没有被其它地方引用,就被回收

直接内存

  1. 概述
  2. 非直接缓冲区vs直接缓冲区
  3. 大小设置方式

StringTable

  1. 解释: String table又称为String pool,字符串常量池,其存在于堆中(jdk1.7之后改的)。最重要的一点,String table中存储的并不是String类型的对象,存储的是指向String对象的索引,真实对象还是存储在堆中, 可以参考这个博客
  2. String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern()时性能会大幅下降。
  1. 使用-XX:StringTablesize可设置stringTable的长度
  2. 在jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。stringTablesize设置没有要求。
  3. 在jdk7中,stringTable的长度默认值是60013。
  4. 在JDK8中,StringTable可以设置的最小值为1009。
  1. String 的不可变性
  1. 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
  2. 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  3. 当调用string的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中
  1. String 的内存分配: 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念, 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
  1. 直接使用双引号声明出来的String对象会直接存储在常量池中,比如:string info=“atguigu.com”;
  2. 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
  3. Java 6及以前,字符串常量池存放在永久代。
  4. Java 7中 对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用string.intern()
  1. String 的基本操作
  2. 字符串拼接操作
  1. 常量与常量的拼接结果在常量池,原理是编译期优化。
  2. 常量池中不会存在相同内容的变量。
  3. 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  4. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
  1. new String()的问题,判断下方代码输出true或false
  1. 第一个为true的原因是jvm存在编译期优化的机制,在编译期(javac *.java时)会将可以拼接的字符串常量帮你自动拼接了,此时由于String table中已经存在了,因此会让str2指向一个与str1相同的那块地址,因此为true。
  2. 第二个为false的原因在于,由于使用了new String(),运行过程中会在堆中重新开辟一个空间存储,与之前的常量字符串没啥关系,因此为false。
  3. 第三个为true原因在于,调用intern()方法时会把在String table中查找是否存在与其值相等的(并不是地址相等)的字符串,发现里面恰好存在,因此返回该存在的引用,String table中的就是str1,因此为true
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "ab" + "c";
        String str3 = new String("ab") + "c";
        String str4 = str3.intern();
        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
        System.out.println(str1 == str4);
    }
  1. intern()方法: 是String的一个native方法,调用的是底层C的方法,当调用intern()方法时,会先在String table中查找是否存在于该对象相同的字符串,若存在直接返回String table中字符串的引用,若不存在则在String table中创建一个与该对象相同的字符串, 也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同,所以(“a”+“b”+“c”).intern()==“abc” 为true
  2. G1的String去重操作: 注意这里说的重复,指的是在堆中的数据,而不是常量池中的,因为常量池中的本身就不会重复
  1. 解释G1对String去重的原因: 经过统计获取到在常见项目中,Java堆中存活的数据集合差不多25%是String对象。更进一步,里面差不多一半String对象是重复的,意思是说:string1.equals(string2)= true。堆上存在重复的string对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的string对象进行去重,这样就能避免浪费内存
  2. 是怎么实现的: 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的string对象,如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的string对象。使用一个hashtable来记录所有的被string对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。如果存在,string对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。
  3. 开启: UsestringDeduplication(bool):开启String去重,默认是不开启的,需要手动开启。PrintstringDeduplicationStatistics(bool):打印详细的去重统计信息。StringDeduplicationAgeThreshold(uintx):达到这个年龄的String对象被认为是去重的候选对象
  1. new String(“ab”)会创建几个对象: 两个, 一个是:new关键字在堆空间中创建。一个是:字符串常量池中的对象
  2. new String(“a”) + new String(“b”) 会创建几个对象: 6个
对象1new StringBuilder()
对象2new String("a")
对象3:常量池的 a
对象4new String("b")
对象5:常量池的 b
对象6:toString中会创建一个 new String("ab")
强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值