Java后端面试学习知识总结——JVM

文章目录

  • Java的平台无关性
  • JVM如何加载.class文件
  • Java运行时数据区
  • 方法区、永久代和元空间的关系,方法区的前世今生
  • ClassLoader
  • ClassLoader的种类
  • 类加载器的双亲委派机制
  • 为什么要使用双亲委派机制
  • Java的类加载机制(类的生命周期)
  • 类的加载方式,loadClass和forName的区别
  • 什么是反射
  • JVM相关常考题与面试真题总结
      • 同一个类可以被加载两次么?
      • JVM三大性能调优参数:
      • JVM内存模型中内存分配策略区别
      • JVM内存模型中堆和栈的区别
      • 字符串常量池相关
        • Java对象在内存中的结构是什么样的?

Java的平台无关性

JVM就是Java虚拟机,Java的跨平台机制就是建立在强大的Java虚拟机的基础上。Java是一种先编译后解释型的语言,当我们写了一段Java代码,在运行之前,它还是一个.java文件,里面是Java语言编写的源码,当Java文件被IDE执行,或者被显式利用javac命令进行编译后,java文件会首先被编译成字节码文件,也就是.class结尾的文件。然后字节码文件再由不同平台的JVM虚拟机去解释/编译成机器识别的语言去让CPU运行。

Java文件编译成class文件之后,在不同平台上运行时(比如windows、Linux或者IOS等)不需要再次编译,因为JVM屏蔽了操作系统底层的差异,会把字节码解释/编译成不同平台上的机器指令。
Java后端面试学习知识总结——JVM_第1张图片

这就是为什么Java语言有平台无关的特性,就归功于JVM的强大,一次编译,跨平台可用。

JVM如何加载.class文件

既然JVM是对class文件进行操作的,那么JVM是如何将.class文件加载到内存中的呢?主要利用的是Class Loader来加载文件,下面看一张JVM组成的抽象结构图:
Java后端面试学习知识总结——JVM_第2张图片
上图中:

  • Class Loader 字节码加载器:也被称作为类加载器,JVM通过类加载器将class文件,也就是类加载到JVM中。同时,Class Loader也负责将加载后的字节码解析成JVM统一要求的对象格式。
  • Excution Engine 执行引擎:是JVM的核心组成部分之一,负责将字节码解释/编译成系统对应的机器指令。
  • Native Interface 本地库接口:融合不同开发语言的原生库为JVM所用,比如C++/C语言的库。

JVM就是通过类加载器将字节码加载进内存当中的(JVM运行在内存中)。

Java运行时数据区

