lukaliou123 / lukaliou123.github.io

lukaliou123在2022年的面试用知识点总结
Other
5 stars 0 forks source link

JVM篇 #11

Open lukaliou123 opened 2 years ago

lukaliou123 commented 2 years ago

1.介绍下Java内存区域

11..JDK1.8之前 image

JDK1.8: image 线程私有:程序计数器,虚拟机栈,本地方法栈 线程共享:堆,方法区,直接内存

1.2.程序计数器(重要)

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。 另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

主要作用: 1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程.上次运行到哪儿了。 例子:A电视时被B打断了,计数器会让A记住看到哪了

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

1.3.虚拟机栈(重要)

与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。 Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息)。 image Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError SOF:若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 OOM:若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更多内存的话。就会抛出 OutOfMemoryError 错误。

Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

1.4.本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

1.5.堆(重要)

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配 java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。 从内存回收角度来看java堆可分为:新生代和老生代。 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。

1.6.方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。

方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。

什么是本地方法(补充)

简单地讲,一个Native Method就是一个Java调用非Java代码的接囗。该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C" 告知C++编译器去调用一个C的函数。

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。

补充2:JVM启动时的重要参数

记住这些就很容易记:启动服务器,调整堆的最大最小,调整年轻代的大小,调整元空间的大小

-server -Xms512m -Xmx512m -Xmn256m -XX:MetaspaceSize=32m XX:MaxMetaspaceSize=50m

-server:启动jvm,一般是hotspot VM

-Xms:这个参数的名字来自于英文单词 "minimum" 的缩写 "min",Xms 的 "ms" 就是 "min size" 的意思,代表了 JVM 堆内存的最小大小。

-Xmx:这个参数的名字来自于英文单词 "maximum" 的缩写 "max",Xmx 的 "mx" 就是 "max size" 的意思,代表了 JVM 堆内存的最大大小。

-Xmn:这个参数的名字来自于英文单词 "new" 的缩写 "n",Xmn 的 "mn" 就是 "new size" 的意思,代表了 JVM 新生代的大小。

lukaliou123 commented 2 years ago

2.说一下Java对象的创建过程

image 1.类加载检查:这是创建对象的第一步。JVM在创建对象时要首先检查这个对象所对应的类是否已经被加载到内存。如果没有被加载,那么JVM就会进行加载,包括链接(验证,准备和解析),初始化等步骤。如果被加载了就不管了

2.分配内存:类加载检查通过后,JVM会在堆内存中为新的对象分配内存。对象的大小在类加载完成后就可以确定了,所以这一步是比较简单的。

3.初始化零值:内存分配完成后,JVM会为对象中的字段设置默认初始值,比如int类型的字段初始值为0,引用类型的字段初始值为null等

4.设置对象头:对象头的设置包括两个主要部分:一是这个对象是哪个类的实例,二是这个对象的哈希码和锁信息。这样JVM在运行时就能通过对象头获取这个对象的元数据信息。

对象头对象头在JVM中占有非常重要的位置,它包含了对象自身的运行时数据,例如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这些信息主要用于JVM的垃圾收集和同步等操作。其中,对象的锁是用于实现Java中的同步机制的,如synchronized关键字。对象的锁状态包括:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态等

5.执行init方法:最后,JVM会调用对象对应类的构造方法进行初始化,这样一个真正可用的对象就产生了。

例子:

7MWR6K``5D4SI 1$K`@BI~R 在这个过程中,首先JVM会检查Robot类是否已经被加载,如果没有就加载它。加载完成后,JVM在堆内存中为新的Robot对象分配内存,然后设置Robot对象的名字和年龄字段为默认值(null和0)。接下来,JVM设置Robot对象的对象头,包括这个对象是Robot类的实例以及对象的哈希码和锁信息。最后,JVM调用Robot类的构造方法,将Robot对象的名字设置为"虹夏",年龄设置为1。现在,这个Robot对象就可以被你使用了

补充:堆和栈

堆(Heap)和栈(Stack),是计算机中的两种不同类型的内存,它们在功能上有一些主要的区别。

堆(Heap):

堆是动态分配的,不需要知道需要多少内存,只需要在运行时分配就好了。它的大小并不固定,可以动态地增长和缩小。 堆内存用于存放由new创建的对象和数组,在函数调用结束后,堆内存并不会被清除。 堆内存由Java垃圾回收器自动管理。当没有引用指向一个对象,那么这个对象就会被垃圾回收器认定为垃圾,它将被清除,释放空间。

栈(Stack):

栈是静态分配的,它用于存放函数、方法调用时的局部变量和参数。在函数调用结束后,存放在栈内的数据会被自动清除栈的大小和生存期是可以自动确定的,当定义一个局部变量时,Java就在栈中为它分配内存空间,当局部变量的作用域结束时,Java会自动释放为它分配的内存空间,这样可以很有效地管理内存。 栈有利于保存执行现场,实现程序的逻辑控制。

堆和栈的区别:

存储内容堆可以存储Java中的对象和数组栈则主要存储一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象的引用。

存储方式:栈中存储的数据大小和生命周期是在编译时就已经确定的,当一个函数被调用时,一个新的栈帧就被压入栈,当一个函数调用结束时,对应的栈帧就会被弹出。而堆则是在运行时根据需要动态分配内存的。

生命周期:栈内存随着方法的生命周期而存在,当方法结束后,分配的内存就会自动释放。对于堆内存,虽然也有可能(比如对象实例结束生命周期),但必须由垃圾收集器来负责回收,而不是由CPU直接管理。

内存管理栈的存取速度比堆要快,仅次于寄存器,堆的存取速度较慢,因为栈的内存空间是连续的,数据的存取速度快。堆的空间比较灵活,也很大,但因为要在运行时动态分配内存,存取速度相对较慢。

lukaliou123 commented 2 years ago

3.JVM内存分配与回收(这里开始涉及垃回收)

image Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 内存中对象的分配与回收

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 image 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收(minor gc)后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。 image 经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To"。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

顺便新生代和老年代的大小比是1:2,可以调整

补充

垃圾回收主要在新生代Eden区进行,当Eden区满时,会触发一次Minor GC,清理掉不再被引用的对象,将仍在被引用的对象移至Survivor区或者老年代。当老年代也满了的时候,会触发一次Major GC或者叫Full GC,这种GC会清理整个堆空间,包括新生代和老年代。

lukaliou123 commented 2 years ago

4.堆内存中对象的分配的基本策略

image

1. 对象优先在 eden 区分配

目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,通过 分配担保机制 把新生代的对象提前转移到老年代中去。

2.大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

3.长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

4.动态对象年龄判定

对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。不一定是15. CMS 就是 6.

lukaliou123 commented 2 years ago

5.主要进行 GC 的区域

部分收集 (Partial GC): 1.新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集; 2.老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集; 3.混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集

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

lukaliou123 commented 2 years ago

6.如何判断对象是否死亡?(两种方法)

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡。 1.引用计数法 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。 2.可达性分析算法 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。 image

7.简单的介绍一下强引用,软引用,弱引用,虚引用

无论是通过引用计数法判断对象引用数量,还是通过可达性分析判断对象的引用链是否可达,判定对象的存活都与“引用”有关 Jdk1.2之后,Java对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用四种(引用强度逐渐减弱)。

1.强引用(StrongReference) 以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference) 如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

3.弱引用(WeakReference) 如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

4.虚引用(PhantomReference) "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

lukaliou123 commented 2 years ago

8.垃圾收集有哪些算法,各自的特点?

1.标记-清除算法 该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题。 a) 效率问题 b) 空间问题(标记清除后会产生大量不连续的碎片) image

2.复制算法 为了解决效率问题,“复制”算法就出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 image 不过因为会导致能用的内存/2,不适合存放大对象的老年代

3.标记整理算法 根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 image 因为多了个整理所以效率变慢,不适合新生代,可以放在垃圾触发少的老年代

4.分代收集算法 当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

为什么堆空间中用分代?(提升GC效率) 比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

lukaliou123 commented 2 years ago

9.常见的垃圾回收器有那些?(CMS,G1重要)

image 1.Serial 收集器 单线程收集器。它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。 image

2.ParNew收集器 Serial 收集器的多线程版本 image

3.ParallelScavenge 收集器 几乎和 ParNew 都一样。Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。

4.CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。 CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。 从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;

并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方

重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。 image

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点 1.对CPU资源敏感 2.无法处理浮动垃圾 3.它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生

5.G1

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多核处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:

1.并行与并发G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 2.分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 3.空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 4.可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1收集器的运作大致分为以下几个步骤: 1. 初始标记 2. 并发标记 3. 最终标记 4. 筛选回收 G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

CMS和G1的主要区别

1.并发与停顿:CMS是一个以最小化应用线程停顿时间为目标的收集器。它在标记和清除阶段都使用并发的方式,尽量减少了垃圾收集时的停顿。但CMS对CPU资源非常敏感,并在并发阶段会影响应用程序的性能。相比之下,G1设计目标是将STW(Stop The World)停顿时间和分布,都变得可预期且可配置,这是通过划分多个小的内存区域(Region)并并发处理实现的。

2.内存碎片和压缩:CMS在进行垃圾收集时可能会产生内存碎片,这是因为它使用的是“标记-清除”算法,在清除过程中并不会压缩内存,因此可能会出现大量的不连续的内存碎片。当程序需要申请大块连续内存而无法找到足够的连续空间时,就会触发一次昂贵的Full GC。而G1通过划分多个小的内存区域并进行区域之间的压缩,解决了这个问题。

3.预测停顿时间模型:G1有一项创新的特性,那就是可以建立可预测的停顿时间模型,即:G1除了让用户可以明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,还能根据程序运行情况自行调整,以达到停顿时间模型的理想效果。

4.长期的稳定性:CMS虽然在许多系统中表现良好,但由于并发模式下的内存碎片问题,可能会在长期运行后引发问题,尤其是内存较大、单次GC时间要求较严格的系统,可能需要频繁的Full GC来整理内存碎片。而G1则更适合需要长期稳定运行的系统。

lukaliou123 commented 2 years ago

补充:类加载的生命周期:

1.加载(Loading):查找并加载类的二进制数据。

2.连接:包括验证、准备和解析三个阶段。

2.1验证(Verify):确保被加载的类的信息符合Java虚拟机规范,没有安全方面的问题。 2.2.准备(Prepare):为类的静态变量分配内存,并将其初始化为默认值。 2.3.解析(Resolve):把类中的符号引用转换为直接引用。 3.初始化(Initialization):为类的静态变量赋予正确的初始值。

以上是类加载的主要阶段,还有两个额外的阶段:

4.使用(Using):Java程序被调用。

5.卸载(Unloading):当类加载器对象被回收时,该类将被卸载。

要注意的是,加载、验证、准备、初始化和卸载这五个阶段的严格顺序是确保Java程序具有良好的可移植性。其中解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的运行时绑定(也称为动态绑定或晚期绑定)。

11.双亲委派模型介绍

每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。 image

双亲委派的好处: 双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

两个重要的好处

1.可以确保 Java 核心库的类型安全:所有的 Java 应用都至少引用 java.lang.Object 类,也就是说在运行期,java.lang.Object 这个类会被加载到 JVM 中。由于这个类是核心库的成员,按照双亲委派模型,这个类会由启动类加载器去加载。而如果用户自定义了一个 java.lang.Object 类的话,那么由于用户自定义的类加载器在尝试加载一个类时,首先会委派给父类加载器,因此终究会由启动类加载器去加载这个类。由于启动类加载器只加载 JVM 的核心库,因此用户定义的这个类会加载失败。

2.可以避免类的重复加载:由于每个类加载器都有自己的类缓存,而类加载的请求最初总是由最底层的类加载器发起,因此通过双亲委派模型,可以确保当一个类被加载到 JVM 中之后,所有的类加载器都可以看到这个类。这就避免了同一个类被多次加载的情况。

补充:

在 Java 中,类加载器的工作过程遵循一个称为“双亲委派模型”的机制。这个模型的基本思想是,如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成

每一个类加载器都有一个父类加载器。这种父子关系通常是通过组合(composition)而不是继承(inheritance)来实现的。系统中最顶层的类加载器(称为“启动类加载器”或者“bootstrap class loader”)是由 JVM 提供的。

双亲委派模型的工作流程是这样的:

1.首先,检查请求的类是否已经被加载过。如果已经加载,则直接返回已经加载的类。每个类加载器都有自己的类缓存。 2.如果类还未被加载,尝试让父类加载器去加载这个类。 3.如果父类加载器不能加载这个类(因为类不在其搜索范围之内),那么才由自己去加载这个类。

这种机制有两个重要的好处

1.可以确保 Java 核心库的类型安全:所有的 Java 应用都至少引用 java.lang.Object 类,也就是说在运行期,java.lang.Object 这个类会被加载到 JVM 中。由于这个类是核心库的成员,按照双亲委派模型,这个类会由启动类加载器去加载。而如果用户自定义了一个 java.lang.Object 类的话,那么由于用户自定义的类加载器在尝试加载一个类时,首先会委派给父类加载器,因此终究会由启动类加载器去加载这个类。由于启动类加载器只加载 JVM 的核心库,因此用户定义的这个类会加载失败

2.可以避免类的重复加载:由于每个类加载器都有自己的类缓存,而类加载的请求最初总是由最底层的类加载器发起,因此通过双亲委派模型,可以确保当一个类被加载到 JVM 中之后,所有的类加载器都可以看到这个类。这就避免了同一个类被多次加载的情况。

双亲委派模型是一种遵循“最小权限原则”的设计思想,即每个类加载器只会尝试加载它自己的类,对于其他的类,总是先让其他的类加载器去尝试加载。这样可以避免不必要的重复加载

lukaliou123 commented 2 years ago

9.常见的垃圾回收器有那些?(CMS,G1重要)

image 1.Serial 收集器 单线程收集器。它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

image

2.ParNew收集器 Serial 收集器的多线程版本

image

3.ParallelScavenge 收集器 几乎和 ParNew 都一样。Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。

4.CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。 CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。 从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;

并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方

重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。 image

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点 1.对CPU资源敏感 2.无法处理浮动垃圾 3.它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生

5.G1

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多核处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:

1.并行与并发G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 2.分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 3.空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 4.控制回收垃圾的时间:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 大对象的处理 除了上面优点之外,还有一个优点,那就是对大对象的处理。在CMS内存中,如果一个对象过大,进入S1、S2区域的时候大于改分配的区域,对象会直接进入老年代。G1处理大对象时会判断对象是否大于一个Region大小的50%,如果大于50%就会横跨多个Region进行存放 image 依旧存在新生代老年代的概念,但是没有严格区分。Region最多分为2048G1收集器的运作大致分为以下几个步骤: 1. 初始标记 2. 并发标记 3. 最终标记 4. 筛选回收 G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

对比:

CMS 收集器 更适用于那些对系统响应时间有极高要求,且可以容忍一些偶然的垃圾收集暂停的场景。比如说,CMS 可能会因为在进行初始标记和最终标记阶段时需要“Stop The World”而导致系统的短暂暂停。如果在你的应用场景中,这种短暂的暂停是可以接受的,那么 CMS 可能是个不错的选择。

G1 收集器 适合于那些堆内存较大,且对系统的停顿时间有比较严格要求的场景.G1 的设计目标是将垃圾收集的停顿时间更可控,它允许用户指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经做到了全程并发标记,大大降低了"Stop The World"的影响。

lukaliou123 commented 1 year ago

重点补充:什么是JVM

JVM,即Java虚拟机,是Java平台的核心组成部分。它负责### 执行Java字节码,是一种能够在各种硬件和操作系统环境中运行Java程序的抽象计算机。在JVM上运行的所有Java程序都是先被编译成Java字节码文件(.class文件)的,然后在运行时被JVM解释执行或者通过即时编译器(JIT)编译成本地代码执行。

JVM的主要功能是进行加载、链接和初始化,以及执行和管理Java程序的生命周期。它的一个重要特性是"一次编写,到处运行",即在不同的平台上,只需要有对应的JVM,就能运行同样的Java字节码,无需对程序进行修改。

JVM内部包含了几个主要的运行时数据区,如方法区(存储已被虚拟机加载的类信息)、堆区(存储对象实例)、栈区(存储局部变量、操作数栈等)、程序计数器(当前线程所执行的字节码的行号指示器)等。这些区域的管理对于理解Java的内存管理和垃圾收集机制至关重要。

此外,JVM也具有内存管理和垃圾回收的功能。它会自动管理Java程序的内存空间,定期进行垃圾回收,释放不再使用的对象占用的内存,极大地方便了Java程序的编写和运行。

这就是我对JVM的基本了解,这个过程涉及到很多底层的细节,包括类加载机制,内存模型,垃圾回收算法等等。我对这些内容都有一定的了解和研究。