JVM

  • JVM的位置

JVM的位置

  • JVM的整体结构

JVM的整体结构

类加载子系统

类的加载过程

  1. 加载
    通过类的全限定名获取定义该类的二进制字节流,并将该字节流所代表的的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个Class对象,来作为方法区这个类的各种数据的访问入口
  2. 链接
    • 验证
      确保Class文件字节流中是符合虚拟机要求的,不会危害JVM自身的安全
    • 准备
      类变量分配内存,并设置类变量的默认初始值
      static int a = 1; // prepare:a=0 ---> initial:a=1

      final修饰的static变量除外,因为他在编译时候就分配内存了,在准备阶段会显示初始化

    • 解析
      将常量池内的符号引用转化为直接引用,大多是在执行完初始化之后再执行

      一个普通Java类加载到内存过程中还会加载很多的类,这些类就是通过符号引用来定位的

  3. 初始化
    执行类构造器方法(),这个方法是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的

    private static int num = 1;
    static{
        num = 3;
    }
    
    0 iconst_1
    1 putstatic #3 <com/atguigu/java/ClassInitTest.num>
    4 iconst_3
    5 putstatic #3 <com/atguigu/java/ClassInitTest.num>
    8 return
    复制代码

    如果该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。虚拟机保证一个类的()方法在多线程下被同步加锁(单例模式-内部类方法,能安全执行的原因)

    • 对类主动使用的七种情况,主动使用都会导致类的初始化
      1. 创建类的实例
      2. 访问某个类或接口的静态变量,或者对静态变量赋值
      3. 调用类的静态方法
      4. 反射调用
      5. 初始化一个类的子类
      6. Java虚拟机启动时被标明为启动类的类
      7. JDK7后的动态语言

类加载器

类加载器是包含关系,不是继承关系,更像是等级关系。不同的类加载器加载不同的类,各司其职

  • BootStrapClassLoader启动类加载器
    1. 使用C/C++编写,嵌套在JVM内部(不继承ClassLoader,谈不上继承,是C++语言实现的)
    2. 用来加载Java的核心类库
    3. 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
  • ExtClassLoader扩展类加载器
    1. Java语言编写,是sun.misc.Launcher类下的内部类
    2. 派生于ClassLoader类,父类加载器是启动类加载器
    3. 从jre/lib/ext子目录下加载类库(如果用户创建的Jar放在此目录下,也会自动由扩展类加载器加载)
  • AppClassLoader系统类加载器
    1. Java语言编写,是sun.misc.Launcher的一个内部类
    2. 派生于ClassLoader类,父类加载器是扩展类加载器
    3. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
    4. 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
  • 用户自定义类加载器
    1. 为什么要自定义?
      1. 隔离加载类(中间件和应用隔离的,中间件自带的Jar包和应用的Jar包出现冲突)
      2. 修改类加载的方式(在需要的时候进行加载)
      3. 扩展加载源(其他字节码来源,如数据库)
      4. 防止源码泄露
    2. 如何自定义?
      1. 继承抽象类java.lang.ClassLoader
      2. JDK1.2前,要重写loadClass(),从而实现自定义类加载逻辑;在JDK1.2后不建议覆盖loadClass(),而是把自定义逻辑写在findClass()中
      3. 编写自定义类加载器时,如果没有过于复杂的需求,直接继承URLClassLoader类,这样可以避免自己去编写findClass()及其获取字节码流的方式,编写更加简洁
  • 双亲委派机制
    Java虚拟机采用按需加载的方式加载类,即只有当要使用这个类时才会将它加载到内存。并且他在加载类时采用的是双亲委派模式,即把请求交由父类去处理,具体工作流程:

    1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
    2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
    3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
  • 沙箱安全机制:
    自定义JDK核心类如java.lang.String,但是在加载自定义String类的时候JVM会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java \lang\String.class),报错信息说没有main方法,就是因为加载的是rt. jar包中的String类。这样可以保证对java核心源代码的保护

