面试:JVM问题

49 篇文章 3 订阅
8 篇文章 0 订阅

文章目录

参考:《面渣逆袭:JVM经典五十问,这下面试稳了

基础概念

什么是JVM

JVM——Java虚拟机,它是Java实现平台无关性的基石。
JVM跨平台:Java程序运行的时候,编译器将Java文件编译成平台无关的Java字节码文件(.class),接下来对应平台JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。
Java跨语言:同时JVM也是一个跨语言的平台,和语言无关,只和class的文件格式关联,任何语言,只要能翻译成符合规范的字节码文件,都能被JVM运行。包括scala、groovy、kotlin等。

jvm运行时区域划分及每个区域的作用

堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器在这里插入图片描述
两个数据共享区域:堆、方法区。三个数据私有区域:java虚拟机栈、本地方法栈、程序计数器。
Java堆(heap):主要存放对象实例和数组,Java堆是垃圾回收的主要区域。从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
方法区:即我们常说的永久代(Permanent Generation),是各个线程共享的内存区域, 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虚拟机栈:是线程私有的,它描述 java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈:线程私有,与虚拟机栈的作用类似,区别是虚拟机栈为执行 Java 方法服务,而本地方法栈则为 Native 方法服务。
程序计数器:线程私有,它是一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。如果执行的是Java方法,这里存放的是指令地址,如果执行的是Native方法,这里的值为空。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

jvm类加载过程

在这里插入图片描述

①加载阶段
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。
②验证阶段
1.文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)
2.元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)
3.字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)
4.符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)
③准备阶段
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值
④解析阶段
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。
字符串常量池:堆上,默认class文件的静态常量池
v运行时常量池:在方法区,属于元空间
⑤初始化阶段
初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。

什么是动态链接?什么时候符号引用会替换成直接引用?

动态链接:每个栈帧都保存了一个可以指向当前方法所在类的运行时常量池, 目的是当方法中需要调用其它方法的时候能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用然后就能直接调用对应的方法,这就是动态链接,不是所有的方法调用都需要进行动态链接的。子类方法重写就是通过动态链接实现的。
有一部分的符号引用会在类加载的解析阶段将符号引用转换为直接引用,这部分操作称之为静态解析。
静态解析:类加载的解析阶段会将部分的符号引用解析为直接引用,这部分的符号引用指的是编译期间就能确定调用的版本,主要包括2大类:

  1. invokestatic: 调用静态方法
  2. invokespecial: 调用实列构造器<init>私有方法,私有方法,父类方法

因为这2类不允许被重写修改, 符合"编译器可知,运行期不可变"的准则,把这类方法称为非虚方法。除去静态解析能在类加载的解析阶段将符号引用解析为直接引用,剩下的符号引用就要在运行期间进行解析。

双亲委派机制

每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
为什么使用双亲委派机制?:此机制保证JDK核心类的优先加载;使得Java程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API不被篡改。如果不用没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的Object 类。
在这里插入图片描述

Jvm垃圾回收:存活算法