在前文展示的JVM结构抽象图中,可以看到有一个区域被称为运行时数据区,这一部分是JVM运行最核心的部分。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域各自有各自的用途,以及创建和销毁的时间,有的区域会随着虚拟机进程的启动而一直存在,有的区域则是依赖用户线程的启动和结束而建立和销毁。

  • 线程私有区域:线程私有区域的内存空间是和线程的创建与销毁绑定的,每个线程都有各自独立的私有区域空间。
    • 程序计数器
      • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行字节码的行号指示器,告诉JVM执行到了哪一行。
      • 程序计数器与线程是一对一私有的,每个线程都有自己的程序计数器。当多线程轮流切换执行时,就是通过程序计数器来恢复上次执行到的位置。
      • 如果线程执行的是Java方法,计数器记录的是字节码指令的地址,如果是Native方法,计数器值为空(Undefined)。
      • 程序计数器空间中不会发生内存泄漏。
    • Java虚拟机栈
      • 与程序计数器一样,Java虚拟机栈也是线程私有的,生命周期与线程一致。虚拟机栈描述的是Java方法执行的线程内存模型,每个方法执行的时候,JVM都会同步创建一个栈帧用来存储局部变量表,操作数栈、动态链接、方法出口等一系列信息。
        • 局部变量表: 用来存储方法执行过程中的所有变量,可能是基本数据类型,也可能是对象引用等。
        • 操作数栈:存储入栈、出栈、复制、交换、产生消费变量等信息。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。
      • 一个方法被调用,就相当于入栈,一个方法执行完毕就会执行出栈。
      • Java虚拟机栈内存区域可能会出现两种爆内存的异常:StackOverflowErrorOutOfMemoryError异常。
        • StackOverflowError:当线程请求的栈深度大于虚拟机所允许的深度,就会抛出这个异常。常见的情形比如递归过深的时候就会抛出栈溢出异常。
        • OutOfMemoryError:如果Java虚拟机容量可以动态扩展,当栈扩展时无法申请到足够的内存就会抛出OOM异常(HotSpot虚拟机不支持动态扩展,不会出现这种情况下的OOM);如果Java虚拟机不支持动态扩展,那么栈如果申请到了内存就不会再出现OOM异常了,但是如果栈在申请空间时申请失败,还是会抛出OOM异常(比如创建大量线程,每个线程都要申请栈空间,当内存达到上限,就无法申请空间,就会抛OOM)。
    • 本地方法栈
      • 本地方法栈的作用于虚拟机栈是非常相似的,不过本地方法栈只服务于本地(Native)方法。

  • 线程公有区域: 线程公有区域的内存空间是随着虚拟机创建而创建的,其生命周期与虚拟机一致,所有线程共享公有区域空间,共享公有区域的数据。
    • Java堆
      • Java堆是JVM所管理的内存中最大的一块。《Java虚拟机规范》中对Java堆的的描述是:所有对象示例以及数组都应当在对上分配。虽然随着Java虚拟机技术的发展,栈上分配的优化手段导致了变化的发生,但这一规定绝大多数时候还是适用的。
      • Java堆是垃圾收集器管理的主要内存区域,所以有时也会被称为GC堆。从内存回收的角度来看,由于经典的垃圾收集器都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”、“老年代”这些叫法,而这些区域只不过是按照分代收集的理论思想,将堆内存划分成不同的区域,这些区域划分只不过是一部分垃圾收集器的设计风格,而并非JAVA虚拟机具体实现的内存布局,更不是《Java虚拟机规范》中的规定。
      • 根据《Java虚拟机规范》中的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,在实际应用中,连续还是不连续,取决于垃圾回收算法是整理型算法还是清除型算法。
    • 方法区
      • 方法区与Java堆一样是线程共享区域,主要用来存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
      • 方法区也会发生垃圾收集,主要针对常量池的回收和对类型的卸载。所以有可能发生内存泄漏。
      • 如果方法区无法满足新的内存分配需求时,会抛出OOM异常。

方法区、永久代和元空间的关系,方法区的前世今生

说到方法区,就很容易引出“永久代”这个概念,尤其是在JDK1.8之前,在很多程序员眼中,永久代就是方法区。这是因为大部分的Java程序员都会在HotSpot虚拟机上进行开发和部署程序。

方法区是《Java虚拟机规范》中对内存的逻辑划分,而“永久代”这个概念,是JDK1.8之前HotSpot对方法区的一种实现方式。本质上这二者并不是等价的。对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。

  • 永久代(PermGen space”):永久代的设计中,使用的是JVM虚拟机内存的一部分,这种设计导致了Java更容易遇到内存溢出的问题。因为永久代中存储的有字符串常量池、静态变量等数据,这部分数据很容易溢出。

  • 元空间(MetaSpace):既然永久代有这么多缺点,那么Java虚拟机的开发人员自然需要想办法进行解决。

    • 于是在JDK1.7中,为了更好地移植JRockit虚拟机中的优秀功能,开发团队将永久代中的字符串常量池、静态变量等移出了永久代,将其放进了堆内存中。
    • 而到了JDK1.8之后,开发团队终于完全废弃了永久代的设计,转而使用了元空间(MetaSpace)对其进行替代。元空间与永久代最大的不同就是使用了本地内存,不需要再去占用JVM内存空间了,这样就保证了方法区很少会出现内存溢出的问题。永久代中剩余的存储数据被完整移到了元空间中,永久代从此退出历史舞台。

ClassLoader

