java虚拟机JVM八股
前述:Ⅰ.⭐️代表面试高频,不要错过。Ⅱ.❌代表可不看。Ⅲ.没有符号标注即为常规基础
1.类加载机制(Java字节码加载过程)⭐️⭐️⭐️
类的加载就是把class文件加载到虚拟机中运行和使用。类加载的过程主要分为加载、验证、准备、解析、初始化五步,而验证、准备、解析又被合称为连接阶段。加载这一步是由类加载器完成,而具体是哪个类加载器加载又由双亲委派模型决定。类加载就是通过全类名获取定义此类的二进制字节流,将字节流所代码的静态存储结构转换为方法区的运行时数据结构,在内存中生成一个代表该类的class对象,作为方法区这些数据的访问入口。验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证可以分为四个阶段,分别是文件格式验证、元数据验证、字节码验证、符号引用验证。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。初始化阶段是执行初始化方法 <clinit> ()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
1.1双亲委派模式
⭐️⭐️
双亲委派模型即各种类加载器之间的层次关系,双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都要有自己的父类加载器。双亲委派模型的执行流程就是首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载,一个类加载器收到了类加载请求,它首先自己不会区尝试加载这个类,而是把这个请求委派给父类加载器区完成,所以最后的加载请求都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会去完成加载。好处:双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。
1.2打破双亲委派模型方法
⭐️
自定义加载器的话,需要继承 ClassLoader
,如果想打破双亲委派模型则需要重写 loadClass()
方法。 如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
1.3类加载器
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。BootstrapClassLoader
(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库以及被 -Xbootclasspath
参数指定的路径下的所有类。ExtensionClassLoader
(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext
目录下的 jar 包和类以及被 java.ext.dirs
系统变量所指定的路径下的所有类。AppClassLoader
(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
1.4如果我自己定义一个类,类名叫 String,这个类能生效吗?
⭐️
不能,Java类加载器在加载类时遵循委托机制,首先尝试从父类加载器加载所需的类。如果自定义了一个名为String的类,加载器会首先尝试从AppClassLoader加载,如果没有找到,会继续向上委托给ExtClassLoader和BootStrap加载器。由于BootStrap加载器在JRE/lib目录的rt.jar中找到了预定义的String类,因此自定义的String类不会被加载和使用
2.运⾏时内存分区(PC,Java虚拟机栈,本地⽅法栈,堆,⽅法区(永久代,元空间))⭐️⭐️
运行时内存分区主要就是线程私有的程序计数器、虚拟机栈和本地方法栈,公有的区域就是堆和方法区,其中方法区在jdk7即之前由永久代实现,在jdk7的时候把方法区的字符串常量池和静态变量移到了堆中,而jdk8之后将方法区剩下的部分移到了元空间中。
程序计数器是一块较小的内存,字节码解释器通过改变程序计数器来一次读取指令,以实现代码的流程控制,比如顺序执行、选择、循环等。在多线程环境下,程序计数器用于记录当前线程执行的位置,从而保证线程切换回来后能够知道线程上次运行到哪儿。并且程序计数器是唯一不会出现oom的区域。
虚拟机栈的生命周期和线程相同,每个方法从调用到执行完毕都对应着栈帧从入栈到出栈的过程。栈帧中拥有局部变量表、操作数栈、动态链接、方法返回地址等。动态链接的作用就是把符号引用转换为调用方法的直接引用。
本地方法栈和虚拟机栈的功能十分类似,区别就是本地方法栈为虚拟机使用本地方法服务,而虚拟机栈为虚拟机使用java方法服务。
堆是虚拟机管理的内存中最大的一块,是所有线程共享的,几乎所有的对象实例都在这里分配。java堆是垃圾收集的主要区域,根据分代垃圾收集算法,堆可以细分为新生代和老年代。
方法区存储已被虚拟机加载的类信息、字段、方法、常量、静态变量、即时编译器编译后的代码缓存等
2.1字符串常量池的作用
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
2.2为啥要把字符串常量池移到堆中?
因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
2.3为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
- 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,更容易出现内存溢出,而元空间使用的是本地内存,受本机可用内存的限制,虽然仍旧可能溢出,但是比原来出现的几率会更小。
- 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由
MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了 - 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
3.JMM:Java内存模型⭐️‼️
Java 内存模型定义了 Java 虚拟机如何与计算机内存进行交互,以及多线程之间如何进行数据同步和通信的规范。JMM 主要解决了在多线程编程中可能出现的可见性、原子性和有序性等问题。JMM 确保了在多线程环境下对共享变量的安全访问。通过合理地使用 volatile、synchronized 和 Locks,开发者能够编写出正确且高效的多线程程序。
- 可见性(Visibility):指一个线程修改的变量对其他线程是否可见。JMM 通过使用 volatile 关键字、synchronized 关键字、以及 Locks 等同步机制来确保变量的可见性。
- 原子性(Atomicity):指操作是不可分割的,要么全部执行成功,要么全部不执行。JMM 通过 synchronized 关键字、Locks、以及原子类(如 AtomicInteger)来保证特定操作的原子性。
- 有序性(Ordering):指程序执行的顺序必须按照代码的先后顺序执行,且线程间可能出现的指令重排序。JMM 通过 volatile 关键字、synchronized 关键字、以及 Locks 来保证程序的有序性。
3.1如果没有 Java 内存模型就会出现以下两大问题:
‼️
- CPU 和 内存一致性问题。
- 指令重排序问题。
4.垃圾对象的识别(引⽤计数、可达性分析)⭐️
引用计数和可达性分析是定义对象是否已经死亡的两个算法,引用计数为维护一个计数器,当有引用指向对象时,计数器加一,当引用消亡时,计数器减一。如果引用计数器的值为0,就说明对象可以回收。但是引用计数器存在循环引用导致死循环的问题。所以我们都是使用的可达性分析法,可达性分析就是指对象到GC ROOT之间是否有引用链相连,如果没有就说明此对象不可用,需要回收。
4.1引用计数法的优点
1、高效 2、运行期没有停顿 3、对象有确定的生命周期 4、易于实现
4.2可以作为gc roots的对象
⭐️
有虚拟机栈中引用的对象,本地方法栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,被同步锁持有的对象,jni引用的对象。
4.3三色标记法
(1)是什么?
根据对象是否被垃圾收集器扫描过而用白、灰、黑三种颜色来标记对象的状态。
白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始阶段,所有的对象都是白色的,若在分析结束之后对象仍然为白色,则表示这些对象为不可达对象,对这些对象进行回收。灰色:表示对象已经被垃圾收集器访问过,但是这个对象至少存在一个引用(属性)还没有被扫描过。黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经被扫描过。黑色表示这个对象扫描之后依然存活,是可达性对象,如果有其他对象引用指向了黑色对象,无须重新扫描,黑色对象不可能不经过灰色对象直接指向某个白色对象。
(2)三色标记的过程
初始状态
初始阶段只有GC Roots是黑色的,其他对象都是白色的,如果没有被黑色对象引用那么最终都会被当做垃圾对象回收。
开始扫描
A和B均为扫描过的对象并且其引用也已经被垃圾回收器扫描过所以此时A、B对象均变为了黑色,而刚扫描到对象C,由于C的D和E还没有被扫描到,所以C暂时为灰色。
顺利扫描结束
此时扫描完成,黑色对象就是存活的对象,即可可达对象,白色对象G为不可达对象,在垃圾回收时就会被回收掉。
(3)三色标记的缺点
CMS和G1等垃圾回收器是一个并发回收的垃圾回收器,在并发的情况下会存在多标和漏标的问题。
(4)解决办法
- 增量更新 当黑色对象插入新的指向白色对象的引用关系时。就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象作为根对象,再重新扫描一遍
- 原始快照 当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后再将这些记录过的引用关系中的灰色对象为根对象再重新扫描一遍。
5.垃圾回收的触发条件⭐️
- 堆内存不足
- System.gc()显示调用,但并不保证jvm会立即执行垃圾回收
- 某些垃圾回收器会根据预设条件自动触发垃圾回收,比如堆内存使用量,垃圾对象占比等
5.垃圾回收算法:标记-清除,标记-整理,复制⭐️⭐️⭐️
标记清除算法分为标记和清除两个阶段,首先标记阶段会标记出不需要回收的对象,在清除阶段会统一回收掉没有标记的对象。它的标记和清除阶段效率都不高,并且会产生大量的内存碎片。
复制算法将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后把使用的这块空间清理掉。虽然改进了标记清除算法,但是使得可用内存缩小为一半,并且不适合老年代这种存活率比较高的场景。
标记整理算法分为标记和整理两个阶段,标记过程和标记清除算法一样,但整理阶段将所有存活的对象向一端移动,然后直接清理掉边界以外的内存。
6.垃圾回收器:⽐较,区别(Serial,ParNew,Parallel Scavenge ,CMS,G1)Stop The World⭐️
Serial(串行)收集器是一个单线程收集器。它只会使用一条垃圾收集线程去收集垃圾,并且在它进行垃圾收集的时候必须暂停其他所有的工作线程(stop the world)。新生代使用复制算法,老年代使用标记整理算法。它的优点就是简单高效,但由于stop the world,停顿时间会很长。适合运行在client模式下的虚拟机
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和serial收集器完全一样。适合运行在server模式下的虚拟机。
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU),所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。新生代采用复制算法,老年代采用标记-整理算法。
cms是第一款真正意义上的并发收集器,它实现了让垃圾收集线程和用户线程同时工作,是一种以获取最短回收停顿时间为目标的收集器。顾名思义,他是基于标记清除算法的,它的运作过程分为四部,分别为初始标记、并发标记、重新标记、并发清除。他的优点就是并发收集、低停顿。他的缺点就有内存碎片,对cpu资源敏感等。
g1收集器面向局部收集的设计思想和基于region的内存布局形式。它也遵循分代收集理论,不过它把堆内存划分为多个大小相等的独立区域region,每个区域可以根据需要扮演新生代的eden空间,survivor空间,或者老年代空间。并且针对扮演不同角色的区域采用不同的垃圾收集策略。除此之外,还建立了可预测的停顿时间模型,处理思路就是去跟踪各个区域里面的垃圾堆积的价值,在后台维护一个优先级列表,每次根据用户设定的收集停顿时间,优先处理回收价值收益大的那些区域。运作过程大致分为初始标记、并发标记、最终标记、筛选回收。
ZGC (Z Garbage Collector)收集器,超低延迟垃圾回收器。工作方式:使用并发标记和重定位技术,暂停时间通常在 10 毫秒以内。适用于需要极低延迟的应用,如金融交易系统。缺点:目前支持的 JVM 版本和平台有限。
7.强、软、弱、虚引⽤
强引用:一个对象具有强引用,那就是必不可少的,垃圾回收器绝不会回收他。当内存空间不足时,java虚拟机宁愿抛出oom错误,使程序异常终止,也不会靠随意回收强引用的对象来解决内存不足的问题。
软引用:一个对象只具有软引用,那就是可有可无的。内存空间足够时,垃圾回收器不会回收他,如果内存空间不足,就会回收这些对象的内存。
弱引用:一个对象只具有弱引用,那也是可有可无的。弱引用和软引用的区别在于,只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它的区域时,一旦发现了只具有弱引用的对象,则不管当前内存空间是否足够,都会回收。
虚引用:顾名思义,就是形同虚设,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
8.内存溢出、内存泄漏排查⭐️
oom就是内存溢出,通常是程序试图分配的内存超出了JVM的可用内存限制而引起的。可能的原因就是请求创建了一个超大对象,通常是一个大数组;超出预期的访问量,比如说常见的促销和秒杀活动;过度使用终结器,导致对象没有被及时gc;内存泄漏,大量对象引用没有释放,JVM 无法对其自动回收;那么排查的话,我们可以仔细审查代码,查找可能导致内存泄漏或不必要内存消耗的地方。特别关注缓存、集合、大对象的创建和使用;查看日志和堆栈跟踪,来确定在哪个部分的代码中导致了内存问题;我们可以查看堆转储文件,使用专门的内存分析工具(如 MAT)来分析堆转储文件,以确定哪些对象占用了大量内存,找出内存泄漏或者过度分配内存的情况。
8.1平时有没有遇到过,怎么做的?
- 现象:网络没有问题的情况下,系统某开放接口从 2023 年 3 月 10 日 14 时许开始无法访问和使用。 原因:-Xmn参数设置成与-Xmx参数一样,堆区被 Young Gen 完全挤占,又有对象想要升代到 Old Gen 时,发现 Old 区空间不足,于是触发 Full GC,触发 Full GC,Old 区空间仍然不够,卒,喜提OOM解决方案:正常情况下,-Xmn参数(控制 Young 区的大小)总是应当小于-Xmx参数(控制堆内存的最大大小),否则就会触发 OOM 错误。
8.2怎么排查的?
首先查看系统日志目录下的app.dump文件,在日志中搜索,找到了若干处内存溢出错误OOM,但是每次出现
OOM错误的位置居然都不一样,问题变得有点复杂。然后用*MAT(Memory Analyzer Tool)*工具打开转储文件,直方图中显示活跃对象居然只有100多M,所以也不是某个类型对象占用大量的内存。然后开始检查机器的内存,根据运维的说法,机器内存为16GB,
top命令查看
java`进程占用内存约为7.8GB,看起来似乎没毛病。
最后发现是应用启停脚本被修改了,旧版本是-Xms8g -Xmx8g -Xmn3g,新版本改为-Xms8g -Xmx8g -Xmn8g。
9.JVM调优,常⽤命令
jvm调优的话,可以通过观察 GC 频率和停顿时间,来进行 JVM 内存空间调整,使其达到最合理的状态。jvm内存空间的调整主要就是三个参数-Xmx,-Xms,-Xmn。在进行内存空间调整的时候,为了避免内存剧烈波动导致的问题,一般我们先调整一点试一试,没太大问题之后再调整到目标值。
具体:GC 频率是很低的。这时候很可能是分配了较大的新生代空间,如果停顿时间也很短的话,那我们就可以判定该应用的内存有优化的空间。一般做法就是缩小分配的新生代的空间。
10.jvm参数
(1)指定堆的内存
-Xms //初始化堆的最小内存-Xmx //初始化堆的最大内存
(2)指定新生代内存
方法一:
-XX:NewSize //新生代最小内存
-XX:MaxNewSize //新生代最大内存
方法二:
-Xmn //指定新生代大小
(3)指定新生代和老年代的比值
-XX:NewRatio
(4)指定永久代的大小
-XX:PermSize=N #方法区 (永久代) 初始大小-XX:MaxPermSize=N #方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError
(5)指定元空间的大小
-XX:MetaspaceSize=N #使用过程中触发 Full GC 的阈值-XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小
注意:无论 -XX:MetaspaceSize
配置什么值,对于 64 位 JVM 来说,Metaspace 的初始容量都是 21807104(约20.8m)
(6)指定垃圾回收器
-XX:+UseSerialGC-XX:+UseParallelGC-XX:+UseParNewGC-XX:+UseG1GC
(7)gc日志记录
#打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
#打印对象分布
-XX:+PrintTenuringDistribution
#打印堆数据
-XX:+PrintHeapAtGC
#打印Reference处理信息
#强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
(8)处理oom
-XX:+HeapDumpOnOutOfMemoryError #指示 JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中。-XX:HeapDumpPath=路径 #表示要写入文件的路径-XX:OnOutOfMemoryError="< cmd args >;< cmd args >" #用于发出紧急命令,以便在内存不足的情况下执行
11.String常量池?
JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
12.哪些区域会发生full gc?
新生代+老年代+永久代
13.MinorGC和full GC的触发条件?
对于Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。
Full GC:
(1)调用 System.gc()建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行
(2)当创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC
(3)通过Minor GC后进入老年代的平均大小大于老年代的可用内存时,也会触发Full GC
(4)当永久代当中没有足够的空间,就会触发一次Full GC
(5)在新生代回收内存时,由Eden区和Survivor From区把存活的对象向Survivor To区复制时,Survivor To空间的可用内存放不下,则把这些对象转存到老年代(这个过程称为分配担保),且老年代无法存放下新生代过度到老年代的对象的时候,便会触发Full GC。
14.方法区在什么情况下会发生OOM?
方法区主要用于存放加载的类信息、字段、方法、常量、静态变量以及即时编译器编译后的代码缓存。并且类被回收的判定条件比较苛刻(所有的类实例都被回收,类加载器被回收,Class对象没有被引用)。如果在运行时产生了大量的类填满了方法区就会导致溢出。比如说使用GCLib 字节码增强了太多类,大量 JSP 的应用。
15.为什么说full GC效率比较低?
(1)fullgc回收的范围大,需要回收新生代、老年代以及方法区。
(2)方法区的回收效率低,因为类被回收的判定条件比较苛刻
(3)fullgc通常使用标记清除算法,标记阶段和清除阶段比较费时
(4)fullgc时需要stop the world,阻塞其他的工作线程
16.full gc频繁,有哪些原因?⭐️
- 堆内存设置不合理 堆内存过小新生代和老年代比例不当 新生代设置过小会导致大量对象快速晋升到老年代,增加full gc的频率新生代设置过大有可能导致老年代过小,容易引发full gc
- 内存泄露 程序中的某些对象不再使用时没有被gc回收,导致堆内存被无用对象填满,触发full gc
- 对象创建过多 短生命周期对象过多,导致堆内存填满,触发gc大对象过多,直接分配在老年代,将其填满,触发full gc
- 垃圾回收器选择不当 GC算法不适合当前的应用场景
- 显示调用System.gc()
- 元空间不足 应用程序加载了大量类和方法
16.如何排查Java 程序是否死锁⭐️
- 可以使用
jstack
工具,它是 jdk 自带的线程堆栈分析工具。比如说现在程序中存在死锁,那么先用ps -ef | grep xxx命令获取进程id,然后使用jstack -l <pid>打印出额外的锁信息 - 采用jconsole工具,连接对应的程序,然后进入线程界面选择检测死锁
17.类文件的结构
主要包括魔数;Class 文件的版本号;常量池;访问标志;当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合;字段表集合;方法表集合;属性表集合
19.CPU占用过高问题的排查及解决⭐️
(1)使用top 定位到占用CPU高的进程PID
(2)top -Hp 进程id,查看Java进程里面的线程的占用情况 ,记下cpu占用过高的线程的id
(3)通过jstack命令获取占用资源异常的线程栈,可暂时保存到一个文件中查看(jstack 31357 > jstack.31357.log),如果想看到关于线程中的锁的附加信息,可以加一个-l参数息
20.堆内存这么组织的?⭐️
堆内存分为新时代、老年代、元空间(8以后替代了永久代)。新生代分为eden区和两个survivor区,默认是8:1:1。老年代存放的是在新生代中多次GC仍然存活的对象,或者对象体积较大,直接进入老年代;老年代占据内存的大部分空间,新生代和老年代的比例为1:2。元空间用于存储类的元数据,元空间使用的是本地内存
21.jvm的构成⭐️
类加载子系统,运行时数据区,执行器,本地接口
22.Java 创建对象的过程,JVM怎么保证内存分配并发安全的⭐️
22.1创建对象的过程
⭐️
(1)类加载检查:JVM 首先检查这个类是否已经被加载、解析和初始化。如果没有,那么必须先执行相应的类加载过程。
(2)分配内存:为新对象分配内存。内存的分配方式取决于垃圾收集器是否采用分代收集以及所使用的具体类型,但主要有两种方式:
- 指针碰撞(Bump the Pointer):如果内存是绝对规整的,那么只需移动指针,将之前是空闲的内存分配给对象即可。
- 空闲列表(Free List):如果内存不是规整的,JVM 需要通过维护一个列表来记录可用的内存,分配时从列表中找到足够大的空间分配给对象。
(3)初始化零值:将分配到的内存空间都初始化为零值(不包括对象头),这一步保证了对象的实例字段在Java代码中可以不赋初值就直接使用。
(4)设置对象头:JVM 对象头包含了对象是哪个类的实例、对象的哈希码、对象的GC分代年龄等信息。这一步将这些信息写入对象头。
(5)执行<init>
方法:执行构造函数,初始化对象。
22.2保证内存分配的并发安全
- TLAB(Thread Local Allocation Buffer):这是一种避免共享的策略。每个线程预先在Java堆中分配一小块内存,称为本地线程分配缓冲区(TLAB)。线程需要分配内存时,首先在自己的TLAB上分配,只有TLAB用尽并重新分配时,才需要同步锁定。这样大大减少了锁的竞争。
- CAS(Compare And Swap)+ 失败重试:在没有使用TLAB或者TLAB用尽需要在共享空间分配内存时,JVM采用CAS操作(比较并交换)来保证更新操作的原子性。如果CAS操作失败(说明有其他线程也在分配内存),则进行失败重试,直到成功为止。
22.3怎么做到多线程同时去分配和回收,效率特别高的?
⭐️
在多线程环境下,为了提高内存分配效率,JVM 还会使用一些线程本地内存分配(Thread-local allocation buffers, TLAB)来减少锁竞争。单个线程内的小对象内存分配可直接在 TLAB 中进行,避免了同步开销。当 TLAB 使用完后,再从全局堆内存申请更多的内存。
22.4jvm怎么向系统申请内存
⭐️
- 根据 -Xms 参数,JVM 会在启动时向操作系统申请分配初始堆内存。通过 mmap 或 malloc 来分配内存。
- 根据 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数,JVM 会为方法区(元空间)保留空间。同样使用操作系统的内存分配 API。
- 每当创建新的线程,JVM 会为其分配栈空间,大小由 -Xss 参数决定。
23.如果频繁的发生young gc,用什么工具或命令来查看?⭐️
使用jstat -gc pid来查看,其中YGC参数就是young gc的次数。进一步的可以使用jstack来分析堆栈信息
24.内存泄漏的根本原因⭐️
内存泄漏的最根本原因通常是因为程序持有对不再需要的对象的引用,导致垃圾回收器无法回收这些对象。
25.java中打开文件需要手动关掉吗?如果你不关掉的话,java的内存回收会回收掉吗??⭐️
在 Java 中,打开文件或其他 I/O 资源时,通常需要手动关闭这些资源。这是因为这些资源是通过操作系统管理的,操作系统为每个打开的文件分配内存和其他资源。Java 的垃圾回收机制主要负责回收不再被引用的对象所占用的内存,但它不直接管理诸如文件句柄、数据库连接、网络连接等底层资源。
26.方法区中的方法的执行过程?‼️
解析方法调用:JVM会根据方法的符号引用找到实际的方法地址
栈帧创建:在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧
执行方法:执行方法内的字节码指令
返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境。
27.有具体的内存泄漏和内存溢出的例子么请举例及解决方案?‼️
1、静态属性导致内存泄露
- 解决:第一,尽量减少静态变量;第二,如果使用单例,尽量采用懒加载。
2、 未关闭的资源,比如,数据库链接、输入流和session对象。
- 解决:第一,始终记得在finally中进行资源的关闭;第二,关闭连接的自身代码不能发生异常;第三,Java7以上版本可使用try-with-resources代码方式进行资源关闭。
3.使用ThreadLocal
- 解决:在finally块中调用threadLocal.remove()来关闭threadLocal
博主博主,网上八股那么多,不知道看哪个怎么办,有没有什么重点的八股拿来学习一下的? 有的,兄弟有的! 会陆续发布一些本牛在实习和秋招过程中总结的八股。