运行时数据区

PC寄存器

  • 作用:存储指向下一条指令的地址,由执行引擎从PC寄存器中不断的读取下一条指令来执行

PC寄存器

  • 为什么使用PC寄存器记录当前线程的执行地址?
    因为CPU需要不停的切换各个线程,当切换回来后,需要知道接着从哪开始继续执行
  • PC寄存器为什么要设定为线程私有?
    为了能够准确记录各个线程正在执行的当前字节码指令地址,为每个线程分配一个PC寄存器,这样的话线程间独立计算,不会存在相互影响的情况

虚拟机栈

  • 虚拟机栈是什么?
    每个线程在创建时都会同步创建一个虚拟机栈,他的内部保存一个个的栈帧,而每一个栈帧就对应着一次方法调用
  • 虚拟机栈的作用?
    主管Java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回
  • 虚拟机栈可能出现的异常
    1. 如果采用固定大小的虚拟机栈,如果线程请求分配的栈容量超过允许的最大容量,会抛出StackOverflowError异常
    2. 如果采用动态扩展的虚拟机栈,在尝试扩展时无法申请到足够的内存,会抛出OutOfMemoryError异常
  • 设置栈大小
    -Xss256k -Xss1m
  • 栈中数据以栈帧的格式存在,栈帧中存储着:
    1. 局部变量表
      • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
      • 局部变量表的长度(容量)在编译期确定,一旦确定不会再修改
      • 字节码指令
      • 局部变量表长度
      • 行号表对应关系
      • 局部变量表
      • 局部变量表,最基本的存储单元是Slot-变量槽,其中32位以内的类型占用一个slot,64位占用两个slot
      • 如果当前帧是由构造方法或实例方法创建的,那么this指针会放置在第一个slot中
      • slot存在重复利用:如果一个局部变量出了作用域,那么在其作用域之后申请的新的局部变量就很有可能会复用过期布局变量的槽位,从而达到节省资源的目的。
    2. 操作数栈
      • 作用: 在方法执行过程中,根据字节码指令,往栈中写入数据或读取数据,即入栈和出栈操作(栈管运行,局部变量表相当于原材料,操作数栈相当于加工工序)
      • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
    3. 动态链接(指向运行时常量池的方法引用)
      • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
      • 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里

      • 方法调用指令
        1. invokestatic:调用静态方法,解析阶段确定了唯一方法版本
        2. invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
        3. invokevirtual:调用所有的虚方法(子类调用父类的final方法,虽然是invokevirtual,但他实际上是非虚方法)
        4. invokeinterface:调用接口方法
        5. invokedynamic:动态解析出需要调用的方法,然后执行
    4. 方法返回地址(方法正常退出或者异常退出的定义)
    5. 一些附加信息
  1. 举例栈溢出的情况?
    StackOverflowError
    通过-Xss可以设置栈的大小
  2. 通过调整栈的大小,就能保证不出现溢出吗?
    不能,可以分为两种情况:

    1. 假设原栈空间可以压栈5000次,如果程序要压栈6000次,那么就会报SOF错误,但是可以通过调整栈大小使它可以压栈7000次,就不会报错了
    2. 原来的程序是一个递归死循环,会一直压栈,那么再调整也无济于事
  3. 分配的栈内存越大越好吗?
    不是
    理论上是减少了SOF发生的概率,但是整个内存空间是有限的,对于其他空间就小了
  4. 垃圾回收是否会涉及到虚拟机栈?(通过垃圾回收来避免SOF)
    不会
    虚拟机栈会产生Error,但是不会GC,他的垃圾回收->出栈形式
  5. 方法中定义的局部变量是否线程安全?
    具体来看
    如果变量在方法内部就消亡了,那么它是线程安全的;如果变量传递出去了,或者逃逸了(作用域不光在内部了)就是非线程安全的

