Java基础、多线程、JVM

6 篇文章 0 订阅

1、简要介绍一下JVM虚拟机?

  通过软件模拟出来的具有完整的硬件系统功能、运行在完全隔离的环境中的完整的计算机系统。虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

2、简述一次GC的过程?

  JVM GC只回收堆区和方法区(堆有存放对象的两大区域),JVM 中的堆一般分为两大部分:新生代和老年代,新生代又分成三个区域,一个Eden和两个Survivor区,默认空间比例是8:1:1。

  新实例的对象首先存放在Eden区,当Eden空间不足,则进行一个minorGC,将存活的对象放在其中一个Survivor中,并且每次minorGC对象年龄加一,当这个Survivor中空间不足,将进行minorGC时把存活的对象复制到另一个Survivor,清空此Survivor,当对象的年龄达到阈值时,将把对象放进老年代,当老年代的空间不足时则进行一个FullGC,或当一个大对象(需要大量连续存储空间的对象)进行一次minorGC存活下来,可直接进入老年代,避免Eden与Survivor之间的大量的复制。

在这里插入图片描述

  持久代也称之为方法区,用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:

  • 1、所有实例被回收;
  • 2、加载该类的Class Loader 被回收;
  • 3、Class 对象无法通过任何途径访问(包括反射);

3、JVM 内存区域划分?

在这里插入图片描述

3.1 程序计数器

  程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

  如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

3.2 Java 虚拟机栈

  与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  经常有人把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java 内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。其中64 位长度的long 和double 类型的数据会占用2 个局部变量空间(Slot),其余的数据类型只占用1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。

3.3 本地方法栈

  本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。 虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

  Java Native方法是使用其他编程语言(如C/C++)实现的Java方法。Java Native方法可以让Java程序访问系统资源和底层硬件,提高Java程序的性能和功能。 以下是一些常见的Java Native方法服务:

  系统调用:Java Native方法可以调用操作系统提供的系统函数和API,如文件读写、网络通信、进程管理等。

  图形界面:Java Native方法可以使用操作系统的原生窗口和控件,实现更高效的图形界面交互。

  数据库访问:Java Native方法可以通过调用本地数据库客户端API,实现更快速和高效的数据库访问。

  外部库支持:Java Native方法可以使用外部C/C++库或DLL文件,扩展Java程序的功能和性能。

  多媒体处理:Java Native方法可以使用本地多媒体库,实现高质量的音频、视频、图像处理等功能。

  硬件访问:Java Native方法可以调用硬件设备的驱动程序,如摄像头、打印机、传感器等,实现对硬件设备的访问和控制。

  需要注意的是,Java Native方法需要使用JNI(Java Native Interface)技术进行编写和调用,编写Native方法需要具有一定的C/C++编程知识和技能。同时,使用Native方法也会增加代码的复杂度和移植性的难度,因此在使用时需要权衡其优缺点。

3.4 Java 堆

  对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

  Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代,再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java 堆中的上述各个区域的分配和回收等细节将会是下一章的主题。

  根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

3.5 方法区

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

  方法区和元数据区都是永久代的物理实现,永久代是一种逻辑上的理论。

  对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。

  Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

3.6 运行时常量池

  运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

  Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

  运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常

3.7 直接内存

  直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

  在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。

  显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

  逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码:

Object obj = new Object();

  假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

  由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。

4、JVM共享内存都有什么,什么是堆外内存?

在这里插入图片描述

  方法区和堆是所有线程共享的数据区;

  Java有堆内存即Hotspot,堆内存是java语言别与其他语言的优势之一,堆内存完全由JVM负责分配和释放。 如果程序没有缺陷代码导致内存泄露,程序员不需要像写C++那样考虑什么时候该释放内存。java中,你只管创建对象,回收内存的事情交给GC。如果有些特殊需求需要自己控制内存分配,还有一个不常用的堆外内存,使用堆外内存是为了能直接分配和释放内存,提高效率。JDK5.0之后,代码中能直接操作本地内存的方式有2种:使用未公开的Unsafe和NIO包下ByteBuffer。但是,堆外内存几乎是不受GC的管理的,也就是说,需要用户自己调用System.gc()进行内存的释放