下面来具体讲解一下ClassLoader,ClassLoader负责将Class文件(这里的Class文件是指一种二进制字节流,包括但不限于在磁盘上的文件、网络、数据库或者动态生成的)加载到内存中,并转换成JVM统一要求格式的对象。

比如我们新建了一个Robot.java的文件,这个文件承载了我们写的源码——一个Robot类,然后经过javac指定的编译,就变成了一种承载二进制字节流的.class文件,也叫字节码文件,此时如果JVM正在运行的程序依赖这个字节码文件,ClassLoader就会加载Robot.class文件到内存中,并将其转换为Class对象(JVM统一要求的格式,类对象),然后需要实例化出Robot对象时,JVM会根据Class对象将Robot对象实例化出来。

所以ClassLoader在Java中有着非常重要的作用,其主要工作在Class装载的加载阶段(一个Class的生命周期分为加载、验证、准备、解析、初始化、使用、卸载七个阶段,下文有具体分析),其主要作用是从系统外部获得Class二进制数据流。是Java的核心组件,在JVM中,所有的Class都是由ClassLoader进行加载的。在ClassLoader将二进制数据流加载到内存中后,JVM才能进行剩下的链接、初始化等操作。


ClassLoader的种类

ClassLoader一共有四种:

  • BootStrap ClassLoader(启动类加载器):C++编写,用来加载核心库java.*中的Class。

  • Extension ClassLoader(扩展类加载器):Java编写,用来夹在扩展库javax.*中的Class。

  • Application ClassLoader(应用类加载器):Java编写,用来加载程序所在目录中的Class。

  • 自定义ClassLoader(自定义类加载器):Java编写,定制化加载策略。

    • 自定义的ClassLoader中,findClass函数指定加载路径去加载类,加载完成后用defindClass解析返回一个类对象。
  • 实现一个自己的类加载器,自定义类加载器代码演示:
    首先在桌面创建一个java文件Wali.java:

public class Wali{
	static{
		System.out.println("Hello Wali!");
	}
}

然后调用javac编译成字节码文件Wali.class,最后在项目中创建一个自己的类加载器类MyClassLoader.java:

import java.io.*;

/**
 * 自定义类加载器要继承自ClassLoader
 */
public class MyClassLoader extends ClassLoader {

    // 加载路径
    private String path;
    // 自定义加载器的名字
    private String classLoaderName;

    public MyClassLoader(String path, String classLoaderName) {
        this.path = path;
        this.classLoaderName = classLoaderName;
    }

    // 覆盖findClass方法来自定义加载策略
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = classLoaderData(name);
        return defineClass(name, b, 0, b.length);
    }

    /**
     * 用来加载类文件
     * @param name
     * @return
     */
    private byte[] classLoaderData(String name) {
        // 生成文件全路径名
        name = path + name + ".class";
        InputStream in = null;
        ByteArrayOutputStream outputStream = null;
        try {
            in = new FileInputStream(new File(name));
            outputStream = new ByteArrayOutputStream();
            int i = 0;
            while ((i = in.read()) != -1) {
                outputStream.write(i);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return outputStream.toByteArray();
    }
}

最后对类加载器进行验证:

public class ClassLoderChecker {

	public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		MyClassLoader mc = new MyClassLoader("C:/Users/SMM/Desktop/", "myClassLoader");
		Class c = mc.loadClass("Wali");
		System.out.println(c.getClassLoader());
		c.newInstance();
	}
}

输出信息:
Java后端面试学习知识总结——JVM_第3张图片
发现确实实现了自定义路径的类加载。


类加载器的双亲委派机制

在JVM中,不同加载器之间是有层次关系的,下图展示的不同类加载器之间的关系就被称为类加载器的“双亲委派模型”
Java后端面试学习知识总结——JVM_第4张图片
双亲委派模型要求除了顶层的启动类加载器之外,别的类加载器都应该有自己的父类加载器。不过这里的父类加载器一般不是以继承的关系实现的,而是通常使用组合的关系来复用父加载器的代码。即在子类中创建一个父类实例,通过委派的机制将加载的任务发给父类来做。

