java面试Java后端开发岗面试中JVM(java虚拟机)相关的常见问题

Posted 棉花糖灬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java面试Java后端开发岗面试中JVM(java虚拟机)相关的常见问题相关的知识,希望对你有一定的参考价值。

0. JVM的组成部分及作用

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Native Interface)

首先通过类加载器会把 Java 代码转换成字节码,运行时数据区再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。



1. 运行时数据区域

  • 程序计数器:是当前线程所执行的字节码的行号指示。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域
  • Java虚拟机栈:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
  • 本地方法栈:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务。本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常
  • Java堆:Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常
  • 方法区:是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。早期,HotSpot使用永久代来实现方法区,后放弃永久代,逐步改用本地内存来实现方法区,JDK8以后使用元空间来代替永久代。如果方法区无法满足新的内存分配需求时,将抛出
    OutOfMemoryError异常。
    • 运行时常量池:是方法区的一部分,存放常量池表,用于存放编译期生成的各种字面量与符号引用。当常量池无法再申请到内存时会抛出OutOfMemoryError异常

直接内存:并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域

可以把Java内存区域笼统地划分为堆内存和栈内存

2. 垃圾收集与内存分配策略

垃圾收集(Garbage Collection,GC),其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,当方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆和方法区这两个区域则有着很显著的不确定性,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。

(1) 判断对象是否已死

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”

  • 引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。原理简单,判定效率高,但需要额外的内存空间,因很难解决对象之间的相互循环引用的问题,此时引用计数器永远不为 0,导致无法对它们进行回收,因此主流的Java虚拟机都没有选用该方法来管理内存
  • 可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的

(2) 引用类型

将引用分为强引用、软引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。

  • 强引用:只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
  • 软引用:在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收
  • 弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生为止
  • 虚引用:也称为“幽灵引用”或者“幻影引用”,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

(3) 回收方法区

方法区垃圾收集的“性价比”通常是比较低的。方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型

  • 废弃的常量:已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量

  • 不再使用的类型:

    • 该类所有的实例都已经被回收
    • 加载该类的类加载器已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

    Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收

(4) 垃圾收集算法

从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”

1) 分代收集理论

收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集
    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集,回收比Full GC频繁
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单
      独收集老年代的行为
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收
      集器会有这种行为
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

2) 标记-清除(Mark-Sweep)算法

首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。缺点是执行效率不稳定,且存在内存空间的碎片化问题

3) 标记-复制算法

简称为复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。缺点是将可用内存缩小为了原来的一半。采用这种收集算法去回收新生代

Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保

4) 标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的

新生代垃圾回收器一般采用的是复制算法;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

(5) 经典垃圾收集器

如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器

  • Serial收集器:一个单线程工作的收集器,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择
  • ParNew收集器:实质上是Serial收集器的多线程并行版本
  • Parallel Scavenge收集器:基于标记-复制算法实现的,是能够并行收集的多线程收集器,目标是达到一个可控制的吞吐量
    • 垃圾收集的自适应的调节策略
  • Serial Old收集器:是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法
  • Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现
  • CMS收集器:是一种以获取最短回收停顿时间为目标的收集器,是基于标记-清除算法实现的,适用于基于浏览器的B/S系统的服务端
  • Garbage First收集器:简称G1收集器,主要面向服务端应用的垃圾收集器

(6) 内存分配与回收策略

新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。

  • 对象优先在Eden分配:对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
  • 大对象直接进入老年代:大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。指定大于该设置值的对象直接在老年代分配
  • 长期存活的对象将进入老年代:对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中
  • 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
  • 空间分配担保:在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的,否则就要进行一次Full GC

3. 类加载机制

(1) 类加载的执行过程

类型的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三个部分统称为连接

  • 加载: 根据查找路径找到相应的 class 文件然后导入
  • 验证: 检查加载的 class 文件的正确性,运行后不会危害虚拟机自身的安全
  • 准备: 给类中的静态变量分配内存空间
  • 解析: 将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址
  • 初始化: 对静态变量和静态代码块执行初始化工作

(2) 类加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

(3) 类加载器分类

站在Java虚拟机的角度来看,分为启动类加载器和其他所有的类加载器。站在Java开发人员的角度来看,分为启动类加载器、扩展类加载器和应用程序类加载器

(4) 双亲委派模型

各种类加载器之间的层次关系被称为类加载器的“双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

4. Java内存模型与线程

(1) Java内存模型

Java内存模型用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果,目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。

Java内存模型把内存划分为主内存和工作内存,规定了所有的变量都存储在主内存中;线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