堆外内存的好处是:

	(1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

	(2)理论上能减少GC暂停时间;

	(3)可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

	(4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据;

5、GC区域?垃圾回收算法?垃圾回收器?G1、CMS、ParNew等垃圾回收器的简介和之间的区别?

5.1 GC区域

	Java堆、方法区;

5.2 垃圾收集算法:

	引用计数算法、可达性算法; 
	由于引用计数法可能会导致内存泄漏i问题,所以Java内采用的是可达性算法。

5.3 垃圾回收算法:

  算法一:引用计数法

  这个方法是最经典点的一种方法。具体是对于对象设置一个引用计数器,每增加一个变量对它的引用,引用计数器就会加1,没减少一个变量的引用,引用计数器就会减1,只有当对象的引用计数器变成0时,该对象才会被回收。可见这个算法很简单,但是简单往往会存在很多问题,这里我列举最明显的两个问题。

  一是采用这种方法后,每次在增加变量引用和减少引用时都要进行加法或减法操作,如果频繁操作对象的话,在一定程度上增加的系统的消耗。

  二是这种方法无法处理循环引用的情况。再解释下什么是循环引用,假设有两个对象 A和B,A中引用了B对象,并且B中也引用了A对象,那么这时两个对象的引用计数器都不为0,但是由于存在相互引用导致无法垃圾回收A和 B,导致内存泄漏。

  算法二:标记清除法

  这个方法是将垃圾回收分成了两个阶段:标记阶段和清除阶段

  在标记阶段,通过跟对象,标记所有从跟节点开始的可达的对象,那么未标记的对象就是未被引用的垃圾对象。

  在清除阶段,清除掉所以的未被标记的对象。

  这个方法的缺点是,垃圾回收后可能存在大量的磁盘碎片,准确的说是内存碎片。因为对象所占用的地址空间是固定的。对于这个算法还有改进的算法,就是我后面要说的算法三。

  算法三:标记压缩清除法(Java中老年代采用)

  在算法二的基础上做了一个改进,可以说这个算法分为三个阶段:标记阶段,压缩阶段,清除阶段。标记阶段和清除阶段不变,只不过增加了一个压缩阶段,就是在做完标记阶段后,将这些标记过的对象集中放到一起,确定开始和结束地址,比如全部放到开始处,这样再去清除,将不会产生磁盘碎片。但是我们也要注意到几个问题,压缩阶段占用了系统的消耗,并且如果标记对象过多的话,损耗可能会很大,在标记对象相对较少的时候,效率较高。

  算法四:复制算法(Java中新生代采用)

  核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象,然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片

   算法五:分代法(Java堆采用)

  主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率

  比如Java虚拟机中的堆就采用了这种方法分成了新生代和老年代。然后对于不同的代采用不同的垃圾回收算法。

  新生代使用了复制算法,老年代使用了标记压缩清除算法。

  算法六:分区算法(新生代采用)

  这种方法将整个空间划分成连续的不同的小区间,每个区间都独立使用,独立回收,好处是可以控制一次回收多少个小区间

5.4 垃圾回收器:

  Java提供多种类型的垃圾回收器。JVM中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。

  Serial收集器:新生代收集器,使用复制算法,使用一个线程进行GC,串行,其它工作线程暂停

  Parallel New收集器:新生代收集器,使用复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

  Parallel Scavenge 收集器:吞吐量优先的垃圾回收器,作用在新生代,使用复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾。

  Serial Old收集器:老年代收集器,单线程收集器,串行,使用标记整理算法,使用单线程进行GC,其它工作线程暂停。

  Parallel Old收集器:吞吐量优先的垃圾回收器,作用在老年代,多线程,并行,多线程机制与Parallel Scavenge差不错,使用标记整理算法,在Parallel Old执行时,仍然需要暂停其它线程。

  CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见Full GC和并发垃圾回收一节),当用户线程内存不足时,采用备用方案Serial Old收集。

6、类加载过程?

  详解直达链接:https://www.nowcoder.com/questionTerminal/3c9591e7f61343faab53f9288ff6a47f

  类加载即为虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的 java 类型。

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。 其中准备、验证、解析3个部分统称为连接(Linking)。

在这里插入图片描述
  加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

7、GC、死锁怎么查问题?

  详解直达链接:https://blog.csdn.net/weixin_28760063/article/details/81266578

  Jconsole: Jconsole是JDK自带的图形化界面工具;

  Jstack: Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

8、Synchronized

8.1 Synchronized特点?

  Synchronized 是 Java 中的一个关键字,可以用来实现线程的同步和协作。

  Synchronized 可以修饰方法和代码块,作用是保证同一时刻只有一个线程可以执行被 Synchronized 修饰的代码。

  Synchronized 是可重入锁,也就是说,一个线程已经获得了某个对象的锁,再次请求该对象的锁时仍然会得到该对象的锁。

8.2 Synchronized 修饰方法和修饰类的区别?

8.2.1 修饰方法

  当 Synchronized 修饰一个方法时,该方法的锁定范围是该对象实例。 也就是说,当一个线程进入该方法时,它会获得该对象实例的锁,其他线程需要等待该线程释放锁才能进入该方法。

  修饰方法时,Synchronized 是不能被继承的。 也就是说,如果一个父类中的方法被 Synchronized 修饰了,那么该方法在子类中并没有被自动继承,如果子类需要同步该方法,还需要手动添加 Synchronized 关键字。

8.2.2 修饰类

  当Synchronized 修饰一个类时,该类的锁定范围是该类的所有实例。 也就是说,当一个线程进入该类的任意一个方法时,它会获得该类的锁,其他线程需要等待该线程释放锁才能进入该类的任意一个方法。

  修饰类时,Synchronized 是可以被继承的。 也就是说,如果一个父类被 Synchronized 修饰了,那么该类在子类中也会被自动继承 Synchronized 关键字。

  无论是 Synchronized 修饰方法还是修饰类,都是通过获取一个对象实例的锁来实现同步。当一个线程进入同步代码块时,它会尝试获取该对象实例的锁,如果锁没有被其他线程持有,则该线程获得锁并继续执行代码,否则该线程会被阻塞,直到锁被其他线程释放。

  需要注意的是,Synchronized 是一种独占锁,也就是说,同一时刻只有一个线程可以获得锁。 因此在高并发环境下,使用 Synchronized 可能会成为性能瓶颈。在这种情况下,可以考虑使用 ReentrantLock 等其他锁机制来替代 Synchronized。

8.3 Synchronized在JDK6做了哪些优化?

  JDK1.6 对锁的实现引入了大量的优化,如偏向锁(偏向于第一个获得它的线程)、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

  偏向锁:当一个线程获得了某个对象的锁时,JVM 会将这个锁标记为偏向锁。在以后的操作中,该线程可以直接获得锁,而无需再次进行同步操作。

  轻量级锁:当多个线程竞争同一个锁时,JVM 会将锁标记为轻量级锁。此时,每个线程都会尝试通过 CAS 操作来获得锁,避免了系统级别的同步操作。

  自旋锁:当线程无法获得轻量级锁时,JVM 会将锁标记为自旋锁。此时,线程不会阻塞,而是循环等待直到获取到锁为止。

8.4 Synchronized和Lock的区别?

类别synchronizedLock
存在层次Java的关键字,在jvm层面上是一个类
锁的释放1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁在finally中必须释放锁,不然容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入 不可中断 非公平可重入 可中断 可公平(两者皆可)
性能少量同步大量同步

9、什么是CAS?Volatile有什么用?

  CAS(Compare and Swap)是一种用于实现多线程同步的原子操作,也是Java中实现锁的基础 它包含三个操作数:内存地址V,期望值A和新值B。CAS操作的含义是:如果地址V中的值与期望值A相等,则用新值B替换原值;否则,不做任何操作。 在Java中,CAS通常使用sun.misc.Unsafe类中的方法实现,它利用CPU提供的原子操作指令来实现非阻塞同步,避免了锁的开销和线程切换的代价,从而提高了并发性能。CAS操作也常用于实现无锁数据结构和高性能的并发算法。

  CAS的缺点:

  1、 循环时间长开销大:如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。不断自旋

  2、 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们只能使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

  3、 可能会引发ABA问题:ABA问题其实就是狸猫换太子;产生原因:CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

  Volatile是Java中的关键字,用于标记变量以指示其可能被多个线程同时访问和修改。它确保了所有线程在读写volatile变量时都能看到最新的值,从而避免了线程间的竞争和数据不一致的问题。Volatile的主要作用是保证可见性和有序性,但不保证原子性。

  volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)

  volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

  禁止指令重排,保证特定操作的执行顺序。

