运行时数据区
1.运行时数据区域包含哪些区域?哪些线程共享?哪些线程独享?哪些区域可能会出现OutOfMemoryError?哪些区域不会出现OutOfMemoryError?
JDK1.8之前:
JDK1.8:
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2.方法区和永久代的区别?
很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区。
3.栈中存放什么数据,堆中呢?
栈中一般存放方法和局部变量,Java中除了一些原生方法调用是通过本地方法栈实现的,其他所有Java方法调用都是通过栈实现的。
堆是所有线程共享的一块内存区域,堆的唯一目的是存放对象实例。
4.字符串常量池在什么位置?JDK1.7为什么要将字符串常量池移动到堆中?
- 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
- JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆
- 为什么?永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
6.Java 对象的创建过程?(默写👍)
- 1.类加载检查:虚拟机遇到一条 new 指令时,会检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 2.分配内存:虚拟机将为新生对象分配内存,内存大小在类加载完成后便可确定。
- 内存分配两种方式:
- 指针碰撞:适用于内存规整的情况,将用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可,使用该分配方式的 GC 收集器:Serial, ParNew
- 空闲列表:适用于内存不规整的情况,JVM会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录,使用该分配方式的 GC 收集器:CMS
- JVM在内存分配时采用下面两种方式保证线程安全:
- CAS+失败重试:CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。(解决并发问题)
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
- 3.初始化零值:这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用
- 4.设置对象头:对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。
- 5.执行 init 方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,[HTML_REMOVED] 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 [HTML_REMOVED] 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
7.对象的访问定位的两种方式知道吗?各有什么优缺点?
句柄:
直接指针:
- 对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。
- 句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
- 直接指针:如果使用直接指针访问,reference 中存储的直接就是对象的地址。
- 优劣?使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
8.堆空间的基本结构了解吗?什么情况下对象会进入老年代?
- Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。
- 在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
- 1.新生代内存(Young Generation)
- 2.老生代(Old Generation)
- 3.永久代(Permanent Generation)
- Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。
- JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存 。
- 大对象(需要大量连续内存空间的对象(比如:字符串、数组))直接进入老年代,这是是JVM的优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
- 长期存活的对象将进入老年代:对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),或者当累积的某个年龄大小超过了 survivor 区的 50% 时,就会被晋升到老年代中。
垃圾收集
9.如何判断对象是否死亡?(两种方法)
10.讲一下可达性分析算法的流程?哪些对象可以作为GC Roots呢?
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
1.引用计数法:
2.可达性分析算法:
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
哪些对象可以作为 GC Roots呢?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对
- 象所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
11.对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程。
12.JDK分别有几种引用类型?
- 1.强引用:以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题
- 2.软引用:如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
- 3.弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
- 4.虚引用:”虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
13.垃圾收集GC算法有哪些?各有什么特点?
- 1.标记-清除算法:分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
- 它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题:标记和清除两个过程效率都不高。
- 空间问题:标记清除后会产生大量不连续的内存碎片。
- 2.复制算法:可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
- 虽然改进了标记-清除算法,但依然存在下面这些问题:
- 可用内存变小:可用内存缩小为原来的一半。
- 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
- 3.标记-整理算法:根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
- 4.分代收集算法:当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
14.默认的垃圾收集器是哪一个?ZGC了解吗?👍
JDK 默认垃圾收集器:
JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
JDK 9 ~ JDK20: G1
ZGC是JDK11推出的低延迟垃圾回收器,主要是在低延迟场景中表现出色。
15.有哪些常见的GC?谈谈你对Minor GC、Full GC的理解。二者分别是在什么时候发生?Minor GC会发生stop the world现象吗?👍
- GC是垃圾回收机制,内存空间是有限的,你创建的每个对象和变量都会占据内存,GC做的就是对象清除将内存释放出来
- Minor GC(新生代GC):主要针对新生代(Young Generation)的垃圾回收,在这个阶段,主要清理的是新创建的对象。一般情况下,大部分对象的生命周期很短暂,所以Minor GC的频率通常比较高。当Eden区域满时,会触发Minor GC。在Minor GC中,首先会将存活的对象移动到Survivor区,如果Survivor区也满了,那么会将存活的对象移动到老年代。Minor GC通常是并发进行的,因此不会发生完全的”Stop the World”(停止一切线程)现象
- Full GC(老年代GC):主要针对老年代(Old Generation)的垃圾回收。Full GC会清理整个堆内存,包括新生代和老年代,Full GC通常发生在老年代空间不足、永久代满(如果使用永久代)等情况下,Full GC会导致全局的暂停,即”Stop the World”,这会使得应用程序的所有线程都暂停,直到垃圾回收完成为止。
16.讲一下CMS垃圾收集器的四个步骤?CMS有什么缺点?😢
- CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快
- 并发标记:同时开启 GC 和用户线程
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫
- 优点:并发收集、低停顿
- 缺点:
- 对 CPU 资源敏感
- 无法处理浮动垃圾
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生
17.并发标记要解决什么问题?并发标记带来了什么问题?如何解决并发扫描对象消失问题?
- 解决:因为像CMS和G1都有一个并发标记的过程,然后JVM一般采用的是可达性分析算法.可达性算法第一步需要进行根节点枚举,根节点枚举耗时一般不会随着堆里面的对象增加而增加的.但是第二步我们需要从GC Roots往下继续遍历对象图,进行”标记“过程。而这一步的停顿时间必然是随着java堆中的对象增加而增加的。”标记”阶段是所有使用可达性分析算法的垃圾回收器都有的阶段。所以并发标记的目的就是要消减这一部分的停顿时间。那就是让垃圾回收器和用户线程同时运行,并发工作.
- 带来的问题:并发标记除了会产生浮动垃圾,还会出现”对象消失”的问题。
- 解决并发扫描对象消失问题:
- 增量更新:
- 原始快照:
18.G1垃圾收集器的步骤?有什么缺点?
和CMS类似:初始标记,并发标记,最终标记,筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) .
缺点?
类
19.什么是字节码?类文件结构的组成了解吗?
- 在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机.
20.类的生命周期?类的加载过程了解吗?加载这一步主要做了什么事情?初始化阶段中哪几种情况必须对类初始化?👍
- 类的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
- 系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
- 加载做了:
- 1.通过全类名获取定义此类的二进制字节流。
- 2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 3.在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
- 初始化阶段以下6中情况必须对类进行初始化:
- 1.当遇到 new、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时
- 2.使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(“…”), newInstance() 等等。如果类没初始化,需要触发其初始化。
- 3.初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 4.当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
- 5.MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 findStaticVarHandle 来初始化要调用的类。
-
- 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
21.双亲委派模型了解吗?双亲委派模型的好处?如果我们不想使用双亲委派模型怎么办?(Tomcat如何打破双亲委托机制?)
- 了解吗?上图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。
- 每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
- 双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改.
- 打破:重写 loadClass()方法之后,我们就可以改变传统双亲委派模型的执行流程.例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。
- 我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。
22.JDK中有哪些默认的类加载器?
- JVM 中内置了三个重要的 ClassLoader:
- 1.BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
- 2.ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
- 3.AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。