本地方法栈

  • 本地方法栈和虚拟机栈类似,不同之处在于它用于管理本地方法的调用,而虚拟机栈管理Java方法的调用
  • 当某个线程调用一个本地方法时,他就不在受虚拟机的限制了,和虚拟机拥有了相同的权限(可以通过本地方法接口访问虚拟机内部的运行时数据区;从本地内存堆中分配任意数量的内存)
  • 本地方法是什么?
    一个native修饰的方法就是一个Java调用非Java代码的接口,他的作用是融合不同的编程语言,在定义native方法时,不提供实现体

  • 堆相关重要概念
    1. 一个JVM实例只存在一个堆内存,启动两个Java进程就对应有两个堆内存
    2. Java堆区在JVM启动时候即被创建,他的空间在此时被确定
    3. 堆可以处于物理上不连续的内存空间中,但在逻辑上应该视为是连续的
    4. 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区TLAB(因为堆内存共享,多线程条件下分配内存会有安全问题,所以采用了预先分配一小块内存给每个线程)
    5. 堆、栈和方法区的关系:栈的栈帧中保存着对象引用,引用指向对象在堆中的位置,对象所属类的信息、方法保存在方法区中
    6. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集时候才会被移除
    7. 堆空间是垃圾回收的重点区域
  • 堆内存细分
    1. JDK7及以前堆内存逻辑上分为了三部分:新生代、老年代和永久代
    2. JDK8及以后堆内存逻辑上分为了三部分:新生代、老年代和元空间
  • 堆空间大小设置和查看(堆空间设置仅包含新生代和老年代)
    1. -Xms用于表示堆区的起始内存,如-Xms6m, -Xms1024k(等于Eden区+一块survivor区的空间大小,由于垃圾收集的原因,另一个survivor不存放对象)
    2. -Xmx用于表示堆区的最大内存 (通常起始内存和最大内存设置为相同的数,这样在垃圾回收机制清理完堆区后不需要重新分配计算堆区的大小,提高了性能)
    3. 默认堆空间的大小:初始内存大小=物理电脑内存大小/64, 最大内存大小=物理电脑内存大小/4
    4. 查看设置的参数:
      • jps + jstat -gc 进程id
      • -XX:+PrintGCDetails
  • 新生代和老年代
    • -XX:newRatio=x 默认为2,即新生代:老年代=1:2
    • -XX:SurvivorRatio=x 默认为8,即Eden区、Survivor区=8:1:1(虽然默认是8,但是由于自适应内存分配策略,实际可能为6:1:1)
    • -Xmn:设置新生代空间大小
  • 对象分配过程
    1. new的对象会先放到Eden区。当Eden区放满后,而程序又在创建对象,JVM此时会发起Minor GC,将Eden区中不再被引用的对象销毁,将引用的对象移动到To Survivor区中(同时标记对象年龄为1)

    图片[1]-JVM-一一网
    2. 程序继续运行,如果再次触发垃圾回收,会将Eden区中存活对象放置到To Survivor区,同时检查From Survivor区中对象是否存活,如果存活就移动到到To Survivor区(对象年龄+1)
    图片[2]-JVM-一一网
    3. 如果再次触发垃圾回收,按照步骤1/2继续进行。此时如果Survivor区中存活对象年龄达到15,会默认晋升到老年代
    图片[3]-JVM-一一网
    x. 其他特殊情况

    什么时候对象会进入老年代?

      1. 对象过大,Eden区放不下,会直接放置到老年代
      2. 对象年龄达到阈值,Eden区和Survivor区执行复制存活对象时发现年龄达到阈值,会晋升到老年代
      3. 在Survivor区中,相同年龄所有对象之和大于Survivor区的一半,那么大于等于该年龄的对象会晋升到老年代
      4. 经历过一次Minor GC后,存活对象过多,一块survivor区放不下,就会放置到老年代中
    复制代码
  • GC分类
    • 部分收集:
      1. 新生代收集(Minor GC/Young GC): 只是新生代(Eden\S0,S1)的垃圾收集(注意:不会针对s0发起GC行为,是在收集Eden区时捎带的收集)
      2. 老年代收集(Major GC/Old GC): 只是老年代的垃圾收集
      3. 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集(因为G1将新生代和老年代打通了)
    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

      触发时机:

        1. 调用System.gc()时,系统建议执行full gc,但是不是必然事件
        2. 老年代空间不足
        3. 方法区空间不足
        4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
        5. 由Eden去、Survivor区向另一个Survivor区复制时,对象大小大于他的可用内存,在将该对象转存到老年代时发现老年代也放不下,此时会发生full gc
      复制代码