10、Java都有哪几种锁?

  自旋锁 ,阻塞锁,可重入锁 ,读写锁 ,互斥锁 ,悲观锁 ,乐观锁 ,公平锁 ,偏向锁, 对象锁,线程锁,锁粗化, 锁消除,轻量级锁,重量级锁,独享锁,共享锁,分段锁。

  Java 中常见的锁主要有以下几种:

10.1 synchronized 关键字锁

  synchronized 是 Java 中最常用的锁,它是基于对象的锁,可以在任何对象及方法上使用。使用 synchronized 关键字修饰的代码块,在同一时刻只允许一个线程进入,其他线程需要等待。

10.2 ReentrantLock 锁

  ReentrantLock 是 Java 5 中提供的一种可重入锁,具有与 synchronized 关键字相同的并发性和内存语义。它相比于 synchronized 关键字,具有更加灵活的线程控制、可中断的等待以及可实现公平锁等优点。 需要注意的是,使用 ReentrantLock 锁时,一定要在 finally 块中释放锁。

10.3 ReadWriteLock 读写锁

  ReadWriteLock 是一种特殊的锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。 Java 中的 ReadWriteLock 接口提供了读写锁的实现。

10.4 StampedLock 锁

  StampedLock 是 Java 8 中新增的一种锁,它支持读写锁和乐观锁,并且相比于 ReadWriteLock 锁,它的并发性能更好。 StampedLock 锁的乐观锁部分类似于无锁并发操作,因此,当读取操作比写入操作更加频繁时,使用 StampedLock 锁可以提高系统的并发性能。

