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): 将垃圾回收过程划分为多个步骤,每次执行其中的一步。这样可以在垃圾回收的同时,减少对应用程序的影响&#