p83-p86

方法区

  • 方法区相关重要概念
    1. 方法区与堆一样,是各个线程共享的内存区域(多个线程尝试去初始化一个类,只能有一个线程去初始化,把类结构和方法放入方法区)
    2. 方法区在虚拟机启动时候被创建,他的实际物理内存空间和Java堆区一样都是不连续的
    3. 方法区大小和堆空间一样,可以选择固定大小或者可扩展
    4. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛内存溢出错误
    5. JDK7及以前,习惯上将方法区称为永久代;JDK8及以后,使用元空间替代了永久代(元空间使用的是本地内存,而永久代使用的是虚拟机中设置的内存)
  • 设置方法区大小的参数
    1. -XX:PermSize= 设置永久代初始分配空间,默认20.75M
    2. -XX:MaxPermSize= 设置永久代最大可分配空间,32位机器默认64M,64位机器默认82M
    3. 在JDK8后,由于方法区结构的变化,使用MetaspaceSize替换PermSize
  • 方法区的内部结构
    1. 类型信息
      1. 对每个加载的类型(类、接口、枚举、注解),JVM必须在方法区中存储以下类型信息:全路径名,直接父类完整有效名,类型修饰符、直接接口的一个有序列表
    2. 域信息(属性)
      1. JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
      2. 域名称、域类型、域修饰符
    3. 方法信息
      1. JVM必须保存所有方法的相关信息以及其声明顺序
      2. 方法名称、方法返回类型、方法参数的数量和类型、方法的修饰符、方法字节码、操作数栈、局部变量表、异常表
    4. 运行时常量池
      1. 为什么需要常量池(class文件中)?
        • Java是面向对象的编程语言,一个简单的应用程序都需要加载相当多的类,如果这些类都放置到一个class文件中是很大的,而且有一些重复的数据像字符串没有必要重复的定义;所以借助常量池,常量池中存放的是符号引用,只有真正加载到内存中,这些符号引用才会变成直接引用(常量池像是RGB三原色,当需要什么颜色可以从三原色中取出自己去调配需要的颜色)
      2. 常量池是什么?
        常量池它是Class文件的一部分,可以看做是一张表,虚拟机指令通过这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
      3. 运行时常量池是什么?
        • 运行时常量池是方法区的一部分,常量池在经过类加载并进入运行数据区中方法区后就是运行时常量池。
        • 运行时常量池包含多种不同的常量,包括编译期就明确的数值字面量,也包括运行期解析后得到的字段引用(和常量池中符号地址不同,这里是转化后的真实地址)等。相较于常量池它具备动态性(如String.intern())
  • 方法区的演进细节
    1. JDK1.6及之前:有永久代,静态变量存放在永久代上
    2. JDK1.7:有永久代,但已经逐步想去永久代,其中的字符串常量池、静态变量从永久代中移除了,保存到堆中
    3. JDK1.8及之后:无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但是字符串常量池、静态变量仍然存放在堆中

    在JDK8之后,永久代为什么要被移除掉?

      1. 永久代的空间大小很难确定。在设置空间大小较小时,容易发生Full GC,其中STW操作会影响用户线程的效率(如果加载的大多数类都还要使用,就会出现OOM了);设置空间过大,空间就浪费掉了。而元空间使用的是本地内存,不再受虚拟机空间限制
      2. 对永久代进行调优比较困难。full gc
    复制代码

    StringTable的存储位置为什么要进行调整?
    在JDK7及以后,将字符串常量池放入堆空间中。因为永久代的回收效率很低,在full GC的时候才会触发,而full gc是老年代或永久代空间不足时才会触发。这就导致StringTable回收效率不高,而我们开发中又会有大量的字符串被创建,这样的话字符串的回收效率很低,进一步导致永久代内存不足,那放到堆里能够及时回收内存

  • 方法区的垃圾收集
    1. 方法区垃圾收集主要是两部分内容:常量池中废弃的常量和不再使用的类型

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

  1. 对象的实例化
    • 对象创建的方式
      1. new
      2. Class的newInstance(): 只能调用空参的构造器,权限必须是public
      3. Constructor的newInstance(Xxx): JDK9中,可以调用空参、带参的构造器,权限没有要求
      4. 使用clone(): 不调用任何构造器,当前类要实现Cloneable接口
      5. 使用反序列化: 从文件、网络中获取一个对象的二进制流,然后还原为对象
      6. 第三方库Objecnesis
    • 对象创建的步骤
      1. 判断对象对应的类是否已经被加载到内存中,如果没有加载要调用类加载器加载相应的类(虚拟机遇到new指令后,首先检查这个指令的参数能否在元空间的常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么在双亲委派模式下,使用当前类加载器查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象)
      2. 为对象分配内存。首先计算对象占用空间大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可(4字节)。如果内存是规整的,采用指针碰撞法,如果内存不规整采用空闲列表法
      3. 处理并发安全问题。采用CAS失败重试,区域加锁保证更新的原子性;为每个线程预先分配一块TLAB
      4. 初始化分配到空间。为所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用

        属性赋值先后顺序:属性默认初始化 – 显式初始化/代码块中初始化 – 构造器中初始化

      5. 设置对象的对象头。
      6. 执行方法进行初始化(属性赋值的后两个都是在这里进行的)
  2. 对象的内部布局
    1. 对象头
      • 运行时元数据
        • 哈希值
        • GC分代年龄
        • 锁状态标志
        • 线程持有的锁
        • 偏向线程ID
        • 偏向时间戳
      • 类型指针
        • 指向类元数据InstanceClass,确定该对象所属的类型
    2. 实例数据
      • 对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

      规则: 相同宽度的字段总是被分配在一起;父类中定义的变量会出现在子类之前;如果CompactFields参数为true,子类的摘编量可能插入到父类变量的空隙

    3. 对齐填充
      • 占位符作用
    4. 实例
      public class CustomerTest {
          public static void main(String[] args) {
              Customer cust = new Customer();
          }
      }
      
      public class Customer{
          int id = 1001;
          String name;
          Account acct;
      
          {
              name = "匿名客户";
          }
          public Customer(){
              acct = new Account();
          }
      
      }
      class Account{
      
      }
      复制代码

  3. 对象的访问定位
    1. 解决的问题:如何通过栈帧中的对象引用访问到其内部的对象实例(栈上引用)
    2. 分类
      1. 句柄访问
        • 原理:在堆空间中开辟一块区域存放句柄池,句柄池中存放着很多句柄,一个对象对应着一个句柄,一个句柄有两个指针:到对象实例数据的指针(他指向堆空间中的实例)和到对象类型数据的指针(他指向方法区中的对象类型)
        • 优点:在栈空间中维护的引用是很稳定的,如果堆空间中对象发生了移动,只需要修改句柄中的一个指针,不需要修改栈中引用
        • 缺点:对象引用要先找到句柄池中对应句柄,再从句柄出发找到实例和实例类型;而且还要单独开辟句柄池空间,效率较低
      2. 直接指针(Hotspot采用)
        • 原理:栈空间引用直接指向了堆中对象实例,在对象实例中有类型指针指向了方法区中的对象类型
        • 优点:相比句柄访问方式,效率要高