10.5 Atomic 类

  Java 中的 Atomic 类提供了一些原子性操作,比如 AtomicInteger、AtomicBoolean 等,它们都是线程安全的。这些类通常使用 CAS(Compare And Swap)算法实现,CAS 算法在 CPU 指令层面支持原子性操作,可以避免锁带来的性能开销。

  总的来说,Java 中提供的锁类型各有优劣,具体的选择要根据具体场景来确定,例如,如果读操作比写操作更频繁,可以使用 ReadWriteLock 或者 StampedLock 锁;如果操作数比较少,可以考虑使用 synchronized 锁或者 Atomic 类。

11、线程池分几种类型?

11.1 newCachedThreadPool

  创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:

  • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
  • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪

11.2 newFixedThreadPool

  创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源

11.3 newSingleThreadExecutor

  创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

11.4 newScheduleThreadPool

  创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。延迟3秒执行。

12、Java乐观锁的实现

  CAS+自旋

13、HashMap

13.1 HashMap是线程安全的么?

  非线程安全

13.2 底层怎么实现的(get,set)?

13.2.1 get 操作

  HashMap 的 get 方法根据 key 查找对应的 value,它的实现过程如下:

  计算 key 的哈希值,通过哈希函数计算得到该 key 对应的桶的索引。

  在桶中查找 key,如果该桶中没有元素或者桶中的元素的 key 不等于待查找的 key,则说明该 key 不存在于 HashMap 中,返回 null。

  如果桶中的元素的 key 等于待查找的 key,则返回该元素的 value。

  如果一个桶中的链表或者红黑树很长,那么查找的效率就会很低。因此,HashMap 在实现过程中,对于一个桶中的元素,当链表的长度大于等于8时,将链表转换为红黑树来提高查询效率

