1.怎么理解跨平台
Java实现跨平台的关键在于其“一次编写,到处运行”的理念。Java程序通过将源代码编译为中间字节码(bytecode),而不是特定于某个平台的机器代码。这个字节码可以在任何安装了Java虚拟机(JVM)的平台上运行。
JVM充当了一个抽象层,负责将字节码翻译为特定平台的机器代码。因此,无论是在Windows、Linux还是其他支持Java的操作系统上,只需安装相应平台的JVM,就能够运行相同的Java程序。
这种设计使得Java程序具有很高的可移植性,因为不需要为每个平台编写不同的代码。这也为开发者提供了更简单、更灵活的开发和维护方式。
2.Java是编译执行还是解释执行
Java是一种同时支持编译执行和解释执行的语言。Java源代码首先被编译成字节码(bytecode),这是一种中间代码。然后,Java虚拟机(JVM)在运行时解释执行这些字节码,将其翻译成机器码,或者通过即时编译(Just-In-Time Compilation,JIT)技术将其编译成本地机器码,提高执行效率。
这种混合的执行方式带来了一些优势。首先,字节码的存在使得Java具有跨平台的特性,因为相同的字节码可以在任何支持Java虚拟机的平台上运行。其次,JIT编译可以在运行时将字节码优化成本地机器码,提高程序的执行效率。这种灵活性和性能的折中使得Java在各种应用场景中都有广泛的应用。
3.六大设计原则
六大设计原则通常是指面向对象设计中的 SOLID 原则,这是由罗伯特·C·马丁(Robert C. Martin)等人提出的一组设计准则,旨在创建更加可维护、灵活和可扩展的软件系统。这六个原则分别是:
-
单一职责原则(Single Responsibility Principle,SRP): 一个类应该只有一个引起变化的原因。换句话说,一个类应该只负责一项职责。
-
开放封闭原则(Open/Closed Principle,OCP): 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。可以通过扩展来增加系统的功能,而无需修改现有代码。
-
里氏替换原则(Liskov Substitution Principle,LSP): 所有引用基类的地方必须能够透明地使用其子类的对象,而且在不改变程序正确性的前提下,子类可以替换父类。
-
接口隔离原则(Interface Segregation Principle,ISP): 一个类不应该强迫其它的类使用它们不需要的方法。应该将接口分解为更小的、更具体的接口,以确保类只需实现其需要的方法。
-
依赖倒置原则(Dependency Inversion Principle,DIP): 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
-
迪米特法则(Law of Demeter,LoD): 一个对象应该对其他对象有最少的了解。一个类应该对自己需要耦合或调用的类知道得最少,也就是只与朋友交流,不与陌生人说话。
这些原则有助于构建灵活、可维护、可扩展的软件系统,促进了面向对象设计的良好实践。
4.面向对象的特征
面向对象编程(Object-Oriented Programming,OOP)具有以下主要特征:
-
封装(Encapsulation): 封装是将对象的状态(属性)和行为(方法)封装在一起,形成一个独立的单元。通过封装,对象的内部实现细节被隐藏,只对外提供有限的接口,提高了代码的模块化和安全性。
-
继承(Inheritance): 继承允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以复用父类的代码,并且可以在不修改父类的情况下扩展或修改其行为。继承提供了代码的重用性和层次性。
-
多态(Polymorphism): 多态允许对象以多种形态表现。它包括编译时多态(方法的重载)和运行时多态(方法的重写)。多态提高了代码的灵活性和可扩展性。
-
抽象(Abstraction): 抽象是将对象的共同特征抽取出来形成类,通过接口和抽象类定义规范。它隐藏了不必要的细节,使得对象的设计更为简化和高效。
这些特征共同构成了面向对象编程的基本原则,使得程序设计更加灵活、可维护、可扩展,并提高了代码的复用性。 OOP的思想是将现实世界的问题映射到程序设计中,使得软件更容易理解和维护。
5.类加载过程(生命周期)
Java类加载过程包括以下几个阶段:
-
加载(Loading): 加载是类加载过程的第一阶段,它负责查找并加载类的字节码文件。这个过程可以通过类加载器来完成。类加载器可以是系统提供的类加载器,也可以是用户自定义的类加载器。
-
验证(Verification): 在验证阶段,Java虚拟机会确保被加载的字节码是合法、符合规范的。这个阶段主要包括文件格式验证、元数据验证、字节码验证和符号引用验证。
-
准备(Preparation): 在准备阶段,Java虚拟机为类的静态变量分配内存并设置默认初始值。这个阶段不会涉及到具体的Java代码执行,而只是分配内存空间。
-
解析(Resolution): 解析阶段是将类、方法、字段等符号引用解析为直接引用的过程。这个过程可以在编译期间进行,也可以在运行期间动态链接。
-
初始化(Initialization): 在初始化阶段,才真正执行类中定义的Java程序代码。这个阶段是类加载过程的最后一个阶段,它负责执行类构造器<clinit>()方法,这个方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static blocks)中的语句合并而成。
这五个阶段统称为类加载的生命周期,其中加载、验证、准备和初始化这四个阶段是按顺序执行的,解析阶段可以在初始化阶段之前或之后执行。类加载过程中,如果在某一阶段出现问题,会抛出相应的异常。
6.类加载器
Java中的类加载器(Class Loader)是负责加载类文件并将其转换为运行时类的一种机制。类加载器主要有以下几种类型:
-
启动类加载器(Bootstrap Class Loader): 这是最顶层的类加载器,负责加载Java的核心类库,通常是由本地代码实现的,不是Java类。它是虚拟机的一部分,负责加载其他扩展类加载器和应用程序类加载器。
-
扩展类加载器(Extension Class Loader): 负责加载Java的扩展类库,一般位于JRE的
lib/ext
目录下。 -
应用程序类加载器(Application Class Loader): 也被称为系统类加载器,负责加载应用程序classpath下的类。它是ClassLoader类的getSystemClassLoader()方法的返回值。
-
自定义类加载器(Custom Class Loader): 开发人员可以根据需求自定义类加载器,继承ClassLoader类并覆写其中的方法。这样可以实现一些特殊的类加载需求,例如从网络或数据库加载类。
类加载器采用双亲委派模型,即每个类加载器在加载类时都会先委托给父加载器去尝试加载。只有当父加载器无法加载时,子加载器才会尝试加载。这种模型保证了类的一致性和避免了类的重复加载,提高了类加载的效率和安全性。
7.自定义classLoader
自定义ClassLoader的作用在于满足一些特殊的类加载需求,允许开发人员实现一些定制化的加载逻辑。以下是自定义ClassLoader的一些常见用途:
-
动态加载类: 允许在运行时从不同的来源加载类,例如从网络、数据库或远程服务器。这对于实现插件系统或动态模块加载很有用。
-
热部署: 通过定制ClassLoader,可以在应用程序运行时替换或更新类,实现热部署的功能,无需重启应用。
-
加密/解密类: 自定义ClassLoader可以用于加载经过加密的类文件,实现类的加密保护,只有在运行时进行解密后才能使用。
-
类版本控制: 在一些场景中,可能需要控制特定类的版本。自定义ClassLoader可以根据版本号来加载类,允许在不同的时间加载不同版本的类。
-
加载非标准位置的类: 有时,类文件并不在标准的classpath路径下,例如从数据库中读取类定义,自定义ClassLoader可以用于从这些非标准位置加载类。
-
资源定制: 通过自定义ClassLoader,可以实现对类的资源文件进行定制,例如改变配置文件的加载逻辑或加载特定版本的资源文件。
-
保护类隔离: 自定义ClassLoader可以实现类的隔离,确保一些类只能被特定的ClassLoader加载,从而实现一定程度的安全隔离。
需要注意的是,自定义ClassLoader需要谨慎处理类加载的委托关系、类加载的生命周期以及父子ClassLoader的关系,以避免潜在的问题。在绝大多数情况下,使用标准的ClassLoader已经能够满足应用程序的需求,自定义ClassLoader通常用于解决一些特殊场景下的需求。
8.JVM内存模型
Java虚拟机(JVM)内存模型主要分为以下几个部分:
-
方法区(Method Area): 用于存储类的结构信息、静态变量、常量,以及编译器生成的其他静态方法和代码块。
-
堆(Heap): 用于存储对象实例。堆是Java程序中动态分配内存的地方,包括新创建的对象和由于垃圾回收而释放的内存空间。
-
栈(Stack): 存储线程执行方法时的局部变量、操作数栈、方法出口等。每个线程都有自己的栈,用于跟踪方法的执行情况。
-
本地方法栈(Native Method Stack): 主要用于执行本地方法(用其他语言编写的方法)。与Java方法栈类似,但是它执行的是本地代码,而不是Java代码。
-
程序计数器(Program Counter Register): 记录当前线程执行的字节码行号,用于支持线程切换和恢复。
-
直接内存(Direct Memory): 不是JVM内部的一部分,但是被视为一种重要的内存区域。主要是通过
ByteBuffer
等类进行直接内存访问,不受JVM垃圾回收管理,需要手动释放。
这些内存区域的组织和作用保证了Java程序的安全性和高度可移植性。垃圾回收主要针对堆内存,而栈和程序计数器等内存区域随线程的创建和销毁而动态分配和释放。
9.垃圾回收算法
Java垃圾回收是自动管理内存的机制,它负责释放不再被程序引用的对象,从而防止内存泄漏和提高程序性能。Java虚拟机(JVM)实现垃圾回收的算法主要有以下几种:
-
标记-清除算法(Mark and Sweep): 这是最基本的垃圾回收算法。它分为两个阶段,首先标记出所有需要回收的对象,然后清除这些对象。缺点是会产生内存碎片,影响内存利用率。
-
复制算法(Copying): 将内存空间分为两块,每次只使用其中一块。当这一块内存满了,就将存活的对象复制到另一块,然后清空原有内存块。优点是减少了内存碎片,但是需要额外的内存空间。
-
标记-整理算法(Mark and Compact): 类似于标记-清除算法,但在标记阶段后,会将存活的对象向一端移动,然后清理掉边界外的内存。减少了内存碎片。
-
分代算法(Generational): 根据对象的生命周期将堆分为新生代和老年代。新生代中的对象生命周期较短,采用复制算法;老年代中的对象生命周期较长,采用标记-整理算法。这样分代的方式可以根据不同的垃圾回收算法适应对象的不同特性,提高回收效率。
-
增量式算法(Incremental): 将垃圾回收过程划分为多个步骤,每次执行其中的一步。这样可以在垃圾回收的同时,减少对应用程序的影响,提高响应速度。
-
并行算法(Parallel): 利用多个处理器同时进行垃圾回收,加快回收速度。在多核处理器环境下,可以通过并行垃圾回收提高性能。
Java的垃圾回收器通常根据应用程序的性质和要求,选择不同的垃圾回收算法组合。例如,Java HotSpot虚拟机使用了分代垃圾回收算法,包括新生代的Parallel Scavenge收集器和老年代的CMS(Concurrent Mark-Sweep)收集器等。
10.垃圾回收器
Java中有多种垃圾回收器,它们的选择取决于应用程序的需求、内存使用模式以及性能要求。以下是一些常见的垃圾回收器:
-
Serial收集器(Serial Garbage Collector):单线程收集器,适用于单核处理器环境。在新生代使用标记-复制算法,老年代使用标记-整理算法。
-
Parallel收集器(Parallel Garbage Collector):也称为吞吐量收集器,多线程收集器,适用于多核处理器环境。在新生代使用标记-复制算法,老年代使用标记-整理算法。
-
CMS收集器(Concurrent Mark-Sweep Garbage Collector):以减少垃圾回收停顿时间为目标。在新生代使用标记-复制算法,老年代使用标记-清理-整理算法。
-
G1收集器(Garbage First Garbage Collector):适用于大内存和多核处理器环境。将堆划分为多个区域,可并发地进行垃圾回收。采用标记-整理算法。
-
ZGC(Z Garbage Collector):以低延迟为目标,适用于大堆内存和多核处理器环境。使用并发的标记-整理算法。
-
Shenandoah收集器:以极低的停顿时间为目标。使用并发的标记-整理算法。
-
Epsilon收集器:一种实验性的收集器,主要用于性能测试,不进行垃圾回收。
-
Serial Old收集器:用于老年代,与Serial新生代收集器搭配使用。
-
Parallel Old收集器:用于老年代,与Parallel新生代收集器搭配使用。
11.怎么标记对象需要回收
在Java中,垃圾回收器通过标记对象的存活状态来确定哪些对象需要被回收。以下是垃圾回收器标记对象需要回收的一般过程:
-
引用计数法:
- 给每个对象关联一个引用计数器,每当有一个地方引用它时,计数加1;引用失效时,计数减1。当计数为零时,说明对象不再被引用,可以被回收。
- 这种方法简单,但不能处理循环引用的情况。
-
可达性分析:
- Java的主流垃圾回收器使用的是可达性分析。从GC Roots(根集)出发,通过一系列引用关系,标记出所有被引用的对象,剩下的即为不可达的对象,它们将被回收。
- GC Roots包括线程栈中的本地变量、静态变量、常量池中的引用等。
-
分代回收:
- 将堆内存划分为不同的代,如新生代和老年代。新创建的对象首先放入新生代,经过几次垃圾回收后,如果仍然存活,会被晋升到老年代。
- 针对不同代采用不同的垃圾回收策略。
-
弱引用、软引用、虚引用:
- 使用这些引用类型可以影响对象的可达性,当对象只被弱引用、软引用、虚引用引用时,垃圾回收器可能更容易将其标记为可回收的。
总体而言,垃圾回收器通过可达性分析来确定对象是否可达,从而决定是否需要被回收。这种自动的垃圾回收机制减轻了开发人员手动释放内存的负担,同时确保了程序的内存安全性。
12.怎么解决频繁GC
频繁的垃圾回收(GC)可能会对Java应用程序的性能产生负面影响。为了解决频繁GC的问题,可以采取以下一些措施:
-
选择合适的垃圾回收器:Java提供了多种垃圾回收器,每个回收器都有不同的特点和适用场景。根据应用程序的特性和性能需求,选择合适的垃圾回收器进行配置。
-
调整堆大小:通过调整堆大小,可以影响垃圾回收的频率和效率。合理设置新生代和老年代的大小,以及最大堆和初始堆大小,可以减少垃圾回收的次数和停顿时间。
-
合理使用对象池:对象池可以帮助减少对象的创建和销毁,从而减轻垃圾回收的压力。通过复用对象,尽量减少对象的瞬时分配,可以降低垃圾回收的频率。
-
优化代码,减少对象的生命周期:优化代码结构,及时释放不再需要的对象引用,确保对象能够在不需要时被及时回收。避免创建大量临时对象,尽量重用对象,减少垃圾的产生。
-
使用并发垃圾回收器:并发垃圾回收器能够在垃圾回收的同时继续执行应用程序的部分任务,从而减小停顿时间。G1、ZGC和Shenandoah是一些支持并发垃圾回收的收集器。
-
分析GC日志,进行调优:监控和分析应用程序的GC日志,了解垃圾回收的情况。根据分析结果,调整垃圾回收器的选择和配置参数,以优化性能。
-
升级JVM版本:随着Java的版本更新,JVM通常会对垃圾回收器进行改进和优化。升级到较新的JVM版本可能会带来性能的提升。
通过综合考虑和实践,可以有效地减少频繁GC对Java应用程序性能的影响。不同的应用场景可能需要不同的调优策略,因此需要根据具体情况进行调整。
13.java new对象做了哪些事情
当在Java中使用 new
关键字创建一个对象时,发生了以下几个关键步骤:
-
分配内存空间: 首先,根据类的定义,分配足够的内存空间来存储对象的实例变量。这是在堆内存中完成的。
-
初始化实例变量: 在内存中分配空间后,Java会调用类的构造方法(如果存在)来初始化对象的实例变量。构造方法负责确保对象在创建时具有合适的初始状态。
-
设置对象的引用: 返回的对象引用被赋给变量(或者被用作参数传递给其他方法)。这使得程序能够通过该引用访问和操作新创建的对象。
这三个步骤确保了对象的正确创建和初始化。需要注意的是,Java中的对象创建和内存管理是由Java虚拟机(JVM)负责的。在程序中,开发人员主要关注如何使用这些对象,而不必亲自管理对象的内存分配和释放。这有助于避免许多常见的内存错误。
14.Java为什么是值传递
ava 之所以被描述为按值传递,是因为在方法调用时,传递给方法的是实际参数的值,而不是参数本身。这意味着在方法内部,对于基本数据类型,对形参的修改不会影响到方法外部的变量;而对于引用类型,对引用的修改不会影响引用指向的对象。
这种按值传递的设计有几个原因:
-
简单性: 按值传递简化了编程模型。调用方不需要担心传递给方法的参数是否会被修改,因为参数的值是传递的副本。
-
可预测性: 按值传递使得程序的行为更加可预测。在方法调用中,我们可以清楚地知道传递给方法的是什么值,而无需担心在方法内部会发生什么未知的修改。
-
线程安全性: 这样的设计有助于保持线程安全。如果在方法调用中传递了引用,并在方法内部修改了引用指向的对象,这可能导致并发问题。按值传递可以降低这类问题的风险。
需要注意的是,虽然按值传递,但对于引用类型,传递的是引用的值,也就是对象的地址。因此,通过引用可以访问和修改对象的状态。这种语言设计上的选择可能会导致一些混淆,因此在理解 Java 的参数传递时,理解按值传递的概念是很重要的。
15.Java对象头都存了哪些东西
ava对象头是对象在堆内存中的开头部分,它包含了一些用于管理对象的元信息。对象头的内容可以包括以下信息:
-
标记字(Mark Word): 标记字用于存储对象的状态信息,例如是否被锁定、是否是偏向锁等。它通常占据对象头的一部分,其具体结构取决于对象的锁状态。
-
类指针(Class Pointer): 类指针指向对象的类元数据,即
Class
对象。通过类指针,JVM能够确定对象的类型,从而正确地执行方法调用和字段访问。 -
数组长度(Array Length): 对于数组对象,对象头还可能包含数组的长度信息。这使得JVM能够快速访问数组的长度而无需遍历整个数组。
这些信息共同构成了对象头,起到了管理和支持Java对象的重要作用。需要注意的是,对象头的结构在不同的JVM实现中可能有所不同,上述内容是一般情况下的常见元素。对象头的设计旨在支持Java内存模型,确保线程安全和高效地管理对象。
16.String的不可变性
在Java中,String
是一个类,用来表示字符串。以下是关于Java String
的一些重要概念:
-
不可变性(Immutable): 字符串对象一旦创建,就不能被修改。任何对字符串的操作都会生成一个新的字符串对象,而原始字符串保持不变。这确保了字符串的安全性和线程安全性。
-
字符串池(String Pool): Java中的字符串池是一种特殊的内存区域,用于存储字符串字面值。当创建字符串时,如果字符串池中已存在相同值的字符串,则返回对池中字符串的引用,而不会创建新对象。这有助于节省内存和提高效率。
17. String、StringBuffer、StringBuilder
String
、StringBuilder
和 StringBuffer
是 Java 中用于处理字符串的三个主要类,它们之间有一些关键的区别:
-
可变性:
String
是不可变的,一旦创建就不能被修改。任何对字符串的操作都会生成一个新的字符串对象。StringBuilder
和StringBuffer
是可变的,允许修改字符串内容。在频繁操作字符串时,使用这两者通常更高效,因为避免了不断创建新的字符串对象。
-
线程安全性:
String
是线程安全的,因为它是不可变的。多个线程可以安全地共享相同的字符串实例。StringBuilder
不是线程安全的,适用于单线程环境。它的操作不是同步的。StringBuffer
是线程安全的,通过同步来保证多线程环境下的安全性。但是,由于同步开销,通常在单线程环境下使用StringBuilder
更为高效。
-
性能:
- 在单线程环境下,
StringBuilder
的性能通常比StringBuffer
更好,因为它不涉及到同步操作。 StringBuffer
在多线程环境下提供了线程安全的操作,但因为同步开销,性能可能相对较低。
- 在单线程环境下,
-
使用场景:
- 如果字符串是固定的,不需要修改,使用
String
是合适的。 - 如果字符串需要频繁修改且在单线程环境下,使用
StringBuilder
更为合适。 - 如果字符串需要频繁修改且在多线程环境下,使用
StringBuffer
以确保线程安全。
- 如果字符串是固定的,不需要修改,使用
18. ArryList和LinkedList
ArrayList
和 LinkedList
都是Java集合框架中的List实现,它们有各自的优势和适用场景。
ArrayList:
-
底层数据结构: 基于动态数组实现。它的内部使用一个数组来存储元素,当数组空间不足时,自动增长数组的大小。
-
访问速度: 由于底层是数组,
ArrayList
支持随机访问,因此通过索引直接访问元素的速度很快。时间复杂度为 O(1)。 -
插入和删除: 在中间或开头插入/删除元素时,可能需要移动其他元素,因此效率较低。时间复杂度为 O(n)。
-
空间使用: 由于是动态数组,可能会分配一些额外的空间,因此相对于
LinkedList
来说,它的空间效率稍差。
LinkedList:
-
底层数据结构: 基于双向链表实现。每个元素都包含对前一个和后一个元素的引用。
-
访问速度: 在
LinkedList
中,随机访问的效率较低,因为需要从头或尾部开始遍历链表。时间复杂度为 O(n)。 -
插入和删除: 在中间或开头插入/删除元素时,由于只需要修改相邻元素的引用,效率较高。时间复杂度为 O(1)。
-
空间使用:
LinkedList
没有像ArrayList
那样的预留空间,因此在一些情况下可能更节省空间。
如何选择:
- 如果需要频繁随机访问元素,并且集合中的元素数量相对固定,推荐使用
ArrayList
。 - 如果需要频繁执行插入和删除操作,并且对随机访问的性能要求较低,可以考虑使用
LinkedList
。 - 在不确定如何选择时,可以根据具体的使用场景进行性能测试和分析,以确定最适合的集合类型。
19.HashSet和HashTable
HashSet
和 Hashtable
是 Java 集合框架中两个不同的类,它们有一些关键的区别:
HashSet:
-
底层数据结构:
HashSet
基于哈希表实现,内部使用HashMap
来存储元素。 -
允许 null 元素:
HashSet
允许插入一个null
元素。 -
不保证顺序:
HashSet
不保证元素的顺序,即不保证元素的存储和遍历顺序一致。 -
非线程安全:
HashSet
不是线程安全的,如果多个线程同时访问一个HashSet
实例,并且至少有一个线程修改了集合,必须在外部进行同步。
Hashtable:
-
底层数据结构:
Hashtable
也是基于哈希表实现的,内部使用数组加链表的结构。 -
不允许 null 键或值:
Hashtable
不允许插入null
键或值。如果尝试插入null
,会抛出NullPointerException
。 -
保证顺序: 从 Java 8 开始,
Hashtable
在遍历时也会按照插入的顺序进行。 -
线程安全:
Hashtable
是线程安全的。所有的方法都是同步的,可以在多线程环境中安全地使用。
如何选择:
- 如果不需要线程安全性,且可以接受元素顺序不确定,通常使用
HashSet
。 - 如果需要线程安全性或者需要保持元素的插入顺序,可以考虑使用
Hashtable
或Collections.synchronizedMap(new HashMap())
。 - 对于新的代码,更推荐使用
HashMap
或LinkedHashMap
替代Hashtable
,以及使用HashSet
替代Hashtable
。
注意:由于 Hashtable
是早期的集合实现,在现代 Java 中,更推荐使用 HashMap
和相关的实现,而不是 Hashtable
。
20.HashMap的原理
HashMap
是 Java 集合框架中的一个重要类,它基于哈希表实现,用于存储键值对。以下是 HashMap
的基本原理:
哈希函数:HashMap
使用哈希函数将键映射到哈希表中的索引位置。这个哈希函数的目标是使得元素均匀分布在哈希表的各个位置,减少冲突。Java 8 中对哈希算法进行了改进,以减少哈希冲突的发生。这有助于分布更均匀地存储键值对,提高 HashMap
的性能。在 Java 8 中,当进行哈希计算时,会预先计算哈希码的高16位,然后与低16位进行异或操作。这种方式可以提高在哈希冲突发生时,更好地分散键值对。
存储结构:哈希表是一个数组,每个元素通常称为桶(bucket)。每个桶可能包含一个或多个键值对。
冲突处理:如果两个不同的键经过哈希函数映射到了同一个位置,就发生了冲突。HashMap
使用链表或红黑树来处理冲突,即在同一个桶中存储一个链表或树结构。
扩容:当 HashMap
中的元素个数达到容量的某个阈值时,会触发扩容操作。扩容会创建一个新的更大的数组,并将所有键值对重新分配到新的桶中,以减少冲突。在 Java 8 中,HashMap
使用了树化机制,即将链表在一定长度(默认为8)以上的情况下转换为红黑树。这样可以在查找、插入和删除时,将时间复杂度从 O(n) 降低到 O(log n)。反之,如果树中的节点数量减少到一定程度,红黑树将被还原为链表,以避免树结构的额外开销。
初始容量和负载因子:HashMap
可以通过构造函数指定初始容量和负载因子。初始容量是哈希表的大小,负载因子是哈希表在扩容前可以达到的平均填充比例。负载因子越小,哈希表的装填程度越低,冲突的可能性越小,但会导致哈希表占用更多的内存。
get 操作:对于 get
操作,HashMap
首先计算键的哈希码,然后根据哈希码找到桶的位置。如果桶中有一个或多个键值对,就遍历链表或树来找到相应的键。
put 操作:对于 put
操作,首先计算键的哈希码,然后找到桶的位置。如果桶为空,直接插入;如果桶非空,可能存在冲突,需要在链表或树中进行插入。在插入之后,如果超过负载因子阈值,就进行扩容。
HashMap
的时间复杂度取决于哈希函数的均匀性和冲突处理的效率。在理想情况下,get
和 put
操作的平均时间复杂度都是 O(1)。在较差的情况下,如果发生冲突,时间复杂度可能升高到 O(n)。因此,选择适当的初始容量和负载因子,以及实现高效的哈希函数,对 HashMap
的性能至关重要。
21.ConcurrentHashMap的原理
ConcurrentHashMap
是 Java 并发包中提供的线程安全的哈希表实现,用于在多线程环境中安全地操作和管理键值对。下面是 ConcurrentHashMap
的一些主要原理:
分段锁(Segmentation):
ConcurrentHashMap
内部维护了一个数组,称为segments
或bins
,它实际上是一个数组的数组。每个segment
是一个独立的哈希表,拥有自己的锁。这样,锁的粒度被缩小,不同的线程可以同时访问不同的segment
,从而提高并发度。- 在 Java 8 中,
ConcurrentHashMap
不再使用传统的分段锁实现,而是引入了基于 CAS 操作的分段锁。这种锁更适用于高并发情况,减少了锁的争用,提高了并发性能。
安全的扩容机制:
- 与普通的哈希表不同,
ConcurrentHashMap
在进行扩容时,只需要锁住被扩容的segment
,而不是整个表。这就使得扩容的过程对于其他未被扩容的segment
是可见的。 - 类似于
HashMap
,Java 8 中的ConcurrentHashMap
也对链表过长的桶进行了优化,将链表转换为红黑树。这有助于在链表较长时提高查找、插入和删除的性能
put
操作的过程:
- 计算哈希值。
- 根据哈希值定位到具体的
segment
。 - 在该
segment
中加锁,保证线程安全。 - 在
segment
中执行put
操作,可能触发扩容。 - 释放
segment
的锁。
get
操作的过程:
- 计算哈希值。
- 根据哈希值定位到具体的
segment
。 - 在该
segment
中查找,无需加锁。 - 返回结果。
支持原子性操作:
ConcurrentHashMap
提供了一些原子性的操作,例如putIfAbsent
、remove
和replace
,这些操作在一个方法调用中完成,而不需要额外的锁。
适应性自旋锁:
ConcurrentHashMap
中使用了适应性自旋锁,这意味着它会动态地调整自旋的次数,以适应当前系统的负载。这有助于在不同负载下获得更好的性能。
总体而言,ConcurrentHashMap
的设计通过分段锁和其他优化,实现了在多线程环境中的高并发性能,并且保持了线程安全。这使得它成为处理并发读写的哈希表的理想选择。
22.java的锁
在Java中,有多种锁的实现用于控制多线程对共享资源的访问。以下是一些常见的锁:
内置锁(Intrinsic Locks):
a. Synchronized关键字:
- 使用
synchronized
关键字,可以对代码块或方法进行加锁。在方法或代码块上添加synchronized
关键字,确保同一时刻只有一个线程能够执行该代码块或方法。
b. ReentrantLock:
ReentrantLock
是显示锁,提供了与synchronized
类似的同步功能,但是具备更灵活的操作方式。可以显式地获取和释放锁,支持可重入、超时、中断等特性。
c. ReentrantReadWriteLock:
ReentrantReadWriteLock
是ReentrantLock
的扩展,提供了读写锁。多个线程可以同时读取共享资源,但在写入时必须互斥。
显示锁(Explicit Locks):
a. Lock接口:
Lock
接口是 Java 并发包中的一部分,定义了锁的基本操作。ReentrantLock
是Lock
接口的一种实现。
b. ReadWriteLock接口:
ReadWriteLock
接口扩展了Lock
接口,提供了读写锁的功能。ReentrantReadWriteLock
是ReadWriteLock
接口的一种实现。
其他锁:
a. StampedLock:
StampedLock
是 Java 8 中引入的新型锁,支持读锁、写锁以及乐观读。它在某些场景下比传统的读写锁性能更好。
更多锁的内容见java各类锁的理解-CSDN博客
23.volatile关键字
volatile
是Java关键字之一,用于修饰变量,主要用于多线程编程。使用 volatile
关键字修饰的变量具有以下特性:
-
可见性(Visibility): 当一个线程修改了
volatile
变量的值,这个新值对于其他线程是可见的。这是因为volatile
会保证所有线程看到的变量值都是最新的。 -
禁止指令重排序(Atomicity):
volatile
变量的读写操作具有原子性。这意味着线程在读取volatile
变量时,会禁止之前的所有指令重排序,确保读取的是最新的值。对volatile
变量的写操作同样保证了禁止后续的所有指令重排序。
尽管 volatile
提供了可见性和禁止指令重排序的特性,但它并不能保证复合操作的原子性。例如,递增操作 count++
不是一个原子操作,因此在多线程环境中使用 volatile
并不能保证线程安全。
24.Automic原子类的原理
Java中的java.util.concurrent.atomic
包提供了一系列的原子类,用于在多线程环境中进行原子操作。这些原子类基于底层的硬件原语(如CAS指令)实现,确保了某些操作的原子性。以下是Atomic
原子类的一些常见原理:
CAS(Compare-And-Swap)操作:CAS 是一种并发算法,用于实现多线程环境下的原子操作。它通过比较内存中的值与预期值,如果相等就将新值写入内存。整个过程是原子的,不存在中途被其他线程干扰的可能。
内存屏障(Memory Barriers):内存屏障是为了保证内存操作的有序性。在多核处理器架构下,不同核的缓存可能会导致线程间的数据不一致。内存屏障通过禁止或强制特定类型的内存操作顺序,保证了操作的有序性。
Volatile关键字:一些Atomic
原子类使用了volatile
关键字,确保了变量的可见性。当一个线程修改了volatile
变量的值,这个新值对于其他线程是可见的。
Unsafe类:Unsafe
是一个提供了底层内存操作的类,它允许直接操作内存,执行CAS等操作。尽管名为 "Unsafe",但它在java.util.concurrent.atomic
包中被合理地使用来实现原子操作。
ABA问题的解决:ABA问题指的是一个值在被修改之前是A,在修改后又恢复成A。AtomicStampedReference
和 AtomicMarkableReference
是Atomic
原子类中专门用于解决ABA问题的类,它们在原子操作的基础上引入了版本号或标记,以便在比较和交换时检测到是否发生了ABA。
这些原理保证了Atomic
原子类的操作是线程安全的,可以在多线程环境下使用而不需要显式加锁。它们提供了一种高效而可靠的方式来执行一些常见的原子性操作,例如递增、递减、交换值等。
25.进程和线程的区别
进程和线程是操作系统中的两个重要概念。进程是程序的执行实例,而线程是进程中的可执行单元。
-
定义:
- 进程是一个独立的执行环境,包含程序、数据和系统资源。它是系统分配资源的基本单位。
- 线程是进程内的一个独立执行单元,共享进程的资源,包括内存空间和文件句柄。
-
独立性:
- 进程是独立的,每个进程有自己的地址空间,数据栈,和文件描述符等。
- 线程是进程内的轻量级执行单元,多个线程共享同一地址空间和其他资源。
-
通信和同步:
- 进程之间通信相对复杂,通常需要使用进程间通信(IPC)机制。
- 线程之间共享同一进程的数据,通信更方便。但也需要同步机制,以防止数据访问冲突。
-
资源开销:
- 进程拥有独立的资源,创建、撤销的开销相对较大。
- 线程共享进程的资源,创建、撤销的开销较小。
-
容错性:
- 进程有自己的地址空间,一个进程的崩溃通常不会影响其他进程。
- 线程共享进程的地址空间,一个线程的错误可能导致整个进程崩溃。
总体而言,线程在相同的进程内共享资源,因此线程间通信更为简便,但需要更加谨慎地处理同步问题。进程则更为独立,但资源开销相对较大。选择使用进程还是线程通常取决于具体应用场景和需求。
25.线程的几种状态
线程在其生命周期中可以处于不同的状态,通常包括以下几种状态:
-
新建(New): 线程被创建但尚未启动执行。
-
就绪(Runnable/Ready): 线程已经被创建并且已经启动,但它还没有开始执行。线程处于就绪状态,等待系统分配处理器资源。
-
运行(Running): 线程获得了处理器资源并正在执行任务。
-
阻塞(Blocked/Waiting): 线程被阻塞,通常是因为等待某个条件的发生(如I/O操作、锁的获取等)。在这个状态下,线程不会占用CPU时间。
-
等待(Waiting): 线程在某个对象上等待,通常是等待其他线程的通知或中断。与阻塞状态不同,等待状态需要其他线程显式地通知或中断。
-
超时等待(Timed Waiting): 类似于等待状态,但在等待一段时间后会自动恢复到就绪状态。例如,使用了带有超时参数的等待操作。
-
终止(Terminated): 线程执行完成,或者因为异常而提前结束,进入终止状态。
线程在这些状态之间转换,具体转换取决于线程的执行和外部条件的变化。理解线程的生命周期和状态有助于合理地管理线程,并避免潜在的并发问题。
26.创建线程的方法
继承Thread类、实现Runnable接口、实现Collable接口(有返回值)
27.线程池的原理
Java线程池的原理主要涉及到以下几个关键组件和概念:
-
ThreadPoolExecutor 类:
ThreadPoolExecutor
是 Java 线程池的核心实现类,它实现了ExecutorService
接口。- 通过构造函数,可以配置线程池的核心线程数、最大线程数、任务队列、线程空闲时间等参数。
-
任务队列(BlockingQueue):
- 线程池通过任务队列存储待执行的任务。常用的队列类型有
LinkedBlockingQueue
、ArrayBlockingQueue
等。 - 当线程池中的线程数达到核心线程数时,新的任务会被放入任务队列。
- 线程池通过任务队列存储待执行的任务。常用的队列类型有
-
线程池状态:
- 线程池有不同的状态,如 RUNNING、SHUTDOWN、STOP、TERMINATED 等,用于表示线程池的运行状态。
- 状态的变化通常由线程池的生命周期方法和内部控制机制引起。
-
线程工厂(ThreadFactory):
- 用于创建新线程的工厂接口。通过指定线程工厂,可以自定义线程的创建过程,比如设置线程的命名规则等。
-
拒绝策略(RejectedExecutionHandler):
- 当任务无法被线程池执行时,会根据指定的拒绝策略来处理。常见的拒绝策略有抛出异常、丢弃任务、丢弃最旧的任务等。
线程池的工作流程大致如下:
- 当有新任务提交时,线程池首先检查当前运行的线程数是否达到核心线程数。如果没有,则创建新的线程执行任务。
- 如果已经达到核心线程数,将任务放入任务队列,等待执行。
- 如果任务队列已满,且当前运行的线程数未达到最大线程数,创建新线程执行任务。
- 如果线程池已经达到最大线程数,采用拒绝策略来处理任务。
- 当线程池关闭时,不再接受新任务,同时等待正在执行的任务和队列中的任务执行完成。
线程池的优势在于提高了线程的复用性、降低了线程创建和销毁的开销,并通过合理配置参数,能够更好地控制系统的并发度。在实际应用中,通过调整线程池的参数和选择合适的队列类型,可以优化系统的性能。
更多线程池的内容见:java线程池-CSDN博客
28.ThreadLocal的原理
ThreadLocal
是 Java 中一个用于提供线程局部变量的类。每个线程都可以通过 ThreadLocal
创建一个独立的、线程本地的变量。其原理主要涉及以下几个关键点:
-
底层数据结构:
ThreadLocal
使用一个特殊的数据结构来存储每个线程的变量,这个数据结构是ThreadLocalMap
。ThreadLocalMap
是一个自定义的哈希表,它的键是ThreadLocal
对象,值是对应线程的变量值。
-
ThreadLocalMap:
- 每个线程都有一个独立的
ThreadLocalMap
对象,用于存储该线程所有使用ThreadLocal
创建的变量。 - 当使用
ThreadLocal
的set()
方法设置变量时,实际是在当前线程的ThreadLocalMap
中以当前ThreadLocal
对象为键存储变量。 - 使用
ThreadLocal
的get()
方法获取变量时,同样是从当前线程的ThreadLocalMap
中查找对应的值。
- 每个线程都有一个独立的
-
解决线程安全问题:
- 由于每个线程都有独立的
ThreadLocalMap
,线程之间不会直接冲突。 ThreadLocal
的实现通过空间换时间的方式,避免了使用同步机制,因此能够提高性能。
- 由于每个线程都有独立的
-
内存泄漏问题:
- 使用
ThreadLocal
时,需要注意防止内存泄漏。当线程结束后,如果ThreadLocal
没有被清理,对应的变量仍然存在于ThreadLocalMap
中。 - 为了避免这个问题,通常在使用完
ThreadLocal
后,应该调用remove()
方法清理。
- 使用
需要注意,ThreadLocal
主要用于解决线程范围内的变量共享问题,不应该被滥用。
29.Java I/O分类
Java I/O(输入/输出)主要分为两大类:字节流(Byte Streams)和字符流(Character Streams)。这两类流分别用于处理二进制数据和文本数据。每一类又分为输入流和输出流,形成四个基本的 I/O 抽象类。
-
字节流(Byte Streams):
- 输入流:
InputStream
是所有字节输入流的父类,提供了读取字节的方法。 - 输出流:
OutputStream
是所有字节输出流的父类,提供了写入字节的方法。
主要的实现类包括:
FileInputStream
和FileOutputStream
:用于读写文件。ByteArrayInputStream
和ByteArrayOutputStream
:用于读写字节数组。BufferedInputStream
和BufferedOutputStream
:提供缓冲功能,提高读写性能。
- 输入流:
-
字符流(Character Streams):
- 输入流:
Reader
是所有字符输入流的父类,提供了读取字符的方法。 - 输出流:
Writer
是所有字符输出流的父类,提供了写入字符的方法。
主要的实现类包括:
FileReader
和FileWriter
:用于读写文件中的字符数据。CharArrayReader
和CharArrayWriter
:用于读写字符数组。BufferedReader
和BufferedWriter
:提供缓冲功能,提高读写性能。
- 输入流:
这些流的层次结构使得 Java I/O 提供了一种灵活、可扩展的方式来处理输入和输出。在选择使用字节流还是字符流时,主要考虑的是处理的数据是二进制数据还是文本数据。字节流适用于二进制数据,而字符流适用于文本数据,因为它们能够正确处理字符编码,而不仅仅是字节的原始形式。
30.Java I/O模型
Java I/O 模型描述了程序与外部输入/输出资源(例如文件、网络)之间的交互方式。主要有三种 I/O 模型:同步阻塞 I/O、同步非阻塞 I/O、以及异步 I/O。
-
同步阻塞 I/O 模型(Blocking I/O):
- 同步(Synchronous): 意味着当应用程序执行 I/O 操作时,会等待直到操作完成。
- 阻塞(Blocking): 意味着当应用程序执行 I/O 操作时,线程会被阻塞,无法执行其他任务。
在同步阻塞 I/O 模型中,读写操作会阻塞当前线程,直到数据准备就绪或写入成功。这是最常见的 I/O 模型,但在高并发的情况下可能会导致性能问题,因为线程可能会长时间地等待。
-
同步非阻塞 I/O 模型(Non-blocking I/O):
- 同步(Synchronous): 仍然是在应用程序执行 I/O 操作时等待操作完成。
- 非阻塞(Non-blocking): 在等待 I/O 操作完成的同时,线程可以继续执行其他任务。
同步非阻塞 I/O 使用非阻塞调用,线程在等待 I/O 操作完成的时候不会被阻塞,可以执行其他任务。但是,仍然需要轮询来检查 I/O 操作是否就绪,这可能导致 CPU 资源的浪费。
-
异步 I/O 模型(Asynchronous I/O):
- 异步(Asynchronous): 意味着应用程序可以继续执行其他任务,而无需等待 I/O 操作完成。
- 非阻塞(Non-blocking): 与同步非阻塞 I/O 一样,线程在等待 I/O 操作完成的同时不会被阻塞。
异步 I/O 模型通过回调函数或事件通知的方式,通知应用程序 I/O 操作的完成,从而避免了轮询的问题。这种模型通常在高并发、高吞吐量的应用中表现良好,但实现相对复杂。
Java 在 NIO(New I/O)中引入了非阻塞 I/O,提供了 java.nio
包,包括 Selector
、Channel
等类,支持同步非阻塞和异步 I/O。 Java 7 引入的 NIO.2(Java 7 File I/O)提供了更多的异步 I/O 支持。在 Java 中,通常根据应用程序的性能需求和复杂度来选择适当的 I/O 模型。
更对I/O见:网络I/O介绍-CSDN博客
31.Java零拷贝
零拷贝(Zero-Copy)是一种优化技术,旨在减少数据在系统内存和应用程序之间的复制次数,提高数据传输效率。在 Java 中,有一些机制和类库支持零拷贝的实现。
-
FileChannel.transferTo()
和FileChannel.transferFrom()
:FileChannel
类提供了transferTo()
和transferFrom()
方法,可以直接在通道之间传输数据,而无需通过中间缓冲区。- 这两个方法可以在文件通道之间、Socket 通道之间进行直接的数据传输。
FileChannel sourceChannel = new FileInputStream("source.txt").getChannel(); FileChannel destinationChannel = new FileOutputStream("destination.txt").getChannel(); long transferred = sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
-
DirectByteBuffer
:- 使用
ByteBuffer
时,可以使用DirectByteBuffer
,它使用直接内存而不是 Java 堆内存。 - 直接内存的数据不受 Java 堆垃圾回收的管理,适用于零拷贝操作。
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
- 使用
-
Java NIO(New I/O):
- Java NIO 提供了
FileChannel
、SocketChannel
、DatagramChannel
等通道,这些通道支持零拷贝操作。 - 可以使用
FileChannel
将文件内容直接映射到内存,而不需要将整个文件内容复制到 Java 堆中。
FileChannel fileChannel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ); MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
- Java NIO 提供了
零拷贝技术通常用于大规模数据传输场景,例如文件传输、网络传输等。使用零拷贝可以减少不必要的数据复制,提高系统性能。在实际应用中,需要根据具体场景和需求来选择合适的零拷贝技术
更对零拷贝内容见:零拷贝的理解-CSDN博客
32.Java反射原理
Java 反射是指在运行时检查、获取和操作类的信息的机制。通过反射,可以在运行时获取类的字段、方法、构造方法等信息,以及动态调用这些方法。反射主要涉及到 java.lang.reflect
包中的类和接口。
关键的类和接口包括:
-
Class 类:
Class
类是 Java 反射的核心,它表示运行时的类。- 通过
Class.forName("className")
或object.getClass()
方法获取一个类的Class
对象。
-
Field 类:
Field
类表示类的字段,包括变量和常量。- 通过
Class
对象的getDeclaredField()
或getField()
方法获取字段对象。
-
Method 类:
Method
类表示类的方法。- 通过
Class
对象的getDeclaredMethod()
或getMethod()
方法获取方法对象。
-
Constructor 类:
Constructor
类表示类的构造方法。- 通过
Class
对象的getDeclaredConstructor()
或getConstructor()
方法获取构造方法对象。
通过这些类和方法,可以在运行时动态地操作类的结构和调用方法。反射的原理主要涉及到类加载、Class
对象的创建、访问控制、以及动态调用方法等方面。
-
类加载:
- 在 Java 中,类的加载是在运行时进行的。当 JVM 需要加载一个类时,会在类路径中查找相应的字节码文件,并将其加载到内存中。
- 反射通过
Class.forName("className")
或object.getClass()
等方式获取Class
对象,触发类的加载。
-
Class 对象的创建:
Class
对象表示加载到内存中的类。可以通过Class
类的静态方法forName()
或通过对象的getClass()
方法获取。- 通过
Class
对象,可以获取类的结构信息,如字段、方法等。
-
访问控制:
- 反射可以突破 Java 的访问控制,即使字段或方法是私有的,也可以通过反射进行访问。
- 可以使用
setAccessible(true)
方法来解除访问控制。
-
动态调用方法:
- 通过
Method
类的invoke()
方法,可以动态地调用类的方法,传递参数并获取返回值。
- 通过
反射提供了一种灵活的机制,使得在运行时可以动态地获取和操作类的信息。然而,由于反射涉及到运行时的类型检查,因此在性能上可能不如直接调用。在使用反射时需要谨慎,避免不必要的性能开销。
33.Java异常分类
在 Java 中,异常分为两大类:编译时异常(Checked Exception)和运行时异常(Unchecked Exception)。
-
编译时异常(Checked Exception):
- 编译时异常是在编译阶段由编译器检查的异常,程序必须显式地处理或声明抛出。
- 这些异常通常是由外部因素导致的,程序员能够合理地预测并处理这些异常。
- 例如,
IOException
、ClassNotFoundException
都是编译时异常的例子。
-
运行时异常(Unchecked Exception):
- 运行时异常是在程序运行期间可能发生的异常,编译器不要求必须显式地处理或声明抛出。
- 运行时异常通常是由程序中的错误逻辑导致的,比如空指针异常、数组越界异常等。
- 例如,
NullPointerException
、ArrayIndexOutOfBoundsException
都是运行时异常的例子。
-
错误(Error):
- 错误是指程序无法处理的严重问题,通常是由系统级别的问题导致的,如内存溢出、线程死锁等。
- 与异常不同,错误是不应该被捕获和处理的,而是应该由程序员采取措施来解决或修复。
Java 异常体系还包括 RuntimeException
类及其子类,它是所有运行时异常的父类。在实际编码中,建议只捕获并处理那些确实可能发生且程序能够合理处理的异常,而对于不可控制的异常或错误,应该让程序崩溃并由程序员来修复。
34.深拷贝和浅拷贝
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是关于对象复制的两个概念,涉及到对象内部的引用类型成员的处理方式。
-
浅拷贝(Shallow Copy):
- 浅拷贝是指只复制对象本身以及对象中的基本数据类型字段,而不复制对象内部的引用类型字段。
- 复制的新对象和原对象共享引用类型字段,即它们指向同一块内存地址。
-
深拷贝(Deep Copy):
- 深拷贝是指不仅复制对象本身,还要递归复制对象内部的引用类型字段,使得新对象和原对象的引用类型字段指向不同的内存地址。
- 实现深拷贝的方式包括手动复制或使用序列化与反序列化。
在深拷贝中,需要确保对象及其所有引用类型字段都是可序列化的,或者手动实现递归复制的逻辑。选择深拷贝还是浅拷贝取决于具体需求,以及对象内部的数据结构和关系。