双亲委派机制加载类的流程如下,先是自底向上查找,然后自顶向下加载:
Java后端面试学习知识总结——JVM_第5张图片
该流程描述:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去加载,每一个层次的类加载器都是如此,因此所有的加载请求最终都会汇聚到顶层加载器,即启动类加载器中去尝试执行,只有当父类加载器反馈自己无法完成这个加载请求(在它的搜索范围内没有找到这个类)时,子类才会尝试自己去加载。

加载流程的核心源码在ClassLoader抽象类中的loadClass(String name, boolean resolve)方法中,源码分析如下:
Java后端面试学习知识总结——JVM_第6张图片

为什么要使用双亲委派机制

类加载器的双亲委派模型在JDK1.2时期被引入,并被广泛应用于此后几乎所有的Java程序中,但双亲委派机制并不是有强制性约束力的要求,而是Java设计者们推荐给开发者的一种类加载的最佳实践策略。

使用双亲委派模型来维护类加载器之间的关系,一个最明显的好处就是Java中的类随着它的类加载器一期具备了一种带有优先级的层次关系。比如java.lang.Object这个类,是Java中非常重要的源码类之一,存放在rt.jar之中,无论是哪一个类加载器要加载这个类,最终都是要委派给顶层的启动类加载器进行加载,因此Object类在程序的各种类加载环境中都可以稳定保证是同一个类。

反之,如果没有双亲委派模型,都任由各个类加载器自行去加载的话,就会出现类型体系紊乱的问题,比如自定义加载器A加载了Object类,自定义加载器B又加载了Object类,那么在JVM内存中这两个Object类对象是不一样的,依据类对象实例化的业务对象也会出现类型不匹配的问题。

或者用户自定义了一个java.lang.Object类,并放在Classpath路径中,如果不使用双亲委派模型,那么系统中也会出现多个不同的Object类。

读者可以尝试自定义一个与rt.jar类库中已有类重名的Java类,会发现它虽然可以正常编译,但却永远无法被加载运行。


Java的类加载机制(类的生命周期)

Java中的类加载机制是指把一个class二进制流加载到内存中并最终形成JVM可用的Java类型对象的流程机制,其中一共分为五个阶段加载、验证、准备、解析和初始化。其中验证、准备、解析阶段可以统称为链接阶段。

而Java类的生命周期从被加载到JVM内存中开始,到卸载出内存为止,整个生命周期在类加载流程的基础上又多出了使用卸载两个阶段。即:加载、验证、准备、解析、初始化、使用、卸载
Java后端面试学习知识总结——JVM_第7张图片

  • 加载:加载(loading)是类加载(ClassLoading)的一个阶段,加载阶段就是类加载器将Class二进制字节流(一般是.class文件)加载进JVM内存,并生成一个代表这个类的Class对象。具体流程分为三步:
  1. 通过一个类的全限定类名(com.xxx.xxx.Demo.java)来获取定义此类的二进制字节流。
  2. 通过将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表了这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  4. 加载阶段和连接阶段的部分动作是交叉进行的,可能加载还未结束,但是连接已经开始。
  • 验证:验证是连接的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的约束。其中又包含了四种验证步骤:
  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证
  • 准备:准备阶段是正式为类中定义的变量(类变量即静态变量,被static修饰的变量)分配内存并设置类变量初始的值的阶段。
  • 解析:解析阶段是JVM虚拟机将常量池内的符号引用替换为直接引用的过程。
  • 初始化:类的初始化阶段是类加载过程的最后一个阶段,在准备阶段的时候类变量已经被赋了初始值,在初始化阶段就会执行变量赋实际值值,并且该阶段会执行静态代码块中的内容。

至此,类加载的流程就结束了,经过加载、连接和初始化,JVM内存中就会出现被加载类对应的Class对象了。

  • 使用:使用阶段是类的生命周期的一部分,但是已经不是类加载的阶段了,该阶段就是使用一个类提供的功能,包括主动引用和被动引用,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。
  • 卸载:在类使用完之后,如果满足下面的情况,类就会被卸载:
  1. 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。

  2. 加载该类的ClassLoader已经被回收。

  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

    如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