13.2.2 set 操作

  HashMap 的 put 方法用于将 key-value 对存储到 HashMap 中,它的实现过程如下:

  计算 key 的哈希值,通过哈希函数计算得到该 key 对应的桶的索引。

  在桶中查找 key,如果该桶中没有元素,则将新的元素添加到该桶中,并将桶的大小加 1。

  如果桶中的元素的 key 等于待添加的 key,则将该元素的 value 更新为新的值。

  如果该桶中的元素个数大于等于阈值(TREEIFY_THRESHOLD),则将链表转换为红黑树,否则直接将元素添加到链表的尾部。

  如果添加新的元素导致桶中元素的数量大于等于负载因子(loadFactor * capacity),则进行扩容操作(resize)。

13.3 JDK1.8之前和之后做了哪些修改?

  增加红黑树优化hashMap的底层结构在jdk1.7中由数组+链表实现,在jdk1.8中由数组+链表+红黑树实现,当链表长度超过8时,HashMap会将链表转化为红黑树来优化查询速度,从而提升HashMap的性能。

  减少哈希冲突:在JDK1.8中,哈希函数的实现变得更加高效,能够更有效地减少哈希冲突,从而提高HashMap的性能。

  减少空间浪费:在JDK1.8中,HashMap的实现采用了一种新的方式来存储键值对,从而减少了空间浪费,提高了HashMap的性能。

  更高效的迭代器:在JDK1.8中,HashMap的迭代器实现变得更加高效,能够更快地遍历HashMap中的所有元素。

  改进了并发性能:在JDK1.8中,HashMap的并发性能得到了很大的提升,采用了一些新的并发算法和技术,从而提高了HashMap的并发性能。

13.4 如果要使得插入kv有序需要使用哪种HashMap?

  LinkedHashMap,TreeMap

13.5 ConcurrentHashMap线程安全是怎么实现的?

  ConcurrentHashMap JDK_1.7使用的就是锁分段技术,ConcurrentHashMap由多个Segment组成(Segment下包含很多Node,也就是我们的键值对了),Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

  ConcurrentHashMap实现 1.8 与 JDK 1.7的区别

  ① 数据结构取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构

  ② 保证线程安全机制JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。

  ③ 锁的粒度原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)

  ④ 定位结点的hash算法简化,会带来弊端:Hash冲突加剧。因此在链表节点数量大于8时,会将链表转化为红黑树进行存储

  ⑤ 查询时间复杂度从原来的遍历链表O(n),变成遍历红黑树O(logN)

14、ArrayList和LinkedList的区别?栈和队列的区别?

  Arraylist:底层是基于动态数组,根据下表随机访问数组元素的效率高,向数组尾部添加元素的效率高,对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。

  Linkedlist基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历。对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

  队列(Queue):先进先出 ;是限定只能在表的一端进行插入和另一端删除操作的线性表 ;基于地址指针进行遍历,而且可以从头部或者尾部进行遍历,但不能同时遍历,无需开辟空间,因为在遍历的过程中不影响数据结构,所以遍历速度要快 ;

  栈(Stack):先进后出 ;是限定之能在表的一端进行插入和删除操作的线性表;只能从顶部取数据,也就是说最先进入栈底的,需要遍历整个栈才能取出来,遍历数据时需要微数据开辟临时空间,保持数据在遍历前的一致性;

