JVM
JVM
JVM 是 java 虚拟机,简单来说就是能执行标准 java 字节码的虚拟计算机
JVM 是如何工作的
首先程序在执行之前先要把 Java 代码(.java)转换成字节码(.class),JVM 通过类加载器(ClassLoader)把字节码加载到内存中,但字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine)将字节码翻译成底层机器码,再交由 CPU 去执行,CPU 执行的过程中需要调用本地库接口(Native Interface)来完成整个程序的运行。
jvm 的组件以及功能
以下是Java虚拟机(JVM)的主要组件以及它们的功能的简要描述:
| 组件 | 功能描述 |
|---|---|
| 类加载器(ClassLoader) | 负责加载Java类文件并将其转换为可执行的字节码,然后加载到内存中。 |
| 运行时数据区(Runtime Data Area) | 存储Java应用程序执行时所需的数据,包括方法区、堆、栈、程序计数器等。 |
| 方法区(Method Area) | 存储类的结构信息,包括类的字段、方法、静态变量等。 |
| 堆(Heap) | 存储Java对象实例,包括通过new关键字创建的对象。 |
| 栈(Stack) | 存储方法调用的局部变量、操作数栈和方法调用的执行环境。 |
| 程序计数器(Program Counter) | 记录当前线程执行的字节码指令位置,用于支持线程切换和方法调用。 |
| 本地方法接口(Native Interface) | 允许Java代码调用本地(非Java)库,通过JNI(Java Native Interface)实现。 |
| 执行引擎(Execution Engine) | 解释和执行字节码指令或将字节码编译成本地机器代码以提高执行速度。 |
| 本地方法库(Native Method Library) | 包含与底层操作系统交互的本地方法的实现。 |
这些组件共同工作,使Java应用程序能够在不同的操作系统上运行,并提供了内存管理、线程管理、安全性和性能优化等功能。
请谈一下方法区,永久代,元空间
方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。
而java8后元空间取代了永久代,元空间相对于永久代最大的改变在于它采用了更灵活的内存管理方式,不再受到固定大小的限制,并且使用不同的垃圾回收机制。这使得元空间更适应于需要加载大量类或动态生成类的应用程序,并且减少了永久代内存溢出的风险。这个改变提高了JVM的可用性和性能。
会发生内存溢出的区域?
以下是Java应用程序中可能发生内存溢出的一些常见区域以及它们的简要阐述:
| 区域 | 内存溢出可能性和原因 |
|---|---|
| 堆内存(Heap) | - 内存溢出(OutOfMemoryError)最常见于堆内存,通常由于创建了太多的对象,使得堆内存耗尽。 - 也可能是单个对象太大,超过了堆的剩余空间。 |
| 栈内存(Stack) | - 栈溢出(StackOverflowError)通常发生于递归调用导致栈空间不断增长,超过了栈的容量。 - 还可能由于线程过多,导致栈空间分配不足而产生溢出。 |
| 方法区(Method Area) | - 方法区内存溢出(OutOfMemoryError)可能发生于加载大量类或生成大量动态代理类时,导致方法区不断增长。 - 在Java 7及之前版本,由于永久代有限,也可能发生永久代内存溢出。 |
| 本地内存(Native Memory) | - 本地内存不受JVM垃圾回收的控制,可能由于本机库或外部资源占用过多内存而导致内存溢出。 - 内存泄漏也可能发生在本地内存中。 |
这些区域中的内存溢出通常由于资源分配不当、资源使用过度或资源管理错误等原因引起。避免内存溢出需要进行适当的资源管理和性能优化,确保应用程序不会无限制地占用内存。
什么是内存泄漏?内存泄漏与内存溢出的区别?
内存泄漏(Memory Leak)和内存溢出(Memory Overflow)是两种不同的内存问题,它们有以下区别:
| 特征 | 内存泄漏 | 内存溢出 |
|---|---|---|
| 定义 | 内存泄漏指的是应用程序中的对象在不再被使用时,仍然被保留在内存中,无法被垃圾回收器释放。 | 内存溢出指的是应用程序尝试分配更多内存,但没有足够的可用内存,导致无法继续正常执行。 |
| 原因 | - 内存泄漏通常是由于程序员忘记释放不再需要的对象的引用,或者引用被无意中保留,导致对象无法被回收。 - 内存泄漏可能是渐进性的,随着时间的推移逐渐消耗内存。 | - 内存溢出通常是由于应用程序需要分配更多内存以容纳新对象,但没有足够的可用内存。 |
| 影响 | - 内存泄漏会逐渐占用系统内存,最终导致系统性能下降,甚至崩溃。 - 内存泄漏通常需要长时间运行应用程序才会显现。 | - 内存溢出会导致应用程序立即崩溃或异常终止,因为无法分配所需的内存。 |
| 识别和解决方法 | - 识别内存泄漏通常需要使用内存分析工具,查找未被释放的对象引用。 - 解决内存泄漏通常需要修复代码,确保在不再需要对象时释放引用。 | - 内存溢出通常通过查看异常堆栈跟踪来识别。 - 解决内存溢出通常需要优化代码,减少内存使用或增加可用内存。 |
总之,内存泄漏是对象在不再需要时仍然被保留在内存中,而内存溢出是应用程序需要更多内存但没有足够可用内存的情况。内存泄漏通常随着时间逐渐累积,而内存溢出会导致应用程序立即崩溃。解决内存泄漏需要找到未释放的对象引用并释放它们,解决内存溢出通常需要优化内存使用或增加可用内存。
请谈一下什么是垃圾回收?
GC 前要做的三件事
- 哪些内存需要回收?
- 什么时候回收?
- 怎么回收?
如何确定垃圾(怎么判断对象是否可以被回收?)?
引用计数法:
只要一个对象被其他变量所引用,就让这个对象的计数 +1,如果某一个变量不在被引用,让
他的计数-1,当这个对象引用计数 =0 的时候,代表这个对象没有再被引用了,就可以作为一个
垃圾被回收掉。引用计数法有一个弊端,在循环引用的场合,如果两个对象被循环无限引用,虽
然都不在使用了,但是两个对象的计数都不为 0,导致不能被回收。
可达性分析:
确定一系列根对象,垃圾回收前先把堆中的对象进行一次扫描,判断每一个对象是不是被根
对象所直接或者间接引用,如果是,那么这个对象就不能被回收。反则如果这个对象没有被根对
象直接或者间接所引用,那么这个对象就可以作为垃圾被回收。
如何确定 GC Roots 对象?
虚拟机栈中引用的对象
方法区中的类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI 引用的对象
这些方式用于确定哪些对象是GC Roots对象,它们是可达性分析的起点,从而决定了哪些对象可以被认为是存活的,哪些可以被垃圾回收。
对象的引用关系都有哪些?
不管是引用计数法还是可达性分析算法都与对象的“引用”有关,这说明对象的引用决定了
对象的生死,对象的引用关系如下。
强引用:在代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还
在,垃圾收集器永远不会回收掉被引用的对象。
软引用: 是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM
认为内存不足时,才会去试图回收软引用指向的对象,JVM 会确保在抛出 OutOfMemoryError
之前,清理软引用指向的对象。
弱引用: 非必需对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一
次垃圾收集发生之前。
虚引用: 也称为幽灵引用或幻影引用,是最弱的一种引用关系,无法通过虚引用来获取
一个对象实例,为对象设置虚引用的目的只有一个,就是当着个对象被收集器回收时收到一
条系统通知。
垃圾回收算法有哪些?
以下是对常见的垃圾回收算法的简单汇总,包括它们的特点和适用场景的修订:
| 算法 | 特点和适用场景 |
|---|---|
| 标记-清除算法(Mark and Sweep) | - 首先标记所有可达对象,然后清除未标记的对象。 - 不会移动对象,但会产生内存碎片。 - 适用于大型堆,但可能引起停顿,主要用于老年代。 |
| 复制算法(Copying) | 为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。 |
| 标记-整理算法(Mark and Compact) | 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。** |
| 分代算法(Generational) | - 将堆内存划分为不同的代(如年轻代和老年代),使用不同的回收算法和周期。 - 基于分代假设:新创建的对象容易成为垃圾,而长期存活的对象较少成为垃圾。 - 年轻代使用复制算法,老年代使用标记-清除算法,综合利用各种算法。 |
这个修订的汇总表提供了更准确的描述,以反映每种垃圾回收算法的特点和适用场景。不同的算法可以在不同的堆内存区域(年轻代和老年代)或不同的应用场景中使用,以提高性能和内存管理效率。
Minor GC 年轻代垃圾回收触发机制
新生代垃圾回收采用的是复制算法,每次垃圾收集都能发现大批对象已经死亡,只有少量存
活,因此选择复制算法,年轻代又被分为 Eden 区,From 区和 To 区。
标记Eden + From Survivor 存活的对象
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
清空 Eden + From Survivor 分区,From Survivor 和 To Survivor 分区交换;
每次交换后存活的对象年龄+1,到达15,升级为老年代,大对象会直接进入老年代;
老年代中当空间到达一定占比,会触发全局回收,老年代一般采取标记-清除算法
Major GC(老年代垃圾回收)
Major GC 指发生在老年代的 GC,MajorGC 采用标记—清除算法。
Major GC 触发条件: 老年代空间不足时,会先尝试触发 Minor GC。Minor GC 之后空间还
不足,则会触发 Major GC。
Full-GC触发条件:
Full-GC是针对整个新生代,老年代和元空间的全局范围内的GC。Full-GC不等于Major GC也不等于Minor GC+Major 发生Full-GC具体看使用了什么垃圾回收器,才能解释是什么样的垃圾回收。当⽼年代的空间使⽤率超过某阈值时(且老年代的major GC 不足以提供充足的空间时),会触发Full GC;当元空间不⾜时(JDK1.7永久代不足),也会触发Full GC;当调⽤System.gc()也会安排⼀次Full GC。
JVM 中为什么新生代中要有两个 Survivor 区?
- 如果 Survivor 是 0 的话,也就是说新生代只有一个 Eden 分区,每次垃圾回收之后,存活的对
象都会进入老生代,这样老生代的内存空间很快就被占满了,从而触发最耗时的 Full GC ,显然 这样的收集器的效率是我们完全不能接受的。 - Survivor 中分为两个区一个 FromService 一个 ToService 区,首先如果只有一个区的话,当新 生代的 Gc 开始工作的时候先把 Eden 区的垃圾回收了,根据其标志-复制算法,我们需要保留的 对象会被移动到 FromService 区中,当FromService 中的内存容量达到了一个阈值,需要我们堆 FromService区进行收集的时候,会导致大量的内存碎片残存其中,以至于后来无法在存入大对象 了;两个区的好处在于,当 FromService区的不需要用到对象也需要被清理的时候,Minor GC 再 次被触发的时候,我们需要保留的对象送到了 ToService 区,然后将ToService 区域和 FromService 区域互换身份,这样我们避免了碎片化的存在,而且永远都有一个干净的内存区域可以使用,是内存区域非常的整洁。
常见的垃圾回收器有哪些??
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
新生代回收器一般采用的是复制算法,复制算法效率较高,但是浪费内存;
老生代回收器一般采用标记清楚算法,比如最常用的CMS;
Serial 垃圾收集器(单线程、复制算法):
Serial 垃圾收集器是最基本的垃圾收集器,它使用的是复制算法,Serial 是一个单线程收集
器,它只会使用一个 CPU 或一条线程去完成垃圾收集,在进行垃圾收集的同时必须暂停其他所
有的工作线程直到垃圾收集结束。简单、高效。对于单个 CPU 环境来说没有线程交互的开销,可
以获得最高的单线程回收效率。但一般限定单核 CPU 才可以使用。
ParNew 垃圾收集器(Serial+ 多线程):
ParNew 垃圾收集器是 Serial 收集器的多线程版本,使用的也是复制算法,除了使用多线程
进行垃圾回收,其余的行为全都和 Serial 一样,ParNew 垃圾收集器在垃圾收集过程中也会产生
STW。ParNew 会默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来
限制垃圾收集器的线程数。
CMS 垃圾回收器 (多线程标记清除算法):
是一款里程碑式的垃圾收集器,为什么这么说呢?因为在它之前,GC 线程和用户线程是无
法同时工作的,即使是 Parallel Scavenge,也不过是 GC 时开启多个线程并行回收而已,GC 的整
个过程依然要暂停用户线程,即 Stop The World。这带来的后果就是 Java 程序运行一段时间就会
卡顿一会,降低应用的响应速度,这对于运行在服务端的程序是不能被接收的。
G1 收集器
Garbage first 垃圾收集器相比 CMS 收集器有以下两个改进:
基于标记-整理算法,不产生内存碎片。可以非常精确控制停顿时间,在不牺牲吞吐量前提
下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,他把堆内存划分为几个固定大小的独立区域(上面提到的
分区收集算法),并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表。每次根
据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级回收机制确保 G1 收集器可
以在有限时间获取最高的垃圾收集效率。
CMS 收集器和 G1 收集器的区别:
CMS 收集器是老年代的收集器,可以配合新生代的 Serial 和 ParNew 收集器一起
使用;
G1 收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
CMS 收集器以最小的停顿时间为目标的收集器;
G1 收集器可预测垃圾回收的停顿时间
CMS 收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1 收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
什么是类加载器,类加载器有哪些?