类的加载方式,loadClass和forName的区别

类的加载方式有两种:

  • 隐式加载:new关键字在创建对象的时候,其实是在向JVM中加载字节码流。
  • 显式加载:使用loadClass方法和forName方法。对于显式加载这种类加载方式来讲,当我们获取到了 Class 对象后,需要调用 Class 对象的 newInstance() 方法来生成对象的实例。

显示加载中最常用的两种方式:loadClass()forName():首先,不管是loadClass()还是forName(),都可以获取类的属性和方法,也可以调用它的任意一个方法和属性(枚举类除外),这也是实现放射的重要方式。

  • loadClass方法:在前文中,我们对loadClass的源码进行了分析,其中有一个参数是布尔值的resolve,这个参数默认是false的。
    Java后端面试学习知识总结——JVM_第8张图片
    然后再loadClass真正干活的方法中会进行判断,如果resolve为true,就执行resolveClass()方法:
    Java后端面试学习知识总结——JVM_第9张图片
    这个方法有什么用呢,点进去可以看到JDK的注释,非常清晰:
    Java后端面试学习知识总结——JVM_第10张图片
    而默认传入的是false,也就是说,默认是不执行这一步的,也就是说,使用默认的loadClass()方法获得的Class对象时还没有进行连接的,只是执行了类装载机制中的第一步:加载。

  • forName方法:同loadClass()一样,我们直接去看源码,源码之前没有秘密,forName()方法是Class类提供的一个方法,点进去就可以找到,可以发现在forName()的执行函数参数中有一个布尔值initalize:
    Java后端面试学习知识总结——JVM_第11张图片
    Java后端面试学习知识总结——JVM_第12张图片
    forName0()这个方法是一个Native的方法,在JDK中是看不到的,需要去OpenJDK的官网看源码,实际上当initialize为true的时候,就会执行连接和初始化操作。

  • 也就是说Class.forName()得到的Class对象是已经初始化完成的,而Classloader.loadClass()得到的Class对象是只进行了加载还未连接的对象。

  • 二者的使用场景:

    • forName():由于forName()会执行完初始化阶段,也就是说类中的静态代码块会被执行完毕,当我们的类中有静态代码块需要执行的时候,就适合使用forName()。比如著名的JDBC驱动在使用的时候就需要用forName(),这是因为JDBC驱动中有一个静态代码块是向DriveManager中注册自己,所以必须初始化。
    • loadClass():而loadClass()虽然不会连接,但是因为只加载类,执行速度就非常快,当我们需要大量加载类,并且暂时用不到的时候,就可以使用loadClass()来进行延迟加载。比如著名的SpringIOC容器,就是使用loadClass()来进行延迟加载,提高加载速度,当用到类的时候,再进行初始化。

什么是反射

Java的反射机制是指:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。

在实际的开发中,我们很少会直接使用反射,但是反射在各种框架中经常使用,理解反射有利于我们理解框架的原理。下面贴一段反射使用的示例代码:

  • 首先建一个类,其中有私有成语变量,私有方法和公有方法:
public class Robot {

    // 私有成员变量
    private String name;

    // 公有方法
    public void sayHi(String helloSentence) {
        System.out.println(helloSentence + "" + name);
    }

    // 私有方法
    private String throwHello(String tag) {
        return "Hello" + tag;
    }
}
  • 然后开始使用反射来获取和执行其中的属性与方法:
import com.Robot;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectRobot {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
            InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        // 装载类,获取类对象
        Class robotClass = Class.forName("com.Robot");
        // 根据类对象实例化出对象
        Robot robot = (Robot)robotClass.newInstance();
        // 查看类对象的名字
        System.out.println("Class Name Is " + robotClass.getName());

        // 利用反射获取私有方法,参数为需要获取的方法名字和参数类的类型
        Method getThrowHello = robotClass.getDeclaredMethod("throwHello", String.class);
        // 强制允许访问私有方法
        getThrowHello.setAccessible(true);
        // 调用私有方法,传参为对象实例和方法需要的参数
        Object str = getThrowHello.invoke(robot, "ReflectVisitPrivate");
        System.out.println("Do throwHelle by reflect " + str);

        // 利用反射获取公有方法
        Method getSayHi = robotClass.getMethod("sayHi", String.class);
        getSayHi.invoke(robot, "ReflectVisitPublic");

        // 利用反射获取私有变量
        Field name = robotClass.getDeclaredField("name");
        // 强制允许访问私有变量
        name.setAccessible(true);
        // 给私有变量赋值
        name.set(robot, "ReflectSetPrivateName");
        getSayHi.invoke(robot, "ReflectVisitPublic2");
    }

}

  • 执行结果:
    Java后端面试学习知识总结——JVM_第13张图片

JVM相关常考题与面试真题总结

同一个类可以被加载两次么?

可以,可以通过构造一个自定义加载器,并且覆盖父类的loadClass方法来加载指定的类,此时JVM虚拟机中就会有同一个类加载出的两个类对象,一个是自定义加载器加载的,一个是应用类加载器加载的。

而由于在JVM中,对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立在JVM中的唯一性,JVM中每一个类加载器都拥有一个独立的类名称空间。也就是说,只有在用一个类加载器加载的情况下,才能比较两个类是否“相等”(包括equals(), isAssignableFrom()等方法)。

而由于自定义类加载器和应用类加载器不是同一个类加载器,所以被这两个类加载器加载的同一个类,在JVM中实例出了两个不相等的类对象,互相独立。

JVM三大性能调优参数:

  • Xms:设定了堆空间的初始值。
  • Xmx:设定了堆空间能达到的最大值。
  • Xss:规定了每个线程的虚拟机栈的大小。
  • 使用方法示例:java -Xms 128m -Xmx 128m -Xss 256k -jar xxxx.jar

JVM内存模型中内存分配策略区别

  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求。
  • 栈式存储:数据区需求在编译时为止,运行时模块入口前确定。
  • 堆式存储:编译时或者运行时模块入口都无法确定,此时需要动态分配。

JVM内存模型中堆和栈的区别

  • 联系:引用对象、数组的时候,栈里定义变量保存堆中目标的首地址。
    Java后端面试学习知识总结——JVM_第14张图片
  • 管理方式的区别:栈在方法运行结束后会自动释放空间,堆空间需要GC回收空间。
  • 空间大小:栈比堆的空间要小。
  • 空间碎片:栈产生的碎片远小于堆。
  • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配,
  • 效率:栈的效率比堆高

字符串常量池相关

  • String s1 = new String("a");String s2 = "a"; 二者相等么?
    Java后端面试学习知识总结——JVM_第15张图片
    Java后端面试学习知识总结——JVM_第16张图片
    答案是不相等,二者内存地址并不一样。这是因为s1在创建的时候使用了new的方式创建,而s2使用了""方式,这两种方式是有区别的:
  • ""中声明的字符串会在字符串常量池中直接创建出来。
  • new出来的字符串对象会先尝试在字符串常量池中创建,然后在堆空间中创建出来字符串对象。
  • 该段代码流程如下:
    Java后端面试学习知识总结——JVM_第17张图片
  • JDK还提供了一个函数名为intern():一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象的引用添加到池中,并返回此String对象的引用。
    • intern()设计的初衷是为了复用String对象,用来节省空间,在JDK1.7字符串常量池从永久代被移到了堆内存中,intern()的机制也发生了变化,本文不介绍1.7之前的intern(),只来验证1.7之后的intern()相关的机制。下面贴一段代码:
      Java后端面试学习知识总结——JVM_第18张图片
      Java后端面试学习知识总结——JVM_第19张图片
      该段代码解释如下:
      Java后端面试学习知识总结——JVM_第20张图片
      其内存执行流程如下:
      Java后端面试学习知识总结——JVM_第21张图片

Java对象在内存中的结构是什么样的?

可以看另外一篇文章具体的分析:Java对象的结构与对象在内存中的结构。

你可能感兴趣的:(Java学习,java,面试)