15、Netty和Jetty区别?

  Jetty是Web容器,类似Tomct之类的;

  Netty是网络通信的框架,Netty可以支持Http协议,Tcp,Ucp,Netty是一个NIO框架,这个框架的主要目的是为了编写服务器端代码的。

  NETTY比JETTY,前者是TCP和UDP这一类SOCKET框架,后者是HTTP框架。

16、静态代理、动态代理

  Java代理是指为其他对象提供一个代理,以控制对该对象的访问。代理可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象添加额外的功能。

  Java代理主要分为静态代理和动态代理两种。

  静态代理

  静态代理是在编译时就已经确定了代理对象,代理类和被代理类的关系,并生成字节码文件。在运行时,通过调用代理对象的方法实现对被代理对象的调用。

  静态代理的实现比较简单,但缺点是只能代理一个特定的类,如果需要代理多个类,需要为每个类都编写一个代理类。

  动态代理

  动态代理是在运行时根据接口创建代理类的实例,代理类并不是预先定义的,而是在运行时动态生成的。

  Java提供了两种动态代理的实现方式:基于接口的动态代理和基于类的动态代理。基于接口的动态代理使用JDK自带的Proxy类,而基于类的动态代理则需要使用第三方库,比如CGLIB。

  动态代理的优点是可以代理多个类,且只需要编写一个代理类。缺点是在创建代理对象时需要实现InvocationHandler接口,并重写invoke()方法,对于初学者来说可能比较复杂。

17 HashMap和HashTable区别

  1) 线程安全性不同

  HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronize的,在多线程并发的情况下,可以直接使用HashTabl,但是使用HashMap时必须自己增加同步处理。

  2) 是否提供contains方法

  HashMap只有containsValue和containsKey方法;HashTable有contains、containsKey和containsValue三个方法,其中contains和containsValue方法功能相同。

  3) key和value是否允许null值

  Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。

  4) 数组初始化和扩容机制

   HashTable在不指定容量的情况下的默认容量为11,而HashMap为16;Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

18 TreeSet和HashSet区别

   HashSet是采用hash表来实现的。其中的元素没有按顺序排列,add()、remove()以及contains()等方法都是复杂度为O(1)的方法。

   TreeSet是采用树结构实现(红黑树算法),元素是按顺序进行排列,但是add()、remove()以及contains()等方法都是复杂度为O(log (n))的方法。它还提供了一些方法来处理排序的set,如first(),last(),headSet(),tailSet()等等。

19 String、StringBuffer 和 StringBuilder 的区别

19.1 可变性

   简单的来说: String 类中使用 final 关键字字符数组保存字符串, privatefinal char value[],所以 String 对象是不可变的。而 StringBuilder 与StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以StringBuilder 与StringBuffer 这两种对象都是可变的

   StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是AbstractStringBuilder 实现的。

AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}

19.2 线程安全性

   String 中的对象是不可变的,也就可以理解为常量,线程安全。 AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、 append、 insert、 indexOf 等公共方法。 StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。 StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

19.3 性能

   每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。 StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

   对于三者使用的总结:

      1. 操作少量的数据 = String

      2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder

      3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffer

20 Final、Finally、Finalize

   final:修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。

   finally:通常放在try…catch的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。

   finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。

21 ==和Equals区别

   == : 如果比较的是基本数据类型,那么比较的是变量的值,如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)

   equals:如果没重写equals方法比较的是两个对象的地址值,如果重写了equals方法后我们往往比较的是对象中的属性的内容,equals方法是从Object类中继承的,默认的实现就是使用==

public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b 为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}

22 Synchronized与Lock的区别

  1)Synchronized能实现的功能Lock都可以实现,而且Lock比Synchronized更好用,更灵活。

  2)Synchronized可以自动上锁和解锁;Lock需要手动上锁和解锁

23 Runnable和Callable的区别

  1)Runnable接口中的方法没有返回值;Callable接口中的方法有返回值

  2)Runnable接口中的方法没有抛出异常;Callable接口中的方法抛出了异常

  3)Runnable接口中的落地方法是call方法;Callable接口中的落地方法是run方法