直接内存

  • 重要理论
    1. 不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。
    2. 在Java堆外的、直接向系统申请的内存空间(Java进程所占用的内存空间可以理解为 堆+直接内存)
    3. 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
    4. 通常,访问直接内存的速度会优于Java堆(读写频繁的场合可以考虑使用直接内存;Java的NIO库允许Java程序使用直接内存,用于数据缓冲区)
    5. 直接内存也可能出现OOM: Direct buffer memory(由于直接内存在Java堆外,当设置的-Xmx执行的最大堆很接近操作系统的最大内存时,直接内存是有可能出现OOM的)
    6. 缺点:分配回收成本较高;不受JVM内存回收管理
    7. 直接内存大小可以直接通过MaxDirectMemorySize设置(如果不指定,默认与堆得最大值-Xmx参数值一致)

执行引擎

  • Java代码的执行分类
    1. 将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
    2. 编译执行,直接编译为机器码。由即时编译器将方法编译为机器码后再执行
  • 解释器和JIT编译器各自的优缺点
    1. 解释器响应速度快。程序启动,解释器拿到字节码马上就可以在PC计数器的帮助下逐行解释字节码;而JIT需要先进行编译成机器指令,然后再去执行
    2. JIT编译器执行效率高。他将字节码编译为机器指令后,可以重复去执行,热点代码执行效率高