判断一个对象是否存活有两种方式:引用计数算法(reference counting)和可达性分析算法。
引用计数法
给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率也很高
缺点:他很难解决对象之间相互循环引用的问题,基本上被抛弃
可达性分析法
目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(Gc Root Set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。
两次标记过程
对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就不会回收。

Jvm垃圾回收:回收算法

垃圾回收算法主要分为以下三种:标记-复制、标记-清除、标记-整理
复制算法:(young)
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收;
优点:实现简单,内存效率高,不易产生碎片
缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低
标记清除:(cms)
标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
缺点:效率低,标记清除后会产生大量不连续的碎片,需要预留空间给分配阶段的浮动垃圾
标记整理:(old)
标记过程仍然与“标记-清除”算法一样,再让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存;解
决了产生大量不连续碎片问题
分代收集
根据各个年代的特点选择合适的垃圾收集算法。
新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

Jvm垃圾回收:垃圾收集器

Serial
Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集
的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。适合用于客户端垃圾收集器。
Parnew
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余
的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
JDK5:parallel Scavenge+(Serial old/parallel old)关注吞吐量
parallel Scavenge:(关注吞吐量)
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
Serial old
Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。主要有两个用途:在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
作为年老代中使用 CMS 收集器的后备垃圾收集方案。
parallel old
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。
JDK8-CMS:(关注最短垃圾回收停顿时间)
CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,STW。
并发标记:进行 ReferenceChains跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,STW。
并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
优点:并发收集、低停顿
缺点:对CPU资源敏感;无法处理浮动垃圾;使用“标记清除”算法,会导致大量空间碎片产生。
JDK9-G1:(精准控制停顿时间,避免垃圾碎片
是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器.以极高概率满足GC停顿时间要求的
同时,还具备高吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是:
【1】基于标记-整理算法,不产生内存碎片。
【2】可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
初始标记:Stop The World,仅使用一条初始标记线程对GC Roots关联的对象进行标记
并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢
最终标记:Stop The World,使用多条标记线程并发执行
筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行
JDK11-ZGC:(在不关注容量的情况获取最小停顿时间5TB/10ms)着色笔技术:加快标记过程
读屏障:解决GC和应用之间并发导致的STW问题
• 支持 TB 级堆内存(最大 4T, JDK13 最大16TB)
• 最大 GC 停顿 10ms
• 对吞吐量影响最大,不超过 15%

Java内存模型(JMM)

java内存模型是一种规范,指多线程共享内存时,每一个线程会从主内存中独立copy一份私有内存出来进行读写。
好处:它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

进阶问题

说一下JDK1.6、1.7、1.8内存区域的变化?

JDK1.6、1.7/1.8内存区域发生了变化,主要体现在方法区的实现:

  • JDK1.6使用永久代实现方法区;
  • JDK1.7时发生了一些变化,将字符串常量池、静态变量,存放在堆上;
  • 在JDK1.8时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。

为什么使用元空间替代永久代作为方法区的实现?

虚拟机使用元空间替代了永久代是因为永久代在过去的实现中存在一些问题和限制,而元空间提供了更好的性能和灵活性。以下是一些详细的原因:

  1. 内存管理:永久代的内存管理是由虚拟机自身控制的,无法根据应用程序的需求进行动态调整。而元空间使用本地内存进行管理,可以根据应用程序的需求动态分配和释放内存,提高内存的利用率。
  2. 永久代内存溢出:在永久代中,存储类的元数据、常量池、静态变量等,当应用程序加载大量类或者使用大量字符串常量时,可能导致永久代内存溢出。而元空间不再有固定的大小限制,可以根据应用程序的需要自动扩展。
  3. 类的卸载:在永久代中,由于类的卸载机制比较复杂,很难实现完全的类卸载。而元空间使用本地内存,可以更容易地实现类的卸载,减少内存的占用。
  4. 性能优化:元空间的实现采用了更高效的数据结构和算法,例如使用指针碰撞(Bump the Pointer)的方式分配内存,减少内存碎片化,提高内存分配的效率。此外,元空间还支持并发的类加载和卸载操作,提高了性能。

总的来说,元空间相对于永久代来说具有更好的内存管理、更高的性能和更灵活的特性,能够更好地满足现代应用程序的需求。因此,虚拟机选择使用元空间替代永久代。

堆内存分配策略:新生代,老年代

  • 对象优先分配在Eden区,如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。而那些无需回收的存活对象,将会进到 Survivor 的 From 区(From 区内存不足时,直接进入 Old 区)。
  • 大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。
  • 动态对象年龄判定:为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到- XX:MaxTenuringThreshold=15才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
  • 空间分配担保:假如在Young GC之后,新生代仍然有大量对象存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。

JVM 内存为什么要分新生代,老年代,元空间

JVM之所以将内存划分为新生代、老年代和元空间,是为了实现更有效的垃圾回收和提高Java应用程序的性能。这种内存分代的策略基于以下考虑:

  1. 对象生命周期不同
  • 大多数对象在被创建后不久就会变得不可达,因此它们的生命周期很短。
  • 但也有一些对象具有较长的生命周期,它们可能在应用程序的整个生命周期内存在。
  1. 不同的垃圾回收算法
  • 针对不同生命周期的对象,JVM可以使用不同的垃圾回收算法。
  • 新生代通常使用复制算法,因为大多数对象很快就会变得不可达。这个算法可以快速回收不再使用的对象。
  • 老年代使用标记-清除或标记-整理算法,因为较长生命周期的对象不适合复制算法,需要更复杂的回收策略。
  1. 性能优化
  • 分代内存管理有助于提高垃圾回收的性能。由于新生代的对象生命周期短暂,因此垃圾回收发生在新生代的频率较高,但每次回收的内存量较小。
  • 老年代的垃圾回收发生频率较低,但每次回收的内存量较大。这减少了垃圾回收的停顿时间,提高了应用程序的响应性能。
  1. 内存碎片问题
  • 通过将内存分为新生代和老年代,可以减少内存碎片问题。在新生代中使用复制算法,内存会被分为较小的块,这有助于减少碎片。
  • 老年代使用标记-清除或标记-整理算法来处理较长生命周期的对象,进一步减少了碎片。
  1. 元数据管理(持久代或元空间)
  • 元数据主要存储在元空间中,将元数据信息单独管理,可以更好地控制和管理类加载和卸载,防止类加载器泄漏和元数据溢出等问题。

总之,分代内存管理是一种有效的策略,可以提高Java应用程序的性能和稳定性,通过根据对象的生命周期和不同的垃圾回收算法来合理管理内存,从而减少垃圾回收的成本和停顿时间,同时降低内存碎片问题。这有助于使Java应用程序更高效地运行。

设置堆内存XMX应该考虑哪些因素

设置Java堆内存大小(-Xmx参数)是一个重要的性能调优决策,需要考虑多个因素,以确保应用程序在合适的内存限制下运行顺畅,避免内存不足或内存浪费的问题。以下是考虑设置-Xmx参数时需要考虑的因素:

  • 应用程序的内存需求:首先要了解应用程序的内存需求。这包括应用程序的数据量、并发用户数、对象创建频率等。不同的应用程序可能需要不同大小的堆内存。
  • 应用程序的性能需求:性能目标对内存大小有很大的影响。如果需要更高的吞吐量和更低的延迟,可能需要分配更多的内存。但要小心不要分配过多,以避免浪费内存。
  • 可用物理内存:要考虑服务器或计算机上的可用物理内存量。将-Xmx参数设置为超过物理内存容量的值可能会导致操作系统频繁地进行内存交换,降低性能。
  • 垃圾回收的开销:堆内存越大,垃圾回收的开销通常也会增加。大堆内存可能需要更长的垃圾回收暂停时间。因此,要权衡内存大小和垃圾回收开销。
  • 堆内存分代结构:Java堆内存通常分为年轻代、老年代和永久代(或元空间,取决于JVM版本)。不同代的分配比例和大小会影响-Xmx的设置。根据应用程序的特性,可以考虑调整不同代的大小。
  • 监控和调整:监控应用程序的内存使用情况,使用工具如JVisualVM、JConsole等来观察堆内存的使用情况。根据监控数据进行动态调整-Xmx参数。
  • 应用程序设计:合理的应用程序设计也可以影响堆内存需求。避免内存泄漏和不必要的对象创建可以降低内存需求。
  • 并发性需求:多线程应用程序通常需要更多的堆内存,因为每个线程都需要一定的内存空间来存储栈帧和局部变量。
  • JVM版本和垃圾回收器:不同的JVM版本和垃圾回收器可能对内存需求有不同的影响。某些垃圾回收器可能更适合大堆内存,而某些适用于小堆内存。

综合考虑这些因素,需要进行实际的性能测试和监控来确定合适的-Xmx参数值。通常建议开始时设置一个合理的初始值,然后通过性能测试和监控来逐渐调整,以满足应用程序的需求并避免不必要的内存浪费。不同的应用程序可能需要不同的内存配置,因此没有一种大小适合所有情况的通用规则。

创建一个对象的步骤

类加载检查 —> 分配内存 —> 初始化零值 —> 设置对象头 —> 执行init方法.
①类加载检查:
虚拟机遇到 new 指令时,首先去检查是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
②分配内存:
在类加载检查通过后,接下来虚拟机将为新生对象分配内存,分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
③初始化零值:
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。具体的零值取决于变量的数据类型。例如,整数类型会初始化为0,布尔类型会初始化为false,对象引用会初始化为null。
④设置对象头:
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头的内存划分也会不一样。
⑤执行 init 方法:
从虚拟机的视⻆来看,一个新的对象已经产生了,但从Java 程序的视⻆来看, 初始化方法还没有执行,所有的字段都还为零。到这一步初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

对象内存空间分配方式

分配内存的方式有两种,分别是指针碰撞、空闲列表。这个取决于我们使用的垃圾回收算法是“标记整理”还是“标记清除”,也就是可分配内存的空间是否规整。
指针碰撞:假设Java堆中内存是绝对规整的,所有使用过的内存都放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲方向挪动一段与对象大小相等的距离。
空闲列表:如果Java堆内存是不规整的,已经被使用的内存与空闲的内存交错在一起,无法进行指针碰撞,虚拟机会会维护一个列表,记录哪块内存可用,哪块内存不可用,分配的时候找到一块足够大的空间划分给对象实例,并更新列表。

内存分配时怎么处理并发分配

  • CAS:对分配内存空间的操作进行同步处理。实际上虚拟机是采用CAS+失败重试的方式保证更新操作的原子性。
  • TLAB:使用本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。为每个线程预先在堆内存中分配一小块内存,也就是TLAB,哪个线程需要分配内存就在哪个线程的TLAB中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,这个时候可以配合CAS进行同步处理。

什么是TLAB

TLAB是Java虚拟机中的一种优化技术,用于提高对象分配的效率。它是一种线程本地的内存分配缓冲区,每个线程在堆上都有自己独立的TLAB。
在Java中,对象的分配通常是通过在堆上分配内存来完成的。为了提高对象分配的效率,Java虚拟机引入了TLAB机制。TLAB会为每个线程预先分配一块内存空间,线程在分配对象时,会从自己的TLAB中进行分配,而不是直接在堆上进行分配。
TLAB的优点有以下几个:

  1. 减少线程同步:由于每个线程都有自己的TLAB,因此不需要进行线程同步操作,可以减少线程竞争和锁的开销。
  2. 提高分配速度:由于对象分配是在TLAB中进行的,而不是在堆上进行,因此可以减少对象分配的开销,提高分配速度。
  3. 提高局部性:由于每个线程都有自己的TLAB,对象分配在TLAB中进行,可以提高局部性,减少对共享内存的访问,提高缓存命中率。

需要注意的是,TLAB的大小是可以配置的,可以根据实际应用的需求进行调整。过小的TLAB可能会导致频繁的垃圾回收,而过大的TLAB可能会浪费内存空间。
总结来说,TLAB是Java虚拟机中的一种优化技术,用于提高对象分配的效率。每个线程都有自己的TLAB,对象分配在TLAB中进行,可以减少线程同步、提高分配速度和提高局部性。TLAB的大小可以配置,需要根据实际需求进行调整。

对象的组成

对象头 + 实例数据 + 对齐填充字节
对象头:对象头主要由两部分组成:

  • 第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为Mark Word,它是个动态的结构,随着对象状态变化。
  • 第二部分是类型指针,指向对象的类元数据类型(即对象代表哪个类)。
  • 此外,如果对象是一个Java数组,那还应该有一块用于记录数组长度的数据

实例数据:对象的实例数据就是在java代码中能看到的属性和他们的值。
对齐填充字节:因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
在这里插入图片描述

对象的大小如何计算

在Java中,对象的大小通常是由对象的实例变量、对象头和内部填充组成的。对象的大小计算方法可以简化为以下几个步骤:

  1. 计算对象头大小:对象头包含了一些关于对象的元信息,如对象的哈希码、锁状态、垃圾回收信息等。对象头的大小在不同的JVM实现和配置下会有所不同。通常,对象头的大小在64位JVM是8个字节,在32位JVM是4个字节。
  2. 计算实例变量大小:对象的实例变量是对象的数据部分,它们占用的内存空间由它们的数据类型和数量决定。例如,一个整数类型的实例变量在64位JVM是8个字节,在32位JVM是4个字节。
  3. 计算内部填充大小:为了对齐数据,Java虚拟机通常在实例变量之间插入一些内部填充。填充的大小取决于虚拟机和操作系统的要求,通常是8字节的倍数。这样可以提高内存的访问效率。

综上,对象总大小等于对象头大小加上实例变量大小和内部填充的大小。

对象怎么访问定位?

主流的访问方式主要有使用句柄和直接指针两种:

  • 句柄:如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图所示:
    在这里插入图片描述
  • 指针:如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图所示:
    在这里插入图片描述

区别:这两种对象访问方式各有优势,HotSpot虚拟机主要使用直接指针来进行对象访问

  • 使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
    使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。

对象一定分配在堆中吗?有没有了解逃逸分析技术?

对象一定分配在堆中吗? 不一定的。对象还可以分配在栈上,对象栈上分配通常是指将对象引用分配到方法调用栈上,而不是在堆内存中分配对象的实例数据。这种分配方式主要涉及基于逃逸分析的优化技术。
随着JIT编译期的发展与逃逸分析技术逐渐成熟,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。其实,在编译期间,JIT会对代码做很多优化。其中有一部分优化的目的就是减少内存堆分配压力,其中一种重要的技术叫做逃逸分析
什么是逃逸分析?

逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。
通俗点讲,当一个对象被new出来之后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸。比如将对象作为方法返回值直接返回。
除此之外,如果对象还有可能被外部线程访问到,例如赋值给可以在其它线程中访问的实例变量,这种就被称为线程逃逸。比如全局变量。

逃逸分析的好处

  • 栈上分配
    如果确定一个对象不会逃逸到线程之外,那么久可以考虑将这个对象在栈上分配,对象占用的内存随着栈帧出栈而销毁,这样做的好处是可以显著提高对象的访问速度,因为栈上的对象引用可以更快地访问,而且不需要垃圾回收。但也需要注意以下几点:
  • 对象的生命周期有限:对象栈上分配的对象的生命周期通常限制在方法调用期间。一旦方法返回,栈上的对象引用将失效,对象的数据也将被销毁。
  • 不适用于大对象:栈内存通常有限,不适合分配大型对象。对于较大的对象,仍然会分配在堆内存中。
  • 逃逸分析优化:逃逸分析是一个复杂的优化过程,Java虚拟机会根据分析结果来决定是否执行栈上分配。不是所有的对象都会被栈上分配,只有那些符合条件的对象才会被优化。
  • 多线程安全性:栈上分配的对象通常只能在创建它的线程中使用,不适合在多线程环境下共享。
  • 同步消除
    线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。
  • 标量替换
    如果一个数据是基本数据类型,不可拆分,它就被称之为标量。把一个Java对象拆散,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么可以不创建对象,直接用创建若干个成员变量代替,可以让对象的成员变量在栈上分配和读写。

可以打破双亲委派机制吗

双亲委派模型通常是由Java虚拟机本身实现和强制执行的,目的是确保类加载的安全性和唯一性。但在某些情况下,你可以通过编写自定义类加载器来打破双亲委派机制。这通常在以下情况下发生:

  1. 加载非标准类文件:如果你需要加载非标准的类文件,例如从数据库或网络中动态加载类,传统的双亲委派模型可能无法满足需求,因此你可能需要编写自己的类加载器。
  2. 实现类隔离:有时,你可能需要在同一个应用程序中加载多个版本的相同类,或者在不同的模块中加载相同的类,这时自定义类加载器可以帮助你实现类的隔离,防止类冲突。
  3. 动态更新类:一些应用程序需要在运行时动态更新类,这也可能需要绕过双亲委派模型,以便能够加载新版本的类。

要打破双亲委派模型,你需要编写自己的类加载器,并覆盖其loadClass方法。在这个方法中,你可以自行决定如何加载类,而不遵循双亲委派规则。通常,你会在自定义类加载器中实现类加载的逻辑,包括从文件系统、网络或其他来源加载类字节码,并使用defineClass方法将类定义加载到JVM中。
需要注意的是,打破双亲委派模型可能会引入类加载的不安全性和不稳定性,因此应该谨慎使用。

如何破坏双亲委派机制:tomcat的类加载机制(了解)

tomcat类加载机制:
WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。
步骤:

  1. 先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。
  2. 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。
  3. 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加
    载。
  4. 如果BoostrapClassLoader,没有加载成功,就会调用自己的findClass方法由自己来对类进行加载,findClass
    加载类的地址是自己本 web 应用下的 class。
  5. 加载依然失败,才使用 AppClassLoader 继续加载。
  6. 都没有加载成功的话,抛出异常。

jvm性能调优:一些常见问题、常用工具、命令

常用JDK命令:jps、jinfo、jstat、jstack、jmap

jps:查看java进程及相关信息
jinfo:查看JVM参数
jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收
jstack:查看JVM线程快照,jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环
jmap:可以用来查看内存信息 (配合jhat使用)
jhat:堆转储快照分析
jstack:Java堆栈跟踪
jcmd:实现上面除了jstat外所有命令的功能

操作系统工具:

top:显示系统整体资源使用情况
vmstat:监控内存和CPU
iostat:监控IO使用
netstat:监控网络使用

可视化的性能监控和故障处理工具:

JConsole
VisualVM
Java Mission Control(JMC)
MAT:Java 堆内存分析工具。
GChisto:GC 日志分析工具。
GCViewer:GC 日志分析工具。
JProfiler:商用的性能分析利器。
arthas:阿里开源诊断工具。
async-profiler:Java 应用性能分析工具,开源、火焰图、跨平台。

JVM性能调优

对应进程的JVM状态以定位问题和解决问题并作出相应的优化 常用命令:jps、jinfo、jstat、jstack、jmap
jps:查看java进程及相关信息

jps -l 输出jar包路径,类全名
jps -m 输出main参数
jps -v 输出JVM参数

jinfo:查看JVM参数

jinfo 11666
jinfo -flags 11666
Xmx、Xms、Xmn、MetaspaceSize

jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收

jstat [option] LVMID [interval] [count] 其中LVMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印)
option参数解释:
-gc 垃圾回收堆的行为统计
-gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计 -gcutil 垃圾回收统计概述
-gcnew 新生代行为统计
-gcold 年老代和永生代行为统计

jstack:查看JVM线程快照,jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环

jstack [-l] (连接运行中的进程)
option参数解释:
-F 当使用jstack 无响应时,强制输出线程堆栈。
-m 同时输出java和本地堆栈(混合模式)
-l 额外显示锁信息

jmap:可以用来查看内存信息 (配合jhat使用)

jmap [option] (连接正在执行的进程)
option参数解释:
-heap 打印java heap摘要
-dump: 生成java堆的dump文件

JVM的常见参数配置知道哪些?

一些常见的参数配置:
堆配置:

-Xms:初始堆大小
-Xms:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3表示Eden:3 Survivor:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小

收集器设置:

-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器

并行收集器设置:

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数
-XX:MaxGCPauseMillis=n:设置并行收集最大的暂停时间(如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况
-XX:ParallelGCThreads=n:设置并发收集器年轻代手机方式为并行收集时,使用的CPU数。并行收集线程数

打印GC回收的过程日志信息:

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

说说你对垃圾收集器的理解

垃圾收集器是Java虚拟机的一部分,负责管理内存中的对象,以确保内存的有效使用和回收不再使用的对象。以下是对垃圾收集器的理解:

  • 内存管理:垃圾收集器负责管理Java应用程序的堆内存。堆内存是用于存储Java对象的区域,而垃圾收集器负责分配、回收和释放这些内存。
  • 自动回收:垃圾收集器自动识别不再被引用的对象,并将其标记为垃圾,然后释放这些垃圾对象占用的内存。这个过程是自动的,程序员无需手动释放内存。
  • 内存泄漏防止:垃圾收集器可以防止内存泄漏,即程序中的对象无法被回收,导致内存消耗不断增加。通过垃圾收集器,不再使用的对象最终会被回收,释放内存。
  • 性能影响:不同的垃圾收集器实现具有不同的性能特性。一些收集器专注于最小化停顿时间(低延迟),而其他收集器则专注于最大化吞吐量。选择合适的垃圾收集器取决于应用程序的性能需求。
  • 分代垃圾收集:垃圾收集器通常使用分代策略,将堆内存分为不同的代(通常是年轻代和老年代),以便根据对象的生命周期采用不同的回收策略。年轻代通常使用快速的回收算法,而老年代则采用更复杂的算法。
  • 垃圾回收算法:不同的垃圾收集器实现使用不同的垃圾回收算法,如标记-清除、复制、标记-整理等。这些算法有不同的优缺点,适用于不同类型的应用程序。

总之,垃圾收集器是Java内存管理的关键组成部分,它负责自动管理对象的内存,防止内存泄漏,并提供不同的实现和配置选项,以满足不同类型的应用程序的性能需求。

如何判断对象可以被回收

在Java中,对象是否可以被回收通常由垃圾回收器决定。垃圾回收器使用一种称为"可达性分析"的算法来确定对象是否可被回收。可达性分析是指如果一个对象无法从任何GC Roots直接或间接访问到,它就被认为是不可达的,可以被垃圾回收。
GC Roots是一组特殊的引用,它们被认为是程序中可访问对象的起始点,即从这些引用开始,可以追踪到所有仍然被程序引用的对象。
GC Roots通常包括以下几种类型的引用:

  • 局部变量引用:在方法中定义的局部变量,包括方法的参数和局部变量,即虚拟机栈(栈桢中的本地变量表)中的引用的对象 ,通常被视为GC Roots。这些变量的引用指向了对象的实例。
  • 活动线程引用:正在运行的线程的引用通常被视为GC Roots。线程本地存储中的对象也是如此。例如ThreadLocal中的本地变量。
  • 静态变量引用:静态变量是类的一部分,它们的引用也被视为GC Roots。静态变量存在于类加载器的内存中。
  • JNI 引用:通过Java Native Interface(JNI)创建的本地代码引用也可以被视为GC Roots。这些引用连接了Java堆内存和本地代码的内存。
  • 虚拟机引导类加载器:虚拟机内部使用的类加载器引用也是GC Roots。它们通常是一些核心类或库。由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots。

可作为GC Roots对象的有哪几种

①虚拟机栈(栈桢中的本地变量表)中的引用的对象
②方法区中的类静态属性引用的对象
③方法区中的常量引用的对象
④本地方法栈中JNI的引用的对象
参考:哪些对象可以作为GC ROOT

说下JVM中一次完整的 GC 流程

JVM中的垃圾回收是自动进行的,它的目标是回收不再使用的对象,释放内存空间,并且保证程序的正常运行。下面是一次完整的GC流程的一般步骤:

  1. 标记阶段:GC从根对象开始,通过根对象的引用链,标记所有可达的对象。根对象包括活动线程的栈帧中的局部变量、静态变量、JNI引用等。
  2. 垃圾标记:在标记阶段完成后,GC会确定哪些对象是垃圾对象,即不可达对象。这些对象将被标记为垃圾,可以被回收。
  3. 垃圾回收:在标记阶段完成后,GC会执行垃圾回收操作,回收被标记为垃圾的对象所占用的内存空间。回收的方式有不同的算法,例如标记-清除、复制、标记-整理等。
  4. 内存整理:在垃圾回收完成后,可能会产生内存碎片。为了提高内存的利用率,GC可能会对内存空间进行整理,将存活的对象紧凑地排列在一起,以便更好地分配新的对象。
  5. 内存分配:在垃圾回收和内存整理完成后,GC会为新的对象分配内存空间。分配的方式有不同的算法,例如指针碰撞、空闲列表等。
  6. 重新分配对象引用:在垃圾回收和内存分配完成后,GC会更新对象之间的引用关系,确保引用指向正确的对象。

以上是一次完整的GC流程的一般步骤。不同的GC算法和实现可能会有所差异,但整体的流程大致相同。GC的目标是回收垃圾对象,释放内存空间,并且尽量减少对应用程序的影响,保证程序的正常运行。

内存溢出和内存泄漏是什么意思?

内存泄露:就是申请的内存空间没有被正确释放,导致内存被白白占用。
内存溢出:就是申请的内存超过了可用内存,内存不够了。
两者关系:内存泄露可能会导致内存溢出。
用一个有味道的比喻,内存溢出就是排队去蹲坑,发现没坑位了,内存泄漏,就是有人占着茅坑不拉屎,占着茅坑不拉屎的多了可能会导致坑位不够用。
在这里插入图片描述
内存溢出
在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能,重点关注堆和栈。

  • Java堆溢出:Java堆用于储存对象实例,只要不断创建不可被回收的对象,比如静态对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常(OutOfMemoryError)。
  • 虚拟机栈.OutOfMemoryError:JDK使用的HotSpot虚拟机的栈内存大小是固定的,我们可以把栈的内存设大一点,然后不断地去创建线程,因为操作系统给每个进程分配的内存是有限的,所以到最后,也会发生OutOfMemoryError异常。或者不断递归也会导致栈溢出。

内存泄漏
内存泄漏可能的原因有很多种:

  • 静态集合类引起内存泄漏:静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。
  • 单例模式:单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
  • 数据连接、IO、Socket等连接:创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。
  • 变量不合理的作用域:一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。
  • hash值发生变化:对象Hash值改变,使用HashMap、HashSet等容器中时候,由于对象修改之后的Hah值和存储进容器时的Hash值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么String类型被设置成了不可变类型。
  • ThreadLocal使用不当:ThreadLocal的弱引用导致内存泄漏也是个老生常谈的话题了,使用完ThreadLocal一定要记得使用remove方法来进行清除。

说一下对象有哪几种引用?强引用、软引用、弱引用和虚引用

Java中的引用有四种,分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 强引用是最传统的引用的定义,是指在程序代码之中普遍存在的引用赋值,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
    虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。
    参考《强引用、软引用、弱引用、虚引用的区别

finalize()方法了解吗?有什么作用?

用一个不太贴切的比喻,垃圾回收就是古代的秋后问斩,finalize()就是刀下留人,在人犯被处决之前,还要做最后一次审计,青天大老爷看看有没有什么冤情,需不需要刀下留人。
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果对象在在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它就”逃过一劫“;但是如果没有抓住这个机会,那么对象就真的要被回收了。

Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC都是什么意思?都什么时候回触发?

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

触发时机:
Minor GC/Young GC

  • 新创建的对象优先在新生代Eden区进行分配,如果Eden区没有足够的空间时,就会触发Young GC来清理新生代。

Full GC

  • Young GC之前检查老年代:在要进行 Young GC 的时候,发现老年代可用的连续内存空间 < 新生代历次Young GC后升入老年代的对象总和的平均大小,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。
  • Young GC之后老年代空间不足:执行Young GC之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次Full GC
  • 老年代空间不足,老年代内存使用率过高,达到一定比例,也会触发Full GC。
  • 空间分配担保失败( Promotion Failure),新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
  • 方法区内存空间不足:如果方法区由永久代实现,永久代空间不足 Full GC。
  • System.gc()等命令触发:System.gc()、jmap -dump 等命令会触发 full gc。

什么是Stop The World ? 什么是 OopMap ?什么是安全点?

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为Stop The World。也简称为STW
在HotSpot中,有个数据结构(映射表)称为OopMap。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在特定的位置生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:

  • 1.循环的末尾(非 counted 循环)
  • 2.方法临返回前 / 调用方法的call指令后
  • 3.可能抛异常的位置

这些位置就叫作安全点(safepoint)。 安全点(Safe Point)是指在Java程序执行过程中的某个特定位置,此时所有线程都处于安全状态,即没有执行关键的代码片段,如循环、方法调用等。在安全点上,垃圾回收器可以安全地进行垃圾回收操作,而不会对正在执行的线程产生影响。
为了进行垃圾回收,垃圾回收器需要暂停所有线程的执行,以便检查和回收不再使用的对象。然而,随意中断线程的执行可能会导致程序状态不一致或产生其他问题。因此,在安全点上,所有线程都会停止执行,等待垃圾回收器完成工作后再继续执行。
安全点的选择是由JVM控制的,通常在一些特定的位置上,例如方法调用、循环跳转、异常处理等。当线程达到安全点时,它会停止执行关键代码片段,并等待垃圾回收器完成垃圾回收操作后再继续执行。
安全点的存在可以减少垃圾回收对运行线程的影响,提高垃圾回收的效率。同时,安全点也是实现线程安全的关键之一,确保垃圾回收器和运行线程之间的正确交互。

什么是三色标记

三色标记是一种用于并发垃圾收集的算法,常用于分代垃圾收集器中的老年代的垃圾回收过程中。它基于对象的可达性来判断对象是否存活,并标记出存活对象。
三色标记算法将对象分为三种状态:白色、灰色和黑色

  • 白色:表示对象尚未被扫描,即未被标记为存活对象。
  • 灰色:表示对象已经被扫描,但其引用的其他对象尚未被扫描。
  • 黑色:表示对象已经被扫描,并且其引用的其他对象也已经被扫描。

垃圾收集器在开始垃圾回收时,将所有对象标记为白色。然后从根对象开始,递归地遍历对象图,将遇到的对象标记为灰色,并将其引用的对象添加到待扫描队列中。接着,垃圾收集器从待扫描队列中取出对象,将其标记为黑色,并将其引用的对象添加到待扫描队列中。这个过程会一直进行,直到待扫描队列为空。
最后,所有未被标记为黑色的对象即为垃圾对象,可以被回收。
三色标记算法的优点是可以在并发执行的情况下进行垃圾回收,减少停顿时间。它通过将对象分为三种状态,避免了在并发执行过程中的同时修改和访问对象的冲突。然而,三色标记算法也有一些缺点,如可能存在标记漏标和标记误标的情况,需要额外的处理来解决这些问题。

能说一下类的生命周期吗?

一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称为连接(Linking)。
在这里插入图片描述

你怎么理解常量池

常量池是Java编程语言中的一个重要概念,它是一种用于存储常量值、符号引用和字面量的数据结构,主要用于提高代码的效率和减少内存消耗。对于类级别的常量池,它存储在类文件class中,对于运行时常量池,它存储在内存中,用于支持类加载后的运行时解析。常量池中的数据项是不可改变的,一旦创建就不能修改。常量池中的数据项是唯一的,相同的常量值或符号引用在常量池中只会出现一次。
常量池允许多个类或方法共享相同的常量值,减少了内存占用。在运行时,Java虚拟机会根据符号引用在常量池中查找对应的实际信息,如类、方法、字段等。
字符串常量池也是常量池的一个重要部分,用于存储字符串字面量。字符串常量池中的字符串是唯一的,相同内容的字符串在池中只有一个副本。字符串常量池的优化提高了字符串的比较效率,例如使用equals()方法比较字符串时,可以直接比较引用。
在Java编程中,程序员通常不需要直接操作常量池,因为Java编译器和虚拟机会自动管理它。

常量池存储在JVM的哪块区域

常量池一般存储在JVM的方法区(或者元空间)。但是字符串常量池比较特殊,不同JDK版本存储位置有点不同,拿Java 8来说,字符串常量池是存储在堆内存中的,用于存储字符串字面量。这是一个特殊的堆内存区域,用于提高字符串操作的效率和节省内存。字符串常量池中的字符串是唯一的,相同的字符串字面量在常量池中只有一个副本。

场景问题

频繁发生fullGC怎么办

Full GC的排查思路大概如下:
1、清楚从程序角度,有哪些原因导致FGC?

  • 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
  • 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
  • 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)
  • 程序BUG
  • 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
  • JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。

2、清楚排查问题时能使用哪些工具

  • 公司的监控系统:大部分公司都会有,可全方位监控JVM的各项指标。
  • JDK的自带工具,包括jmap、jstat等常用命令。
  • 可视化的堆内存分析工具:JVisualVM、MAT等。

3、排查指南

  • 查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
  • 了解该时间点之前有没有程序上线、基础组件升级等情况。
  • 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
  • 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
  • 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
  • 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。

频繁发生younggc怎么办

优化Minor GC频繁问题:通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过增大新生代空间-Xmn来降低Minor GC的频率。

  • 调整启动参数,增加新生代大小
  • 调整堆内存大小
  • dump数据或者分析 GC 日志,检查代码中是否有内存泄露
  • 调整GC策略,修改收集器或一些GC参数,比如换G1收集器,调整younggc触发阈值等
  • 查看是否有创建频繁垃圾对象,或者出现大量无用对象,及时回收

JVM的堆内存Xms和Xmx参数设置为相同值有什么好处

把两者设置为一致,是为了避免频繁扩容和GC释放堆内存造成的系统开销/压力。
在GC后,仍然没有足够内存空间,堆内存就会进行扩容,反之如果GC后空闲空间足够多,就会进行缩容。
默认堆内存空闲空间<40%时,会进行扩容到Xmx;堆内存空闲空间>70%时,会缩小到Xms。

你们线上用的什么垃圾收集器?为什么要用它?

  • Parallel Scavenge + Parallel Old:Java8默认新生代用的Parallel Scavenge收集器,老年代用的是Parallel Old 收集器。Parallel Scavenge的特点是高吞吐,适用于业务相对复杂,但并发并不是非常高,所以希望尽可能的利用处理器资源,出于提高吞吐量的考虑采用Parallel Scavenge + Parallel Old的组合。
  • Parallel New+CMS:关注服务的响应速度,所以采用了CMS来降低停顿时间。
  • G1垃圾收集器:因为它不仅满足我们低停顿的要求,而且解决了CMS的浮动垃圾问题、内存碎片问题。

一些收集器的适用场景:

  • Serial :如果应用程序有一个很小的内存空间(大约100 MB)亦或它在没有停顿时间要求的单线程处理器上运行。
  • Parallel:如果优先考虑应用程序的峰值性能,并且没有时间要求要求,或者可以接受1秒或更长的停顿时间。
  • CMS/G1:如果响应时间比吞吐量优先级高,或者垃圾收集暂停必须保持在大约1秒以内。
  • ZGC:如果响应时间是高优先级的,或者堆空间比较大。

有做过JVM调优吗?

一个JVM调优流程图:
在这里插入图片描述

一个中规中矩的案例:电商公司的运营后台系统,偶发性的引发OOM异常,堆内存溢出。
1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,单方面的加大了堆内存从4G调整到8G -Xms8g。
2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。
3、用JProfiler 对 堆dump文件进行分析,通过JProfiler查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。
4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,有个方法引起了我的注意,导出订单信息。
5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。
6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现导出订单的按钮前端居然没有做点击后按钮置灰交互事件,后端也没有做防止重复提交,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,然后就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。
7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是做了两个处理:
在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击
后端代码加分布式锁,做防重处理
这样双管齐下,保证导出的请求不会一直打到服务端,问题解决!

内存飙高问题怎么排查?

分析:内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收

  • 1、先观察垃圾回收的情况
    jstat -gc PID 1000 查看GC次数,时间等信息,每隔一秒打印一次。
    jmap -histo PID | head -20 查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。
    如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。
  • 2、导出堆内存文件快照
    jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆内存信息到文件。
  • 3、使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。

有没有处理过内存泄漏问题?是如何定位的?

内存泄漏是内在病源,外在病症表现可能有:

  • 应用程序长时间连续运行时性能严重下降
  • CPU 使用率飙升,甚至到 100%
  • 频繁 Full GC,各种报警,例如接口超时报警等
  • 应用程序抛出 OutOfMemoryError 错误
  • 应用程序偶尔会耗尽连接对象

严重内存泄漏往往伴随频繁的 Full GC,所以分析排查内存泄漏问题首先还得从查看 Full GC 入手。主要有以下操作步骤:

  • 使用 jps 查看运行的 Java 进程 ID
  • 使用top -p [pid] 查看进程使用 CPU 和 MEM 的情况
  • 使用 top -Hp [pid] 查看进程下的所有线程占 CPU 和 MEM 的情况
  • 将线程 ID 转换为 16 进制:printf “%x\n” [pid],输出的值就是线程栈信息中的nid。例如:printf “%x\n” 29471,换行输出 731f。
  • 抓取线程栈:jstack 29452 > 29452.txt,可以多抓几次做个对比。在线程栈信息中找到对应线程号的 16 进制值,如下是 731f 线程的信息。线程栈分析可使用 Visualvm 插件 TDA。“Service Thread” #7 daemon prio=9 os_prio=0 tid=0x00007fbe2c164000 nid=0x731f runnable [0x0000000000000000]
    java.lang.Thread.State: RUNNABLE
  • 使用jstat -gcutil [pid] 5000 10 每隔 5 秒输出 GC 信息,输出 10 次,查看 YGC 和Full GC 次数。通常会出现 YGC 不增加或增加缓慢,而 Full GC 增加很快。或使用 jstat -gccause [pid] 5000 ,同样是输出 GC 摘要信息。或使用 jmap -heap [pid] 查看堆的摘要信息,关注老年代内存使用是否达到阀值,若达到阀值就会执行 Full GC。
  • 如果发现 Full GC 次数太多,就很大概率存在内存泄漏了
  • 使用 jmap -histo:live [pid] 输出每个类的对象数量,内存大小(字节单位)及全限定类名。
  • 生成 dump 文件,借助工具分析哪 个对象非常多,基本就能定位到问题在那了使用 jmap 生成 dump 文件:# jmap -dump:live,format=b,file=29471.dump 29471
    Dumping heap to /root/dump …Heap dump file created可以使用 jhat 命令分析:jhat -port 8000 29471.dump,浏览器访问 jhat 服务,端口是 8000。通常使用图形化工具分析,如 JDK 自带的 jvisualvm,从菜单 > 文件 > 装入 dump 文件。或使用第三方式具分析的,如 JProfiler 也是个图形化工具,GCViewer 工具。Eclipse 或以使用 MAT 工具查看。或使用在线分析平台 GCEasy。注意:如果 dump 文件较大的话,分析会占比较大的内存。基本上就可以定位到代码层的逻辑了。
  • 在 dump 文析结果中查找存在大量的对象,再查对其的引用。
  • dump 文件分析

有没有处理过内存溢出问题?

内存泄漏和内存溢出二者关系非常密切,内存溢出可能会有很多原因导致,内存泄漏最可能的罪魁祸首之一。
排查过程和排查内存泄漏过程类似。

你觉得应该怎么实现一个热部署功能?

我们已经知道了Java类的加载过程。一个Java类文件到虚拟机里的对象,要经过如下过程:首先通过Java编译器,将Java文件编译成class字节码,类加载器读取class字节码,再将类转化为实例,对实例newInstance就可以生成对象。
类加载器ClassLoader功能,也就是将class字节码转换到类的实例。在Java应用中,所有的实例都是由类加载器,加载而来。
一般在系统中,类的加载都是由系统自带的类加载器完成,而且对于同一个全限定名的java类(如com.csiar.soc.HelloWorld),只能被加载一次,而且无法被卸载。
这个时候问题就来了,如果我们希望将java类卸载,并且替换更新版本的java类,该怎么做呢?
既然在类加载器中,Java类只能被加载一次,并且无法卸载。那么我们是不是可以直接把Java类加载器干掉呢?答案是可以的,我们可以自定义类加载器,并重写ClassLoader的findClass方法。
想要实现热部署可以分以下三个步骤:

  1. 销毁原来的自定义ClassLoader
  2. 更新class类文件
  3. 创建新的ClassLoader去加载更新后的class类文件。

到此,一个热部署的功能就这样实现了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值