1、什么是类加载器?
类加载器负责加载所有的类,其为所有被载入内存的类生成一个java.lang.Class实例对象。
2、类加载器有哪些?
JVM有三种类加载器:
(1)启动类加载器
该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自java.lang.classLoader。
(2)扩展类加载器
它的父类为启动类加载器,扩展类加载器是纯java类,是ClassLoader类的子类,负责加载JRE的扩展目录。
(3)应用程序类加载器
它的父类为扩展类加载器,它从环境变量classpath或者系统属性java.lang.path所指定的目录中加载类,它是自定义的类加载器的父加载器。
说一下类加载的执行过程?
当程序主动使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接、初始化3个步骤对该类进行类加载。
1、加载
加载指的是将类的class文件读入到内存中,并为之创建一个java.lang.Class对象。
2、连接
当类被加载之后,系统为之生成一个对应的Class对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到JRE中。
3、初始化
为类的静态变量赋予初始值。
什么是双亲委派模型?

如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器执行,如果父加载器还存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器,如果父类加载器可以完成父加载任务,就成功返回,如果父加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。
双亲委派模式的优势:
- 避免重复加载;
- 考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派模式传递到启动加载器,而启动加载器在核心Java
API中发现同名的类,发现该类已经被加载,就不会重新加载网络传递的Integer类,而直接返回已加载过的Integer.class,这样可以防止核心API库被随意篡改。
JVM 内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为 Eden 和 Survivor。
1)共享内存区划分
共享内存区 = 持久带 + 堆
持久带 = 方法区 + 其他
Java 堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
2)一些参数的配置
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设
定)
Survivor 区中的对象被复制次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold)
3)为什么要分为 Eden 和 Survivor?为什么要设置两个 Survivor区?
如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年
代。老年代很快被填满,触发 Major GC.老年代的内存空间远大于新生代,进行一
次 Full GC 消耗的时间比 Minor GC 长得多,所以需要分为 Eden 和 Survivor。
Survivor 的存在意义,就是减少被送到老年代的对象,进而减少 Full GC 的发生,
Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,
才会被送到老年代。
设置两个 Survivor 区最大的好处就是解决了碎片化,刚刚新建的对象在 Eden 中,
经历一次 Minor GC,Eden 中的存活对象就会被移动到第一块 survivor space
S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0 中
的存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为
这种复制算法保证了 S1 中来自 S0 和 Eden 两部分的存活对象占用连续的内存空
间,避免了碎片化的发生)
JVM 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代
当 Eden 区的空间满了, Java 虚拟机会触发一次 Minor GC,以收集新生代的垃
圾,存活下来的对象,则会转移到 Survivor 区。
大对象(需要大量连续内存空间的 Java 对象,如那种很长的字符串)直接进入老
年态;
如果对象在 Eden 出生,并经过第一次 Minor GC 后仍然存活,并且被 Survivor
容纳的话,年龄设为 1,每熬过一次 Minor GC,年龄+1,若年龄超过一定限制
(15),则被晋升到老年态。即长期存活的对象进入老年态。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行 Full GC,Full
GC 清理整个内存堆 – 包括年轻代和年老代。
Major GC 发生在老年代的 GC,清理老年区,经常会伴随至少一次 Minor GC,
比 Minor GC 慢 10 倍以上。
JVM 内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存。

指令重排序:
代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会
采用将指令乱序执行(out-of-order execution,简称 OoOE 或 OOE)的方
法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令
本文详细介绍了JVM的工作原理,包括类加载过程、内存区域如方法区、堆、栈等的作用,以及垃圾回收的机制和不同算法。重点讨论了内存溢出和内存泄漏的区别,以及如何判断对象是否可回收。此外,还涵盖了类加载器的层次结构、双亲委派模型和JVM内存模型的相关概念,如重排序、内存屏障和主内存、工作内存等。
1148

被折叠的 条评论
为什么被折叠?