主内存、工作内存与Java堆、栈、方法区等并不是同一个层次的对内存的划分,这两者基本上是没有任何关系的。主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更基础的层次上说,主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(或者是硬件、操作系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的是工作内存

(2) 内存间交互操作

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

(3) Java内存模型必须满足的规则

不允许read和load、store和write操作之一单独出现;不允许一个线程丢弃它最近的assign操作;不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中;一个新的变量只能在主内存中“诞生”;一个变量在同一个时刻只允许一条线程对其进行lock操作;如果对一个变量执行lock操作,那将会清空工作内存中此变量的值;如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作;对一个变量执行unlock操作之前,必须先把此变量同步回主内存中

(4) 对于volatile型变量的特殊规则

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制

当一个变量被定义成volatile之后,它将具备两项特性:第一项是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。volatile变量的运算在并发下一样是不安全的。使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

(5) 针对long和double型变量的特殊规则

允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非
原子性协定”

(6) 原子性、可见性与有序性

  • 原子性:
  • 可见性:可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改,除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized和final
  • 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的

(7) 先行发生原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到

(8) 线程的实现

实现线程主要有三种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现)。

  • 内核线程实现:内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上
  • 用户线程实现:狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的
  • 混合实现:既存在用户线程,也存在轻量级进程

(9) Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种,分别是协同式(Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度。

如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。优点实现简单,切换操作对线程自己是可知的;缺点线程执行时间不可控。如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定

(10) 线程状态转换:

  • 新建(New)
  • 运行(Runnable)
  • 无限期等待(Waiting)
  • 限期等待(Timed Waiting):
  • 阻塞(Blocked)
  • 结束(Terminated)

(11) 协程和纤程

协程的主要优势是轻量,无论是有栈协程还是无栈协程,都要比传统内核线程要轻量得多。对于有栈协程,有一种特例实现名为纤程(Fiber)

5. 线程安全与锁优化

(1) 线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的

(2) Java语言中的线程安全

可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

  • 不可变:不可变(Immutable)的对象一定是线程安全的
  • 绝对线程安全:绝对的线程安全能够完全满足Brian Goetz给出的线程安全的定义
  • 相对线程安全:相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的
  • 线程兼容:线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用
  • 线程对立:线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码

(3) 线程安全的实现方法

  • 互斥同步(阻塞同步):同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量Mutex)和信号量(Semaphore)都是常见的互斥实现方式。互斥是方法,同步是目的。,最基本的互斥同步手段就是synchronized关键字。会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止
  • 非阻塞同步:互斥同步属于一种悲观的并发策略,其总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享的数据是否真的会出现竞争,它都会进行加锁。乐观并发策略:不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止
  • 无同步方案:有一些代码天生就是线程安全的。可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行

(4) 比较并交换(Compare-and-Swap,下文称CAS)

CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。上述的处理过程是一个原子操作

(5) 锁优化

如适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等

  • 自旋锁与自适应自旋:为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的
  • 锁消除:锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除
  • 锁粗化:如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
  • 轻量级锁:
  • 偏向锁:它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。


(6) TreadLocal原理

ThreadLocal本质是变量 ,它为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 每个线程都有一个map,类似于context,用于存储每一个线程的变量副本。既然是map,key就是ThreadLocal名,value就是变量副本。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题, 同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量,这时该变量是多个线程共享的, 程序设计和编写难度相对较大 。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。

线程里面会有一个context,即上下文,可以往context里面存放东西,随后在线程管辖范围内都可以获取到。ThreadLocal在set存数据到线程context的时候,把自己(this)也放进去

内存泄漏就是东西放在内存里面,但你忘记它放哪里了,它占着一块内存,但是不能回收。 ThreadLocal作为key,存入ThreadLocalMap里面,但是因为key被包装成弱引用,很容易导致内存泄漏 。

//声明一个ThreadLocal变量b,此时开辟了一块内存,里面存放的b对象
ThreadLocal<Integer> b = new ThreadLocal<Integer>();

//注意,这里不是赋值,赋值是=操作符
b.set(10)

(1)10不是放在了b里面,10和b是两个独立存放的东西,不是包含关系。

(2)10和b是两个独立存放的变量,如果其中的一个被清理,那么另外一个不受影响的。

10和b两个的独立存放的东西,只不过我们不能直接访问到10,必须通过b来传话,原因很简单,在map里面,value要通过key来访问。

不过,此时b被包装成了弱引用,也就是说它被打了一个标签,这样它很容易被gc。一旦b被清理了,10就找不到了,从而造成了内存泄漏。

(1)有两个变量a和b,如果后面不再参与计算,则会被自动回收;

(2)有两个变量a和b,存放在map里面,a是key,b是value。如果map一直存在,a和b因为被map关联,则a和b就一直不能被回收;

(3)有两个变量a和b,存放在map里面,a是key(但是a被弱引用包装了一下),b是value。如果map一直存在,a和b因为被map关联,则b就一直不能被回收,但是a可以被回收。一旦a回收了,那么无法通过a找到b了,这就是b出现内存泄漏。

以上是关于java面试Java后端开发岗面试中JVM(java虚拟机)相关的常见问题的主要内容,如果未能解决你的问题,请参考以下文章

java面试Java后端开发岗面试中操作系统相关的常见问题

java面试java后端开发岗面试中数据结构相关的常见问题

java面试Java后端开发岗面试中计算机网络相关的常见问题

Java面试java后端开发岗面试中MySQL数据库相关的常见问题

[ Java面试题 ]Java 开发岗面试知识点解析

Java开发岗面试知识点解析