24 什么是分布式锁

  当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。分布式锁可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存,如 Redis,通过set (key,value,nx,px,timeout)方法添加分布式锁。

25 什么是分布式事务

  分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

26 Java 面向对象编程三大特性

26.1 封装

  封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

26.2 继承

  继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。

  关于继承如下 3 点请记住:

    1. 子类拥有父类非 private 的属性和方法。

    2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

    3. 子类可以用自己的方式实现父类的方法。

26.3 多态

  所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

  在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

27 自动装箱与拆箱

  装箱:将基本类型用它们对应的引用类型包装起来;

  拆箱:将包装类型转换为基本数据类型;

28 接口和抽象类的区别是什么

  1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法

  2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定

  3. 一个类可以实现多个接口,但最多只能实现一个抽象类

  4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定

  5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范

29 成员变量与局部变量的区别有那些

  1. 从语法形式上看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰;

  2. 从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存

  3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。

  4. 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被 final 修饰的成员变量也必须显示地赋值);而局部变量则不会自动赋值。

29 静态方法和实例方法有何不同

  1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。

  2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制.

30 Java 中的异常处理

  Java 异常类层次结构图:

在这里插入图片描述

  在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable类。 Throwable: 有两个重要的子类: Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。Error(错误) :是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM( Java 虚拟机)出现的问题。

  例如, Java 虚拟机运行错误( Virtual MachineError),当JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时, Java 虚拟机( JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java 虚拟机运行错误( Virtual MachineError)、类定义错误( NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。

  Exception(异常) :是程序本身可以处理的异常。 Exception 类有一个重要的子类 RuntimeException。 RuntimeException 异常由 Java 虚拟机抛出。

  NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常)和ArrayIndexOutOfBoundsException (下标越界异常)。

  注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。

  Throwable 类常用方法

    public string getMessage():返回异常发生时的详细信息

     public string toString():返回异常发生时的简要描述

    public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同

    public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息
异常处理总结

 try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch块,则必须跟一个 finally 块。

 catch 块:用于处理 try 捕获到的异常。 finally 块:无论是否捕获或处理异常, finally 块里的语句都会被执行。

  当在 try 块或 catch 块中遇到 return 语句时, finally 语句块将在方法返回之前被执行。

  在以下 4 种特殊情况下, finally 块不会被执行:

    1. 在 finally 语句块中发生了异常。

    2. 在前面的代码中用了 System.exit()退出程序。

    3. 程序所在的线程死亡。

    4. 关闭 CPU。

31 SafePoint 是什么

  GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始执行 GC.

    1. 循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入safepoint)

    2. 方法返回前

    3. 调用方法的 call 之后

    4. 抛出异常的位置

32 类加载器、双亲委派机制

  Java中的类加载器采用的是双亲委派模型,这是一种保证 Java程序安全稳定的机制。

  当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。 当一个类需要被加载时,先由 AppClassLoader 加载,若无法找到,就交由 Extension ClassLoader 加载,若仍然无法找到,最后交由 BootStrap ClassLoader 加载。若 BootStrap ClassLoader 也无法加载,则会抛出 ClassNotFoundException。

  在双亲委派模型中,四种类加载器:

  BootStrap ClassLoader(启动类加载器):它是 Java类加载器的顶层,主要用来加载 Java的核心类库,如 rt.jar、resources.jar、charsets.jar等,它加载的路径是 JAVA_HOME/jre/lib目录,它是由 C++实现的,并不是一个Java类。

  Extension ClassLoader(扩展类加载器):它主要用来加载 Java的扩展库,默认加载的路径是 JAVA_HOME/jre/lib/ext目录。

  App ClassLoader(应用程序类加载器):它用来加载应用程序的类路径,一般是加载当前应用程序的classpath下的类。

  用户自定义类加载器:通过继承 java.lang.ClassLoader 类的方式实现。

  双亲委派模型的优势在于防止类的重复加载,保证了类的唯一性和一致性,从而保证了Java程序的安全性和稳定性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值