字符串拼接

  1. 字符串拼接面试题总结
    1. 常量与常量的拼接结果在常量池中,原理是编译期优化
    2. 常量池中不会存放相同内容的常量
    3. 只要拼接操作中有一个是变量,他的存储位置就在堆中,变量拼接的原理是StringBuilder(final String s = “1”,不算变量,此时存在编译期优化)
    4. 如果拼接的结果调用intern(),则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
  2. intern方法
    • 判断字符串常量池中是否存在该字符串值(内部使用equals判断),如果存在,则返回常量池中该字符串的地址;如果字符串常量池中不存在,则在常量池中加载一份,并返回该对象的地址。
    • 在JDK1.6及之前,如果字符串常量池中没有,会把此对象复制一份,放入字符串常量池,并返回字符串常量池中的对象地址;在JDK1.7及之后,如果字符串常量池中没有,会把对象的引用地址复制一份,放入池中,并返回串池中的引用地址
  3. 如何保证变量s指向的是字符串常量池中的数据呢?
    1. String s = “shkstart”;//字面量定义的方式
    2. 调用intern()
        String s = new String("shkstart").intern();
        String s = new StringBuilder("shkstart").toString().intern();
    复制代码
  4. new String(“ab”)会创建几个对象?
    • 两个对象,一个是new关键字在堆空间创建的,另一个是字符串常量池中的对象
  5. new String(“a”) + new String(“b”)会创建几个对象?
    • 六个对象,拼接操作StringBuilder,String(“x”)会创建两个,共四个,返回为String类型创建一个String对象
    • 此操作不会在字符串常量池中生成对象”ab”
  6. 关于intern()的面试难题?
    String s = new String("1");// s指向堆中创建的字符串对象1
    s.intern();//调用此方法之前,字符串常量池中已经存在了"1",所以此操作没有起到任意效果
    String s2 = "1";// 此时s2执向的是s放置在字符串常量池中的1
    System.out.println(s == s2);//jdk6:false   jdk7/8:false		一个是堆空间中的,一个是常量池中的,一定是不相等的
    
    String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")   并且字符串常量池中不会存在"11"
    s3.intern();//在字符串常量池中生成"11"。
    // jdk6:由于字符串常量池位于永久代中,必然会创建了一个新的对象"11",他的新地址位于永久代中。 jdk7:由于将字符串常量池移动到了堆空间中,为考虑空间的节约,此时常量池中并没有创建"11",而是复制堆空间中现有的"11"对象的地址放到常量池中
    String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
    System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
复制代码

图片[4]-JVM-一一网

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享