From f1767c027f1a17140a204507708f43d127cbdeff Mon Sep 17 00:00:00 2001 From: zeroornull Date: Thu, 20 Mar 2025 19:51:00 +0800 Subject: [PATCH] structure change commit. --- src/interview/Devops/CI CD.md | 136 + src/interview/Devops/Docker.md | 213 ++ src/interview/Devops/Linux.md | 269 ++ src/interview/Devops/MonitoringSystem.md | 67 + src/interview/Frontend/Vue.md | 60 + src/interview/Java/JavaIO.md | 345 +++ src/interview/Java/JavaJVM和调优.md | 766 ++++++ src/interview/Java/Java基础.md | 706 +++++ .../{java/java.md => Java/Java并发.md} | 2264 +---------------- src/interview/Java/Java新版本.md | 417 +++ src/interview/Java/Java集合.md | 115 + src/interview/Java/README.md | 11 + src/interview/Microservices/Kubernetes.md | 292 +++ src/interview/Microservices/ServiceMesh.md | 30 + src/interview/Microservices/SpringCloud.md | 321 +++ src/interview/Other/Other.md | 287 +++ src/interview/README.md | 1 + 17 files changed, 4039 insertions(+), 2261 deletions(-) create mode 100644 src/interview/Devops/CI CD.md create mode 100644 src/interview/Devops/Docker.md create mode 100644 src/interview/Devops/Linux.md create mode 100644 src/interview/Devops/MonitoringSystem.md create mode 100644 src/interview/Frontend/Vue.md create mode 100644 src/interview/Java/JavaIO.md create mode 100644 src/interview/Java/JavaJVM和调优.md create mode 100644 src/interview/Java/Java基础.md rename src/interview/{java/java.md => Java/Java并发.md} (59%) create mode 100644 src/interview/Java/Java新版本.md create mode 100644 src/interview/Java/Java集合.md create mode 100644 src/interview/Java/README.md create mode 100644 src/interview/Microservices/Kubernetes.md create mode 100644 src/interview/Microservices/ServiceMesh.md create mode 100644 src/interview/Microservices/SpringCloud.md create mode 100644 src/interview/Other/Other.md diff --git a/src/interview/Devops/CI CD.md b/src/interview/Devops/CI CD.md new file mode 100644 index 0000000..8e1797b --- /dev/null +++ b/src/interview/Devops/CI CD.md @@ -0,0 +1,136 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: CI/CD +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +### 15.3 CI/CD + +#### 什么是CI? + +CI的英文名称是Continuous Integration,中文翻译为:持续集成。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/ce0416342a57976c8643e5d4992b4022.png) + +CI中,开发人员将会频繁地向主干提交代码,这些新提交的代码在最终合并到主干前,需要经过编译和自动化测试流进行验证。 持续集成(CI)是在源代码变更后自动检测、拉取、构建和(在大多数情况下)进行单元测试的过程。持续集成的目标是快速确保开发人员新提交的变更是好的,并且适合在代码库中进一步使用。CI的流程执行和理论实践让我们可以确定新代码和原有代码能否正确地集成在一起。 + +通俗点讲就是:通过持续集成, 开发人员能够在任何时候多次向仓库提交作品,而不是独立地开发每个功能模块并在开发周期结束时一一提交。这里的一个重要思想就是让开发人员更快更、频繁地做到这一点,从而降低集成的开销。 实际情况中,开发人员在集成时经常会发现新代码和已有代码存在冲突。 如果集成较早并更加频繁,那么冲突将更容易解决且执行成本更低。当然,这里也有一些权衡,这个流程不提供额外的质量保障。 事实上,许多组织发现这样的集成方式开销更大,因为它们依赖人工确保新代码不会引起新的 bug 或者破坏现有代码。 为了减少集成期间的摩擦,持续集成依赖于测试套件和自动化测试。 然而,要认识到自动化测试和持续测试是完全不同的这一点很重要。 + +CI 的目标是将集成简化成一个简单、易于重复的日常开发任务, 这样有助于降低总体的构建成本并在开发周期的早期发现缺陷。 要想有效地使用 CI 必须转变开发团队的习惯,要鼓励频繁迭代构建, 并且在发现 bug 的早期积极解决。 + +#### 什么是CD? + +这里的CD可对应多个英文名称,**持续交付Continuous Delivery**和**持续部署Continuous Deployment**。下面我们分别来看看上面是持续交付和持续部署。 + +- **持续交付** + +持续交付(CD)实际上是 CI 的扩展,其中软件交付流程进一步自动化,以便随时轻松地部署到生成环境中。 成熟的持续交付方案也展示了一个始终可部署的代码库。使用 CD 后,软件发布将成为一个没有任何紧张感的例行事件。 开发团队可以在日常开发的任何时间进行产品级的发布,而不需要详细的发布方案或者特殊的后期测试。 + +完成 CI 中构建及单元测试和集成测试的自动化流程后,持续交付可自动将已验证的代码发布到存储库。为了实现高效的持续交付流程,务必要确保 CI 已内置于开发管道。持续交付的目标是拥有一个可随时部署到生产环境的代码库。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/de6634289b0d35347313a62b597ef5f8.png) + +在持续交付中,每个阶段(从代码更改的合并,到生产就绪型构建版本的交付)都涉及测试自动化和代码发布自动化。在流程结束时,运维团队可以快速、轻松地将应用部署到生产环境中或发布给最终使用的用户。 + +CD 集中依赖于部署流水线,团队通过流水线自动化测试和部署过程。此流水线是一个自动化系统, 可以针对构建执行一组渐进的测试套件。CD 具有高度的自动化,并且在一些云计算环境中也易于配置。在流水线的每个阶段,如果构建无法通过关键测试会向团队发出警报。否则,将继续进入下一个测试, 并在连续通过测试后自动进入下一个阶段。流水线的最后一个部分会将构建部署到和生产环境等效的环境中。 这是一个整体的过程,因为构建、部署和环境都是一起执行和测试的,它能让构建在实际的生产环境可部署和可验证。 + +- **持续部署** + +持续部署扩展了持续交付,以便软件构建在通过所有测试时自动部署。在这样的流程中, 不需要人为决定何时及如何投入生产环境。CI/CD 系统的最后一步将在构建后的组件/包退出流水线时自动部署。 此类自动部署可以配置为快速向客户分发组件、功能模块或修复补丁,并准确说明当前提供的内容。采用持续部署的组织可以将新功能快速传递给用户,得到用户对于新版本的快速反馈,并且可以迅速处理任何明显的缺陷。 用户对无用或者误解需求的功能的快速反馈有助于团队规划投入,避免将精力集中于不容易产生回报的地方。 + +随着 DevOps 的发展,新的用来实现 CI/CD 流水线的自动化工具也在不断涌现。这些工具通常能与各种开发工具配合, 包括像 GitHub 这样的代码仓库和 Jira 这样的 bug 跟踪工具。此外,随着 SaaS 这种交付方式变得更受欢迎, 许多工具都可以在现代开发人员运行应用程序的云环境中运行,例如 GCP 和 AWS。但是对于一个成熟的CI/CD管道(Pipeline)来说,最后的阶段是持续部署。作为持续交付——自动将生产就绪型构建版本发布到代码存储库——的延伸,持续部署可以自动将应用发布到生产环境。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/05d85f8881f1ab385d607f5e37152779.png) + +#### 什么是CI/CD的管道? + +CI / CD管道是与自动化工具和改进的工作流程集成的部署管道。 如果执行得当,它将最大程度地减少人为错误,并增强整个SDLC的反馈循环,使团队可以在更短的时间内交付较小的发行版。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/23e1fa583338bcea7b1a93a6be00012c.png) + +典型的CI / CD管道必须包括以下阶段: + +- 构建阶段 +- 测试阶段 +- 部署阶段 +- 自动化测试阶段 +- 部署到生产 + +#### 如何理解DevOPS? + +DevOps是Development和Operations的组合,是一种方法论,是一组过程、方法与系统的统称,用于促进应用开发、应用运维和质量保障(QA)部门之间的沟通、协作与整合。以期打破传统开发和运营之间的壁垒和鸿沟。 + +CI、CD和DevOps之间的关系 : + +![img](https://b2files.173114.xyz/blogimg/2025/03/49dd00c6c6faaa9c0887c8e02e60c832.png) + +#### 在完全部署到所有用户之前,有哪些方法可以测试部署? + +由于必须回滚/撤消对所有用户的部署可能是一种代价高昂的情况(无论是技术上还是用户的感知),已经有许多技术允许“尝试”部署新功能并在发现问题时轻松“撤消”它们。这些包括: + +- **蓝/绿测试/部署** + +在这种部署软件的方法中,维护了两个相同的主机环境 —— 一个“蓝色” 和一个“绿色”。(颜色并不重要,仅作为标识。)对应来说,其中一个是“生产环境”,另一个是“预发布环境”。 + +在这些实例的前面是调度系统,它们充当产品或应用程序的客户“网关”。通过将调度系统指向蓝色或绿色实例,可以将客户流量引流到期望的部署环境。通过这种方式,切换指向哪个部署实例(蓝色或绿色)对用户来说是快速,简单和透明的。 + +当新版本准备好进行测试时,可以将其部署到非生产环境中。在经过测试和批准后,可以更改调度系统设置以将传入的线上流量指向它(因此它将成为新的生产站点)。现在,曾作为生产环境实例可供下一次候选发布使用。 + +同理,如果在最新部署中发现问题并且之前的生产实例仍然可用,则简单的更改可以将客户流量引流回到之前的生产实例 —— 有效地将问题实例“下线”并且回滚到以前的版本。然后有问题的新实例可以在其它区域中修复。 + +- **金丝雀测试/部署** + +在某些情况下,通过蓝/绿发布切换整个部署可能不可行或不是期望的那样。另一种方法是为金丝雀测试/部署。在这种模型中,一部分客户流量被重新引流到新的版本部署中。例如,新版本的搜索服务可以与当前服务的生产版本一起部署。然后,可以将 10% 的搜索查询引流到新版本,以在生产环境中对其进行测试。 + +如果服务那些流量的新版本没问题,那么可能会有更多的流量会被逐渐引流过去。如果仍然没有问题出现,那么随着时间的推移,可以对新版本增量部署,直到 100% 的流量都调度到新版本。这有效地“更替”了以前版本的服务,并让新版本对所有客户生效。 + +- **功能开关** + +对于可能需要轻松关掉的新功能(如果发现问题),开发人员可以添加功能开关。这是代码中的 if-then 软件功能开关,仅在设置数据值时才激活新代码。此数据值可以是全局可访问的位置,部署的应用程序将检查该位置是否应执行新代码。如果设置了数据值,则执行代码;如果没有,则不执行。 + +这为开发人员提供了一个远程“终止开关”,以便在部署到生产环境后发现问题时关闭新功能。 + +- **暗箱发布** + +在暗箱发布中,代码被逐步测试/部署到生产环境中,但是用户不会看到更改(因此名称中有暗箱一词)。例如,在生产版本中,网页查询的某些部分可能会重定向到查询新数据源的服务。开发人员可收集此信息进行分析,而不会将有关接口,事务或结果的任何信息暴露给用户。 + +这个想法是想获取候选版本在生产环境负载下如何执行的真实信息,而不会影响用户或改变他们的经验。随着时间的推移,可以调度更多负载,直到遇到问题或认为新功能已准备好供所有人使用。实际上功能开关标志可用于这种暗箱发布机制。 + +#### 什么是持续测试? + +持续测试是一个过程,它将自动化测试作为软件交付通道中内嵌的一部分,以尽快获得软件发布后业务风险的反馈。 + +**持续测试与自动化测试的侧重点**? + +- 自动化测试旨在生成一组与用户故事或应用程序要求相关的通过/失败的数据点。 +- 持续测试侧重于业务风险,并提供有关软件是否可以发布的判断。要实现这一转变,我们需要停止询问“我们是否已完成测试?”而是集中精力在“发布版本是否具有可接受的业务风险级别?” + +**为什么我们需要持续测试**? + +今天,整个行业的变化要求测试更多,同时使自动化测试更难实现(至少使用传统工具和方法): + +- 应用程序体系结构越来越分散和复杂,包含云,API,微服务等,并在单个业务事务中创建几乎无限的不同协议和技术组合。 +- 由于Agile,DevOps和持续交付,许多应用程序现在每两周发布一次,每天发布数千次。因此,可用于测试设计,维护和特别是执行的时间大大减少。 + +既然软件是业务的主要接口,那么应用程序故障就是业务失败, 如果它影响用户体验,即使是看似微不足道的小故障也会产生严重后果。因此,与应用相关的风险已成为即使是非技术性商业领袖的主要关注点。 + +#### 如何做版本管理? + +![img](https://b2files.173114.xyz/blogimg/2025/03/497a589939d8f689ed34a8f60c99c09b.png) + +- **Master 分支** 主分支,这个分支最近发布到生产环境的代码,最近发布的Release, 这个分支只能从其他分支合并,不能在这个分支直接修改 +- **Develop 分支** 这个分支是我们是我们的主开发分支,包含所有要发布到下一个Release的代码,这个主要合并与其他分支,比如Feature分支 +- **Feature 分支** 这个分支主要是用来开发一个新的功能,一旦开发完成,我们合并回Develop分支进入下一个Release +- **Release 分支** 当你需要发布一个新Release的时候,我们基于Develop分支创建一个Release分支,完成Release后,我们合并到Master和Develop分支 +- **Hotfix 分支** 当我们在Production发现新的Bug时候,我们需要创建一个Hotfix, 完成Hotfix后,我们合并回Master和Develop分支,所以Hotfix的改动会进入下一个Release \ No newline at end of file diff --git a/src/interview/Devops/Docker.md b/src/interview/Devops/Docker.md new file mode 100644 index 0000000..da2440e --- /dev/null +++ b/src/interview/Devops/Docker.md @@ -0,0 +1,213 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Docker +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +### 15.2 Docker + +#### 什么是虚拟化技术? + +在计算机技术中,虚拟化(Virtualization)是一种资源管理技术。它是将计算机的各种实体资源,如:服务器、网络、内存及存储等,予以抽象、转换后呈现出来,打破实体结构间的不可切割的障碍,使用户可以用更好的方式来利用这些资源。 + +虚拟化的目的是为了在同一个主机上运行多个系统或应用,从而提高系统资源的利用率,并带来降低成本、方便管理和容错容灾等好处。 + +- **硬件虚拟化** + +硬件虚拟化就是硬件物理平台本身提供了对特殊指令的截获和重定向的支持。支持虚拟化的硬件,也是一些基于硬件实现软件虚拟化技术的关键。在基于硬件实现软件虚拟化的技术中,在硬件是实现虚拟化的基础,硬件(主要是CPU)会为虚拟化软件提供支持,从而实现硬件资源的虚拟化。 + +- **软件虚拟化** + +软件虚拟化就是利用软件技术,在现有的物理平台上实现对物理平台访问的截获和模拟。在软件虚拟化技术中,有些技术不需要硬件支持,如:QEMU;而有些软件虚拟化技术,则依赖硬件支持,如:VMware、KVM。 + +#### 什么是Docker? + +Docker是一个开源的应用容器引擎,它让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到安装了任何 Linux 发行版本的机器上。Docker基于LXC来实现类似VM的功能,可以在更有限的硬件资源上提供给用户更多的计算资源。与同VM等虚拟化的方式不同,LXC不属于全虚拟化、部分虚拟化或半虚拟化中的任何一个分类,而是一个操作系统级虚拟化。 + +Docker是直接运行在宿主操作系统之上的一个容器,使用沙箱机制完全虚拟出一个完整的操作,容器之间不会有任何接口,从而让容器与宿主机之间、容器与容器之间隔离的更加彻底。每个容器会有自己的权限管理,独立的网络与存储栈,及自己的资源管理能,使同一台宿主机上可以友好的共存多个容器。 + +Docker借助Linux的内核特性,如:控制组(Control Group)、命名空间(Namespace)等,并直接调用操作系统的系统调用接口。从而降低每个容器的系统开销,并实现降低容器复杂度、启动快、资源占用小等特征。 + +#### Docker和虚拟机的区别? + +虚拟机Virtual Machine与容器化技术(代表Docker)都是虚拟化技术,两者的区别在于虚拟化的程度不同。 + +- **举个例子** + +1. **服务器**:比作一个大型的仓管基地,包含场地与零散的货物——相当于各种服务器资源。 +2. **虚拟机技术**:比作仓库,拥有独立的空间堆放各种货物或集装箱,仓库之间完全独立——仓库相当于各种系统,独立的应用系统和操作系统。 +3. **Docker**:比作集装箱,操作各种货物的打包——将各种应用程序和他们所依赖的运行环境打包成标准的容器,容器之间隔离。 + +- **基于一个图解释** + +![img](https://b2files.173114.xyz/blogimg/2025/03/87cd800c8a6485fbe41084d47fbeba79.jpg) + +1. 虚拟机管理系统(Hypervisor)。利用Hypervisor,可以在主操作系统之上运行多个不同的从操作系统。类型1的Hypervisor有支持MacOS的HyperKit,支持Windows的Hyper-V以及支持Linux的KVM。类型2的Hypervisor有VirtualBox和VMWare。 +2. Docker守护进程(Docker Daemon)。Docker守护进程取代了Hypervisor,它是运行在操作系统之上的后台进程,负责管理Docker容器。 +3. vm多了一层guest OS,虚拟机的Hypervisor会对硬件资源也进行虚拟化,而容器Docker会直接使用宿主机的硬件资源 + +- **基于虚拟化角度** + +1. **隔离性** 由于vm对操作系统也进行了虚拟化,隔离的更加彻底。而Docker共享宿主机的操作系统,隔离性较差。 +2. **运行效率** 由于vm的隔离操作,导致生成虚拟机的速率大大低于容器Docker生成的速度,因为Docker直接利用宿主机的系统内核。因为虚拟机增加了一层虚拟硬件层,运行在虚拟机上的应用程序在进行数值计算时是运行在Hypervisor虚拟的CPU上的;另外一方面是由于计算程序本身的特性导致的差异。虚拟机虚拟的cpu架构不同于实际cpu架构,数值计算程序一般针对特定的cpu架构有一定的优化措施,虚拟化使这些措施作废,甚至起到反效果。 +3. **资源利用率** 在资源利用率上虚拟机由于隔离更彻底,因此利用率也会相对较低。 + +#### Docker的架构? + +Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。 + +- **Docker 客户端(Client)** : Docker 客户端通过命令行或者其他工具使用 Docker SDK (https://docs.docker.com/develop/sdk/) 与 Docker 的守护进程通信。 +- **Docker 主机(Host)** :一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。 + +Docker 包括三个基本概念: + +- **镜像(Image)**:Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。 +- **容器(Container)**:镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。 +- **仓库(Repository)**:仓库可看着一个代码控制中心,用来保存镜像。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/6e0c333ef30b9a19cdb6ef7dad83f4d3.png) + +#### Docker镜像相关操作有哪些? + +```bash +# 查找镜像 +docker search mysql + +# 拉取镜像 +docker pull mysql + +# 删除镜像 +docker rmi hello-world + +# 更新镜像 +docker commit -m="update test" -a="pdai" 0a1556ca3c27 pdai/ubuntu:v1.0.1 + +# 生成镜像 +docker build -t pdai/ubuntu:v2.0.1 . + +# 镜像标签 +docker tag a733d5a264b5 pdai/ubuntu:v3.0.1 + +# 镜像导出 +docker save > pdai-ubuntu-v2.0.2.tar 57544a04cd1a + +# 镜像导入 +docker load < pdai-ubuntu-v2.0.2.tar +``` + +#### Docker容器相关操作有哪些? + +```bash +# 容器查看 +docker ps -a + +# 容器启动 +docker run -it pdai/ubuntu:v2.0.1 /bin/bash + +# 容器停止 +docker stop f5332ebce695 + +# 容器再启动 +docker start f5332ebce695 + +# 容器重启 +docker restart f5332ebce695 + +# 容器导出 +docker export f5332ebce695 > ubuntu-pdai-v2.tar + +# 容器导入 +docker import ubuntu-pdai-v2.tar pdai/ubuntu:v2.0.2 + +# 容器强制停止并删除 +docker rm -f f5332ebce695 + +# 容器清理 +docker container prune + +# 容器别名操作 +docker run -itd --name pdai-ubuntu-202 pdai/ubuntu:v2.0.2 /bin/bash +``` + +#### 如何查看Docker容器的日志? + +```bash +#例:实时查看docker容器名为user-uat的最后10行日志 +docker logs -f -t --tail 10 user-uat + +#例:查看指定时间后的日志,只显示最后100行: +docker logs -f -t --since="2018-02-08" --tail=100 user-uat + +#例:查看最近30分钟的日志: +docker logs --since 30m user-uat + +#例:查看某时间之后的日志: +docker logs -t --since="2018-02-08T13:23:37" user-uat + +#例:查看某时间段日志: +docker logs -t --since="2018-02-08T13:23:37" --until "2018-02-09T12:23:37" user-uat + +#例:将错误日志写入文件: +docker logs -f -t --since="2018-02-18" user-uat | grep error >> logs_error.txt +``` + +#### 如何启动Docker容器?参数含义? + +```bash +[root@pdai docker-test]# docker run -itd pdai/ubuntu:v2.0.1 /bin/bash +``` + +- `-it` 可以连写的,表示 `-i -t` +- `-t`: 在新容器内指定一个伪终端或终端。 +- `-i`: 允许你对容器内的标准输入 (STDIN) 进行交互 +- `-d`: 后台模式 + +#### 如何进入Docker后台模式?有什么区别? + +- 第一种:`docker attach` + +```bash +[root@pdai ~]# docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +f5332ebce695 pdai/ubuntu:v2.0.1 "/bin/bash" 38 minutes ago Up 2 seconds 22/tcp, 80/tcp jolly_kepler +[root@pdai ~]# docker attach f5332ebce695 +root@f5332ebce695:/# echo 'pdai' +pdai +root@f5332ebce695:/# exit +exit +[root@pdai ~]# docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +``` + +看到没,使用`docker attach`进入后,exit便容器也停止了。 + +- 第二种:`docker exec` + +```bash +[root@pdai ~]# docker exec -it f5332ebce695 /bin/bash +Error response from daemon: Container f5332ebce69520fba353f035ccddd4bd42055fbd1e595f916ba7233e26476464 is not running +[root@pdai ~]# docker restart f5332ebce695 +f5332ebce695 +[root@pdai ~]# docker exec -it f5332ebce695 /bin/bash +root@f5332ebce695:/# exit +exit +[root@pdai ~]# docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +f5332ebce695 pdai/ubuntu:v2.0.1 "/bin/bash" 42 minutes ago Up 8 seconds 22/tcp, 80/tcp jolly_kepler +``` + +注意: + +- 我特意在容器停止状态下执行了`docker exec`,是让你看到`docker exec`是在容器启动状态下用的,且注意下错误信息; +- 推荐大家使用 `docker exec` 命令,因为此退出容器终端,不会导致容器的停止。 \ No newline at end of file diff --git a/src/interview/Devops/Linux.md b/src/interview/Devops/Linux.md new file mode 100644 index 0000000..75b5537 --- /dev/null +++ b/src/interview/Devops/Linux.md @@ -0,0 +1,269 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Linux +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +## 15 DevOps + +### 15.1 Linux + +#### 什么是Linux? + +Linux是一种基于UNIX的操作系统,最初是由Linus Torvalds引入的。它基于Linux内核,可以运行在由Intel,MIPS,HP,IBM,SPARC和Motorola制造的不同硬件平台上。Linux中另一个受欢迎的元素是它的吉祥物,一个名叫Tux的企鹅形象。 + +#### UNIX和LINUX有什么区别? + +Unix最初是作为Bell Laboratories的专有操作系统开始的,后来产生了不同的商业版本。另一方面,Linux是免费的,开源的,旨在为大众提供非适当的操作系统。 + +#### 什么是BASH? + +BASH是Bourne Again SHell的缩写。它由Steve Bourne编写,作为原始Bourne Shell(由/ bin / sh表示)的替代品。它结合了原始版本的Bourne Shell的所有功能,以及其他功能,使其更容易使用。从那以后,它已被改编为运行Linux的大多数系统的默认shell。 + +#### 什么是Linux内核? + +Linux内核是一种低级系统软件,其主要作用是为用户管理硬件资源。它还用于为用户级交互提供界面。 + +#### 什么是LILO? + +LILO是Linux的引导加载程序。它主要用于将Linux操作系统加载到主内存中,以便它可以开始运行。 + +#### 什么是交换空间? + +交换空间是Linux使用的一定空间,用于临时保存一些并发运行的程序。当RAM没有足够的内存来容纳正在执行的所有程序时,就会发生这种情况。 + +#### Linux的基本组件是什么? + +就像任何其他典型的操作系统一样,Linux拥有所有这些组件:内核,shell和GUI,系统实用程序和应用程序。Linux比其他操作系统更具优势的是每个方面都附带其他功能,所有代码都可以免费下载。 + +#### Linux系统安装多个桌面环境有帮助吗? + +通常,一个桌面环境,如KDE或Gnome,足以在没有问题的情况下运行。尽管系统允许从一个环境切换到另一个环境,但这对用户来说都是优先考虑的问题。有些程序在一个环境中工作而在另一个环境中无法工作,因此它也可以被视为选择使用哪个环境的一个因素。 + +#### BASH和DOS之间的基本区别是什么? + +BASH和DOS控制台之间的主要区别在于3个方面: + +BASH命令区分大小写,而DOS命令则不区分; 在BASH下,/ character是目录分隔符,\作为转义字符。在DOS下,/用作命令参数分隔符,\是目录分隔符 DOS遵循命名文件中的约定,即8个字符的文件名后跟一个点,扩展名为3个字符。BASH没有遵循这样的惯例。 + +#### GNU项目的重要性是什么? + +这种所谓的自由软件运动具有多种优势,例如可以自由地运行程序以及根据你的需要自由学习和修改程序。它还允许你将软件副本重新分发给其他人,以及自由改进软件并将其发布给公众。 + +#### 描述root帐户? + +root帐户就像一个系统管理员帐户,允许你完全控制系统。你可以在此处创建和维护用户帐户,为每个帐户分配不同的权限。每次安装Linux时都是默认帐户。 + +#### 如何在发出命令时打开命令提示符? + +要打开默认shell(可以找到命令提示符的位置),请按Ctrl-Alt-F1。这将提供命令行界面(CLI),你可以根据需要从中运行命令。 + +#### 如何知道Linux使用了多少内存? + +在命令shell中,使用“concatenate”命令:cat / proc / meminfo获取内存使用信息。你应该看到一行开始像Mem:64655360等。这是Linux认为它可以使用的总内存。 + +你也可以使用命令 + +```bash +free - m +vmstat +top +htop +``` + +找到当前的内存使用情况 + +#### Linux系统下交换分区的典型大小是多少? + +交换分区的首选大小是系统上可用物理内存量的两倍。如果无法做到这一点,则最小大小应与安装的内存量相同。 + +#### 什么是符号链接? + +符号链接的行为类似于Windows中的快捷方式。这些链接指向程序,文件或目录。它还允许你即时访问它,而无需直接转到整个路径名。 + +#### Ctrl + Alt + Del组合键是否适用于Linux? + +是的,它确实。就像Windows一样,你可以使用此组合键来执行系统重启。一个区别是你不会收到任何确认消息,因此,立即重启。 + +#### 如何引用连接打印机等设备的并行端口? + +在Windows下,你将并行端口称为LPT端口,而在Linux下,你将其称为/ dev / lp。因此,LPT1,LPT2和LPT3在Linux下称为/ dev / lp0,/ dev / lp1或/ dev / lp2。 + +#### 硬盘驱动器和软盘驱动器等驱动器是否用驱动器号表示? + +在Linux中,每个驱动器和设备都有不同的名称。例如,软盘驱动器称为/ dev / fd0和/ dev / fd1。IDE / EIDE硬盘驱动器称为/ dev / hda,/ dev / hdb,/ dev / hdc等。 + +#### 如何在Linux下更改权限? + +假设你是系统管理员或文件或目录的所有者,则可以使用chmod命令授予权限。使用+符号添加权限或 - 符号拒绝权限,以及以下任何字母:u(用户),g(组),o(其他),a(所有),r(读取),w(写入)和x(执行)。例如,命令chmod go + rw FILE1.TXT授予对文件FILE1.TXT的读写访问权限,该文件分配给组和其他组。 + +#### 在Linux中,为不同的串口分配了哪些名称? + +串行端口标识为/ dev / ttyS0到/ dev / ttyS7。这些是Windows中COM1到COM8的等效名称。 + +#### 如何在Linux下访问分区? + +Linux在驱动器标识符的末尾分配数字。例如,如果第一个IDE硬盘驱动器有三个主分区,则它们将命名/编号,/ dev / hda1,/ dev / hda2和/ dev / hda3。 + +#### 什么是硬链接? + +硬链接直接指向磁盘上的物理文件,而不指向路径名。这意味着如果重命名或移动原始文件,链接将不会中断,因为链接是针对文件本身的,而不是文件所在的路径。 + +#### Linux下文件名的最大长度是多少? + +任何文件名最多可包含255个字符。此限制不包括路径名,因此整个路径名和文件名可能会超过255个字符。 + +#### 什么是以点开头的文件名? + +通常,以点开头的文件名是隐藏文件。这些文件可以是包含重要数据或设置信息的配置文件。将这些文件设置为隐藏会使其不太可能被意外删除。 + +#### 解释虚拟桌面? + +这可以作为最小化和最大化当前桌面上不同窗口的替代方案。当你可以打开一个或多个程序时,使用虚拟桌面可以清除桌面。你可以简单地在虚拟桌面之间进行随机播放,而不是在每个程序中保持完整的程序,而不是最小化/恢复所有这些程序。 + +#### 如何在Linux下跨不同的虚拟桌面共享程序? + +要在不同的虚拟桌面之间共享程序,请在程序窗口的左上角查找看起来像图钉的图标。按此按钮将“固定”该应用程序到位,使其显示在所有虚拟桌面上,位于屏幕上的相同位置。 + +#### 无名(空)目录代表什么? + +此空目录名称用作Linux文件系统的无名基础。这用作所有其他目录,文件,驱动器和设备的附件。 + +#### 什么是pwd命令? + +pwd命令是print working directory命令的缩写。 + +```bash +PWD +/home/guru99/myDir +``` + +#### 什么是守护进程? + +守护进程是提供基本操作系统下可能无法使用的多种功能的服务。其主要任务是监听服务请求,同时对这些请求采取行动。服务完成后,它将断开连接并等待进一步的请求。 + +#### 如何从一个桌面环境切换到另一个桌面环境,例如从KDE切换到Gnome? + +假设你已安装这两个环境,只需从图形界面注销即可。然后在登录屏幕上,键入你的登录ID和密码,并选择要加载的会话类型。在你将其更改为其他选项之前,此选项将保持默认状态。 + +#### Linux下的权限有哪些? + +Linux下有3种权限: + +- 读取:用户可以读取文件或列出目录 +- 写入:用户可以写入新文件到目录的文件 +- 执行:用户可以运行文件或查找特定文件一个目录 + +#### 区分大小写如何影响命令的使用方式? + +当我们讨论区分大小写时,只有当每个字符按原样编码时,命令才被认为是相同的,包括小写和大写字母。这意味着CD,CD和Cd是三个不同的命令。使用大写字母输入命令,它应该是小写的,将产生不同的输出。 + +#### 是否可以使用快捷方式获取长路径名? + +就在这里。称为文件名扩展的功能允许你使用TAB键执行此操作。例如,如果你有一个名为/ home / iceman / assignments目录的路径,则键入如下:/ ho [tab] / ice [tab] / assi [tab]。但是,这假设路径是唯一的,并且你正在使用的shell支持此功能。 + +#### 什么是重定向? + +重定向是将数据从一个输出定向到另一个输出的过程。它还可以用于将输出作为输入定向到另一个进程。 + +#### 什么是grep命令? + +grep使用基于模式的搜索的搜索命令。它使用与命令行一起指定的选项和参数,并在搜索所需的文件输出时应用此模式。 + +#### 当发出的命令与上次使用时产生的结果不同时,会出现什么问题? + +从看似相同的命令获得不同结果的一个非常可能的原因与区分大小写问题有关。由于Linux区分大小写,因此先前使用的命令可能以与当前格式不同的格式输入。例如,要列出目录中的所有文件,应键入命令ls,而不是LS。如果没有存在该确切名称的程序,则键入LS将导致错误消息,或者如果存在名为LS的程序执行另一个功能,则可能产生不同的输出。 + +#### /usr/local的内容是什么? + +它包含本地安装的文件。此目录在文件存储在网络上的环境中很重要。具体来说,本地安装的文件将转至/usr/local/bin,/usr/local/lib等。此目录的另一个应用是它用于从源安装的软件包,或未正式随分发一起提供的软件。 + +#### 你如何终止正在进行的流程? + +系统中的每个进程都由唯一的进程ID或pid标识。使用kill命令后跟pid来终止该进程。 + +要立即终止所有进程,请使用kill 0。 + +#### 如何在命令行提示符中插入注释? + +通过在实际注释文本之前键入#符号来创建注释。这告诉shell完全忽略后面的内容。例如“#这只是shell将忽略的注释。” + +#### 什么是命令分组以及它是如何工作的? + +你可以使用括号对命令进行分组。例如,如果要将当前日期和时间以及名为OUTPUT的文件的内容发送到名为MYDATES的第二个文件,可以按如下方式应用命令分组:(date cat OUTPUT)> MYDATES + +#### 如何从单个命令行条目执行多个命令或程序? + +你可以通过使用分号符号分隔每个命令或程序来组合多个命令。例如,你可以在单个条目中发出这样一系列命令: + +```bash +ls –l cd .. ls –a MYWORK which is equivalent to 3 commands: ls -l cd.. ls -a MYWORK +**请注意,这将按指定的顺序依次执行。 +``` + +#### 编写一个命令,查找扩展名为“c”的文件,并在其中出现字符串“apple”? + +```bash +find ./ -name "*.c" | xargs grep –i "apple" +``` + +#### 编写一个显示所有.txt文件的命令,包括其个人权限。 + +```bash +ls -al * .txt +``` + +#### 解释如何为Git控制台着色? + +要为Git控制台着色,可以使用命令git config-global color.ui auto。在命令中,color.ui变量设置变量的默认值,例如color.diff和color.grep。 + +#### 如何在Linux中将一个文件附加到另一个文件? + +要在Linux中将一个文件附加到另一个文件,你可以使用命令cat file2 >> file 1. operator >>附加指定文件的输出或创建文件(如果未创建)。而另一个命令cat文件1文件2>文件3将两个或多个文件附加到一个文件。 + +#### 解释如何使用终端找到文件? + +要查找文件,你必须使用命令,查找。-name“process.txt”。它将查找名为process.txt的文件的当前目录。 + +#### 解释如何使用终端创建文件夹? + +要创建文件夹,你必须使用命令mkdir。它将是这样的:〜$ mkdir Guru99 + +#### 解释如何使用终端查看文本文件? + +要查看文本文件,请使用命令cd转到文本文件所在的特定文件夹,然后键入less filename.txt。 + +#### 解释如何在Ubuntu LAMP堆栈上启用curl? + +要在Ubuntu上启用curl,首先安装libcurl,完成后使用以下命令sudo /etc/init .d / apache2 restart或sudo service apache2 restart。 + +#### 解释如何在Ubuntu中启用root日志记录? + +启用root日志记录的命令是 + +```bash +#sudo sh-c'echo“greater-show-manual-login = true”>> / etc / lightdm / lightdm.conf' +``` + +#### 如何在启动Linux服务器的同时在后台运行Linux程序? + +通过使用nohup。它将停止接收NOHUP信号的进程,从而终止它,你注销了调用的程序。并在后台运行该过程。 + +#### 解释如何在Linux中卸载库? + +要在Linux中卸载库,可以使用命令 + +```bash +sudo apt-get remove library_name +``` \ No newline at end of file diff --git a/src/interview/Devops/MonitoringSystem.md b/src/interview/Devops/MonitoringSystem.md new file mode 100644 index 0000000..3e00cf4 --- /dev/null +++ b/src/interview/Devops/MonitoringSystem.md @@ -0,0 +1,67 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: 监控体系 +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +### 15.4 监控体系 + +#### 为什么要有监控系统? 谈谈你对监控的理解? + +**监控的目标**? + +- 发现问题:当系统发生故障报警,我们会收到故障报警的信息。 +- 定位问题:故障邮件一般都会写某某主机故障、具体故障的内容,我们需要对报警内容进行分析。比如一台服务器连不上,我们就需要考虑是网络问题、还是负载太高导致长时间无法连接,又或者某开发触发了防火墙禁止的相关策略等,我们就需要去分析故障具体原因。 +- 解决问题:当然我们了解到故障的原因后,就需要通过故障解决的优先级去解决该故障。 +- 总结问题:当我们解决完重大故障后,需要对故障原因以及防范进行总结归纳,避免以后重复出现。 + +**具体而言**? + +- 对系统不间断的实时监控:实际上是对系统不间断的实时监控(这就是监控); +- 实时反馈系统当前状态:我们监控某个硬件、或者某个系统,都是需要能实时看到当前系统的状态,是正常、异常、或者故障。 +- 保证服务可靠性安全性:我们监控的目的就是要保证系统、服务、业务正常运行 +- 保证业务持续稳定运行:如果我们的监控做得很完善,即使出现故障,能第一时间接收到故障报警,在第一时间处理解决,从而保证业务持续性的稳定运行。 + +#### 监控体系监控哪些内容? + +1、**硬件监控** 通过SNMP来进行路由器交换机的监控(这些可以跟一些厂商沟通来了解如何做)、服务器的温度以及其它,可以通过IPMI来实现。当然如果没有硬件全都是云,直接跳过这一步骤。 + +2、**系统监控** 如CPU的负载,上下文切换、内存使用率、磁盘读写、磁盘使用率、磁盘inode使用率。当然这些都是需要配置触发器,因为默认太低会频繁报警。 + +3、**服务监控** 比如公司用的LNMP架构,Nginx自带Status模块、PHP也有相关的Status、MySQL的话可以通过Percona官方工具来进行监控。Redis这些通过自身的info获取信息进行过滤等。方法都类似。要么服务自带。要么通过脚本来实现想监控的内容,以及报警和图形功能。 + +4、**网络监控** 如果是云主机又不是跨机房,那么可以选择不监控网络。当然你说我们是跨机房以及如何如何,推荐使用smokeping来做网络相关的监控,或者直接交给你们的网络工程师来做,因为术业有专攻。 + +5、**安全监控** 如果是云主机可以考虑使用自带的安全防护。当然也可以使用iptables。如果是硬件,那么推荐使用硬件防火墙。使用云可以购买防DDOS,避免出现故障导致down机一天。如果是系统,那么权限、密码、备份、恢复等基础方案要做好。Web同时也可以使用Nginx+Lua来实现一个Web层面的防火墙。当然也可以使用集成好的OpenResty。 + +6、**Web监控** Web监控的话题其实还是很多。比如可以使用自带的Web监控来监控页面相关的延迟、js响应时间、下载时间、等等。这里我推荐使用专业的商业软件监控宝或听云来实现。毕竟人家全国各地都有机房(如果本身是多机房那就另说了)。 + +7、**日志监控** 如果是Web的话可以使用监控Nginx的50x、40x的错误日志,PHP的ERROR日志。其实这些需求无非是,收集、存储、查询、展示,我们其实可以使用开源的ELKStack来实现。Logstash(收集)、Elasticsearch(存储+搜索)、Kibana(展示)。 + +8、**业务监控** 上面做了那么多,其实最终还是保证业务的运行。这样我们做的监控才有意义。所以业务层面这块的监控需要和开发以及总监开会讨论,监控比较重要的业务指标,(需要开会确认)然后通过简单的脚本就可以实现,最后设置触发器即可 。 + +9、**流量分析** 平时我们分析日志都是拿awk sed xxx一堆工具来实现。这样对我们统计IP、PV、UV不是很方便。那么可以使用百度统计、Google统计、商业,让开发嵌入代码即可。为了避免隐私也可以使用Piwik来做相关的流量分析。 + +10、**可视化** 通过Screen以及引入一些第三方的库来美化界面,同时我们也需要知道,订单量突然增加、突然减少。或者说突然来了一大波流量,这流量从哪儿来,是不是推广了,还是被攻击了。可以结合监控平来梳理各个系统之间的业务关系。 + +11、**自动化监控** 如上我们做了那么多的工作,当然不能是一台一台的来加key实现。可以通过Zabbix的主动模式以及被动模式来实现。当然最好还是通过API来实现。 + +#### 监控一般采用什么样的流程? + +- **采集** 通过SNMP、Agent、ICMP、SSH、IPMI等对系统进行数据采集 +- **存储** 各类数据库服务,MySQL、PostgreSQL, 时序库等 +- **分析** 提供图形及时间线情况信息,方便我们定位故障所在 +- **展示** 指标信息、指标趋势展示 +- **报警** 电话、邮件、微信、短信、报警升级机制 +- **处理** 故障级别判定,找响应人员进行快速处理 \ No newline at end of file diff --git a/src/interview/Frontend/Vue.md b/src/interview/Frontend/Vue.md new file mode 100644 index 0000000..41fa097 --- /dev/null +++ b/src/interview/Frontend/Vue.md @@ -0,0 +1,60 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Vue +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +### 1.vue的生命周期有哪些及每个生命周期做了什么? + +`beforeCreate`是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。 + +`created`在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。 + +`beforeMount`发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。 + +`mounted`在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。 + +`beforeUpdate`发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。 + +`updated`发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。 + +`beforeDestroy`发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。 + +`destroyed`发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。 + +### 8.vue组件通信方式有哪些及原理 + +父子组件通信 + +父->子`props`,子->父 `$on、$emit` + +获取父子组件实例 `$parent、$children` + +`Ref` 获取实例的方式调用组件的属性或者方法 + +`Provide、inject` 官方不推荐使用,但是写组件库时很常用 + +兄弟组件通信 + +```javascript +javascript 代码解读复制代码Event Bus` 实现跨组件通信 `Vue.prototype.$bus = new Vue +Vuex +``` + +跨级组件通信 + +```bash +bash 代码解读复制代码Vuex +$attrs、$listeners +``` diff --git a/src/interview/Java/JavaIO.md b/src/interview/Java/JavaIO.md new file mode 100644 index 0000000..19ca3c1 --- /dev/null +++ b/src/interview/Java/JavaIO.md @@ -0,0 +1,345 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Java IO +index: true +headerDepth: 3 +order: 4 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- +## 4 Java IO + +> Java IO相关 + +### 4.1 基础IO + +#### [#](https://pdai.tech/md/interview/x-interview.html#如何从数据传输方式理解io流)如何从数据传输方式理解IO流? + +从数据传输方式或者说是运输方式角度看,可以将 IO 类分为: + +1. **字节流**, 字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节。) +2. **字符流**, 字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。 + +**字节是给计算机看的,字符才是给人看的** + +- **字节流** + +![img](https://b2files.173114.xyz/blogimg/2025/03/1457114da1be9556eebadff672d78afc.png) + +- **字符流** + +![img](https://b2files.173114.xyz/blogimg/2025/03/3eba60e14c0777f6815da413d78ffecf.png) + +- **字节转字符**? + +![img](https://b2files.173114.xyz/blogimg/2025/03/974fed10a0d03a29a8ff92ac69e07194.png) + +#### 如何从数据操作上理解IO流? + +从数据来源或者说是操作对象角度看,IO 类可以分为: + +![img](https://b2files.173114.xyz/blogimg/2025/03/b6fba9b0e5f13ac9f1aada1914eab2ad.png) + +#### Java IO设计上使用了什么设计模式? + +**装饰者模式**: 所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。 + +- **装饰者举例** + +设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。 + +下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/ac7c7f4030e037c0fcb919f1111bd3f7.jpg) + +- 以 InputStream 为例 + - InputStream 是抽象组件; + - FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; + - FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 + +![image](https://b2files.173114.xyz/blogimg/2025/03/73713cdf33a3beb26aaa02a0cc8a1de2.png) + +实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 + +```java +FileInputStream fileInputStream = new FileInputStream(filePath); +BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); +``` + +DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 + +### 4.2 5种IO模型 + +#### 什么是阻塞?什么是同步? + +- **阻塞IO 和 非阻塞IO** + +这两个概念是**程序级别**的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了) + +- **同步IO 和 非同步IO** + +这两个概念是**操作系统级别**的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。 + +#### 什么是Linux的IO模型? + +网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。刚才说了,对于一次IO访问(以read举例),**数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间**。所以说,当一个read操作发生时,它会经历两个阶段: + +- 第一阶段:等待数据准备 (Waiting for the data to be ready)。 +- 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。 + +对于socket流而言, + +- 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。 +- 第二步:把数据从内核缓冲区复制到应用进程缓冲区。 + +网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。网络IO的模型大致有如下几种: + +1. 同步阻塞IO(bloking IO) +2. 同步非阻塞IO(non-blocking IO) +3. 多路复用IO(multiplexing IO) +4. 信号驱动式IO(signal-driven IO) +5. 异步IO(asynchronous IO) + +![img](https://b2files.173114.xyz/blogimg/2025/03/d513714dd8a87c7fd2b0bea7d3ae284e.png) + +PS: 这块略复杂,在后面的提供了问答,所以用了最简单的举例结合Linux IO图例帮你快速理解。 + +#### 什么是同步阻塞IO? + +应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。 + +- **举例理解** + +你早上去买有现炸油条,你点单,之后一直等店家做好,期间你啥其它事也做不了。(你就是应用级别,店家就是操作系统级别, 应用被阻塞了不能做其它事) + +- **Linux 中IO图例** + +![img](https://b2files.173114.xyz/blogimg/2025/03/72bebd621376c95b46750ba2a2636528.png) + +#### 什么是同步非阻塞IO? + +应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。 + +- **举例理解** + +你早上去买现炸油条,你点单,点完后每隔一段时间询问店家有没有做好,期间你可以做点其它事情。(你就是应用级别,店家就是操作系统级别,应用可以做其它事情并通过轮询来看操作系统是否完成) + +- **Linux 中IO图例** + +![img](https://b2files.173114.xyz/blogimg/2025/03/1f59eda3a1d50459108d26a60964a72d.png) + +#### 什么是多路复用IO? + +系统调用可能是由多个任务组成的,所以可以拆成多个任务,这就是多路复用。 + +- **举例理解** + +你早上去买现炸油条,点单收钱和炸油条原来都是由一个人完成的,现在他成了瓶颈,所以专门找了个收银员下单收钱,他则专注在炸油条。(本质上炸油条是耗时的瓶颈,将他职责分离出不是瓶颈的部分,比如下单收银,对应到系统级别也时一样的意思) + +- **Linux 中IO图例** + +使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。 + +它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/3f71f559ff70411e3d33d28161b907bf.png) + +#### 有哪些多路复用IO? + +目前流程的多路复用IO实现主要包括四种: `select`、`poll`、`epoll`、`kqueue`。下表是他们的一些重要特性的比较: + +| IO模型 | 相对性能 | 关键思路 | 操作系统 | JAVA支持情况 | +| ------ | -------- | ---------------- | ------------- | ------------------------------------------------------------ | +| select | 较高 | Reactor | windows/Linux | 支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型 | +| poll | 较高 | Reactor | Linux | Linux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式 | +| epoll | 高 | Reactor/Proactor | Linux | Linux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO | +| kqueue | 高 | Proactor | Linux | 目前JAVA的版本不支持 | + +多路复用IO技术最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接请求准备好。其他情况下多路复用IO技术发挥不出来它的优势。另一方面,使用JAVA NIO进行功能实现,相对于传统的Socket套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。 + +#### 什么是信号驱动IO? + +应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。 + +相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。 + +- **举例理解** + +你早上去买现炸油条,门口排队的人多,现在引入了一个叫号系统,点完单后你就可以做自己的事情了,然后等叫号就去拿就可以了。(所以不用再去自己频繁跑去问有没有做好了) + +- **Linux 中IO图例** + +![img](https://b2files.173114.xyz/blogimg/2025/03/52dddf2144b63584f2c9347d5bdd7f68.png) + +#### 什么是异步IO? + +相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。 + +- **举例理解** + +你早上去买现炸油条, 不用去排队了,打开美团外卖下单,然后做其它事,一会外卖自己送上门。(你就是应用级别,店家就是操作系统级别, 应用无需阻塞,这就是非阻塞;系统还可能在处理中,但是立刻响应了应用,这就是异步) + +- **Linux 中IO图例** + +(Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv) + +![img](https://b2files.173114.xyz/blogimg/2025/03/014e3f14da0fc522196450e74463f684.png) + +#### 什么是Reactor模型? + +大多数网络框架都是基于Reactor模型进行设计和开发,Reactor模型基于事件驱动,特别适合处理海量的I/O事件。 + +- **传统的IO模型**? + +这种模式是传统设计,每一个请求到来时,大致都会按照:请求读取->请求解码->服务执行->编码响应->发送答复 这个流程去处理。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/e3435d682d6c6455355cb30eb261c1e8.png) + +服务器会分配一个线程去处理,如果请求暴涨起来,那么意味着需要更多的线程来处理该请求。若请求出现暴涨,线程池的工作线程数量满载那么其它请求就会出现等待或者被抛弃。若每个小任务都可以使用非阻塞的模式,然后基于异步回调模式。这样就大大提高系统的吞吐量,这便引入了Reactor模型。 + +- **Reactor模型中定义的三种角色**: + +1. **Reactor**:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。 +2. **Acceptor**:处理客户端新连接,并分派请求到处理器链中。 +3. **Handler**:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。可用资源池来管理。 + +- **单Reactor单线程模型** + +Reactor线程负责多路分离套接字,accept新连接,并分派请求到handler。Redis使用单Reactor单进程的模型。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/56c683b77082c846b3fff1a5e711589d.png) + +消息处理流程: + +1. Reactor对象通过select监控连接事件,收到事件后通过dispatch进行转发。 +2. 如果是连接建立的事件,则由acceptor接受连接,并创建handler处理后续事件。 +3. 如果不是建立连接事件,则Reactor会分发调用Handler来响应。 +4. handler会完成read->业务处理->send的完整业务流程。 + +- **单Reactor多线程模型** + +将handler的处理池化。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/396a1707faa152da5227fc196ea8427e.png) + +- **多Reactor多线程模型** + +主从Reactor模型: 主Reactor用于响应连接请求,从Reactor用于处理IO操作请求,读写分离了。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/1fe78cdc15fc2131011095592a76fd5c.png) + +#### 什么是Java NIO? + +NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。**传统IO基于字节流和字符流进行操作**,而**NIO基于Channel和Buffer(缓冲区)进行操作**,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。 + +NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/f01a75dce5062ac8b31e5fb78eec67fa.png) + +### 4.3 零拷贝 + +#### [#](https://pdai.tech/md/interview/x-interview.html#传统的io存在什么问题-为什么引入零拷贝的)传统的IO存在什么问题?为什么引入零拷贝的? + +如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。 + +传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。 + +代码通常如下,一般会需要两个系统调用: + +```c +read(file, tmp_buf, len); +write(socket, tmp_buf, len); +``` + +代码很简单,虽然就两行代码,但是这里面发生了不少的事情。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/7f4423a9b07b6efec62da9b8ead8cb03.png) + +首先,**期间共发生了 4 次用户态与内核态的上下文切换**,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。 + +上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。 + +其次,还发生了 **4 次数据拷贝**,其中**两次是 DMA 的拷贝**,另外**两次则是通过 CPU 拷贝**的,下面说一下这个过程: + +- **第一次拷贝**,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。 +- **第二次拷贝**,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。 +- **第三次拷贝**,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。 +- **第四次拷贝**,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。 + +我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。 + +这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。 + +所以,**要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数**。 + +#### mmap + write怎么实现的零拷贝? + +在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。 + +```c +buf = mmap(file, len); +write(sockfd, buf, len); +``` + +mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/665b6b7951011033f570d44fdc42bc74.png) + +具体过程如下: + +- 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区; +- 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据; +- 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。 + +我们可以得知,通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。 + +但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。 + +#### sendfile怎么实现的零拷贝? + +在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下: + +```c +#include +ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); +``` + +它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。 + +首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。 + +其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图: + +![img](https://b2files.173114.xyz/blogimg/2025/03/2c6ecf312b82d5865673e094c67f4870.png) + +但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(**The Scatter-Gather Direct Memory Access**)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。 + +你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性: + +```c +$ ethtool -k eth0 | grep scatter-gather +scatter-gather: on +``` + +于是,从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下: + +- 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里; +- 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝; + +所以,这个过程之中,只进行了 2 次数据拷贝,如下图: + +![img](https://b2files.173114.xyz/blogimg/2025/03/c5dc7c70150fe98c131bbfb2a6f1ef73.png) + +这就是所谓的**零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的**。 + +零拷贝技术的文件传输方式相比传统文件传输的方式,**减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运**。 + +## \ No newline at end of file diff --git a/src/interview/Java/JavaJVM和调优.md b/src/interview/Java/JavaJVM和调优.md new file mode 100644 index 0000000..6e64723 --- /dev/null +++ b/src/interview/Java/JavaJVM和调优.md @@ -0,0 +1,766 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: JVM和调优 +index: true +headerDepth: 3 +order: 5 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- +## 5 JVM和调优 + +> JVM虚拟机和调优相关。 + +### 5.1 类加载机制 + +#### [#](https://pdai.tech/md/interview/x-interview.html#类加载的生命周期)类加载的生命周期? + +其中类加载的过程包括了**加载、验证、准备、解析、初始化**五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)*。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。 + + +![img](https://b2files.173114.xyz/blogimg/2025/03/938e32576ac2e8a5330674ec1afd5d93.png) + +- 类的加载: 查找并加载类的二进制数据 +- 连接 + - 验证: 确保被加载的类的正确性 + - 准备: 为类的静态变量分配内存,并将其初始化为默认值 + - 解析: 把类中的符号引用转换为直接引用 +- 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。 +- 使用: 类访问方法区内的数据结构的接口, 对象是Heap区的数据 +- 卸载: 结束生命周期 + +#### 类加载器的层次? + +![img](https://b2files.173114.xyz/blogimg/2025/03/fb3ff60c80c5b96d109be59e46e7fb30.png) + +- **启动类加载器**: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 +- **扩展类加载器**: Extension ClassLoader,该加载器由`sun.misc.Launcher$ExtClassLoader`实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。 +- **应用程序类加载器**: Application ClassLoader,该类加载器由`sun.misc.Launcher$AppClassLoader`来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 +- **自定义类加载器**: 因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点: + - 在执行非置信代码之前,自动验证数字签名。 + - 动态地创建符合用户特定需要的定制化构建类。 + - 从特定的场所取得java class,例如数据库中和网络中。 + +#### Class.forName()和ClassLoader.loadClass()区别? + +- `Class.forName()`: 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块; +- `ClassLoader.loadClass()`: 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。 +- `Class.forName(name, initialize, loader)`带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。 + +#### JVM有哪些类加载机制? + +- **JVM类加载机制有哪些**? + +1. **全盘负责**,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入 +2. **父类委托**,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类 +3. **缓存机制**,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效 +4. **双亲委派机制**, 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。 + +- **双亲委派机制过程?** + +1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。 +2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。 +3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载; +4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。 + +### 5.2 内存结构 + +#### 说说JVM内存整体的结构?线程私有还是共享的? + +JVM 整体架构,中间部分就是 Java 虚拟机定义的各种运行时数据区域。 + +![jvm-memory-structure](https://b2files.173114.xyz/blogimg/2025/03/349ba45d008c3979602df9157b9597d7.jpg) + +Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。 + +- **线程私有**:程序计数器、虚拟机栈、本地方法区 +- **线程共享**:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存) + +#### 什么是程序计数器(线程私有)? + +PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。 + +- **PC寄存器为什么会被设定为线程私有的?** + +多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。 + +#### 什么是虚拟机栈(线程私有)? + +主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。 + +- **特点?** + +1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器 +2. JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着**入栈**(进栈/压栈),方法执行结束**出栈** +3. 栈不存在垃圾回收问题 +4. 可以通过参数`-Xss`来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度 + +- **该区域有哪些异常**? + +1. 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 **StackOverflowError** 异常 +2. 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个**OutOfMemoryError**异常 + +- **栈帧的内部结构?** + +1. 局部变量表(Local Variables) +2. 操作数栈(Operand Stack)(或称为表达式栈) +3. 动态链接(Dynamic Linking):指向运行时常量池的方法引用 +4. 方法返回地址(Return Address):方法正常退出或异常退出的地址 +5. 一些附加信息 + +![jvm-stack](https://b2files.173114.xyz/blogimg/2025/03/d56db4f1e02265ca20870fa5ca5dbc11.jpg) + +#### Java虚拟机栈如何进行方法计算的? + +以如下代码为例: + +```java +private static int add(int a, int b) { + int c = 0; + c = a + b; + return c; +} +``` + +可以通过jsclass 等工具查看bytecode + +![img](https://b2files.173114.xyz/blogimg/2025/03/f3b9c1353fdd2e9f9e7723f1ff6f8f25.png) + +压栈的步骤如下: + +```java +0: iconst_0 // 0压栈 +1: istore_2 // 弹出int,存放于局部变量2 +2: iload_0 // 把局部变量0压栈 +3: iload_1 // 局部变量1压栈 +4: iadd //弹出2个变量,求和,结果压栈 +5: istore_2 //弹出结果,放于局部变量2 +6: iload_2 //局部变量2压栈 +7: ireturn //返回 +``` + +如果计算100+98的值,那么操作数栈的变化如下图 + +![img](https://b2files.173114.xyz/blogimg/2025/03/a968e2c20822af3419cceea3fe681b9e.png) + +#### 什么是本地方法栈(线程私有)? + +- **本地方法接口** + +一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法。 + +- **本地方法栈(Native Method Stack)** + +Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用 + +#### 什么是方法区(线程共享)? + +方法区(method area)只是 **JVM 规范**中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而**永久代(PermGen)\**是 \*\*Hotspot\*\* 虚拟机特有的概念, Java8 的时候又被\**元空间**取代了,永久代和元空间都可以理解为方法区的落地实现。 + +JDK1.8之前调节方法区大小: + +```bash +-XX:PermSize=N //方法区(永久代)初始大小 +-XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError +``` + +JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是本机内存。参数设置: + +```bash +-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小) +-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小 +``` + +**栈、堆、方法区的交互关系** + +![img](https://b2files.173114.xyz/blogimg/2025/03/e70400fd99edf6f7301d60e20f04a6a0.png) + +#### 永久代和元空间内存使用上的差异? + +Java虚拟机规范中只定义了方法区用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译后的代码等数据 + +1. jdk1.7开始符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,但分离的并不彻底,此时永久代中还保存另一些与类的元数据无关的杂项 +2. jdk8后HotSpot 原永久代中存储的类的**元数据将存储在metaspace**中,而**类的静态变量和字符串常量将放在Java堆中**,metaspace是方法区的一种实现,只不过它使用的不是虚拟机内的内存,而是本地内存。在元空间中保存的数据比永久代中纯粹很多,就只是类的元数据,这些信息只对编译期或JVM的运行时有用。 +3. 永久代有一个JVM本身设置固定大小上线,无法进行调整,而**元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError**。 +4. **符号引用没有存在元空间中,而是存在native heap中**,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM。 + +#### 堆区内存是怎么细分的? + +对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。 + +为了进行高效的垃圾回收,虚拟机把堆内存**逻辑上**划分成三块区域(分代的唯一理由就是优化 GC 性能): + +1. 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代 +2. 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大 + +![jvm-memory](https://b2files.173114.xyz/blogimg/2025/03/2fbbc559cdf20242c200b7c85c941ec0.png) + +Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过 `-Xmx` 和 `-Xms` 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 `OutOfMemoryError` 异常。 + +- **年轻代 (Young Generation)** + +年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 **Minor GC**。年轻一代被分为三个部分——伊甸园(**Eden Memory**)和两个幸存区(**Survivor Memory**,被称为from/to或s0/s1),默认比例是`8:1:1` + +1. 大多数新创建的对象都位于 Eden 内存空间中 +2. 当 Eden 空间被对象填充时,执行**Minor GC**,并将所有幸存者对象移动到一个幸存者空间中 +3. Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的 +4. 经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代 + +- **老年代(Old Generation)** + +旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。 + +大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝 + +#### JVM中对象在堆中的生命周期? + +1. 在 JVM 内存模型的堆中,堆被划分为新生代和老年代 + - 新生代又被进一步划分为 **Eden区** 和 **Survivor区**,Survivor 区由 **From Survivor** 和 **To Survivor** 组成 +2. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区 + - 此时 JVM 会给对象定义一个**对象年轻计数器**(`-XX:MaxTenuringThreshold`) +3. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC) + - JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1 + - 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1 +4. 如果分配的对象超过了`-XX:PetenureSizeThreshold`,对象会**直接被分配到老年代** + +#### JVM中对象的分配过程? + +为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。 + +1. new 的对象先放在伊甸园区,此区有大小限制 +2. 当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区 +3. 然后将伊甸园中的剩余对象移动到幸存者 0 区 +4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1 区 +5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区 +6. 什么时候才会去养老区呢? 默认是 15 次回收标记 +7. 在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理 +8. 若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常 + +#### 什么是 TLAB (Thread Local Allocation Buffer)? + +- 从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内 +- 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为**快速分配策略** +- OpenJDK 衍生出来的 JVM 大都提供了 TLAB 设计 + +#### 为什么要有 TLAB ? + +- 堆区是线程共享的,任何线程都可以访问到堆区中的共享数据 +- 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的 +- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 + +尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。 + +在程序中,可以通过 `-XX:UseTLAB` 设置是否开启 TLAB 空间。 + +默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过 `-XX:TLABWasteTargetPercent` 设置 TLAB 空间所占用 Eden 空间的百分比大小。 + +一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。 + +### 5.3 GC垃圾回收 + +#### 如何判断一个对象是否可以回收? + +- **引用计数算法** + +给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 + +两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 + +正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。 + +- **可达性分析算法** + +通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。 + +![image](https://b2files.173114.xyz/blogimg/2025/03/e9e1029af98b1b016472a96b5e684718.png) + +Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容: + +- 虚拟机栈中引用的对象 +- 本地方法栈中引用的对象 +- 方法区中类静态属性引用的对象 +- 方法区中的常量引用的对象 + +#### 对象有哪些引用类型? + +无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。 + +Java 具有四种强度不同的引用类型。 + +- **强引用** + +被强引用关联的对象不会被回收。 + +使用 new 一个新对象的方式来创建强引用。 + +```java +Object obj = new Object(); +``` + +- **软引用** + +被软引用关联的对象只有在内存不够的情况下才会被回收。 + +使用 SoftReference 类来创建软引用。 + +```java +Object obj = new Object(); +SoftReference sf = new SoftReference(obj); +obj = null; // 使对象只被软引用关联 +``` + +- **弱引用** + +被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。 + +使用 WeakReference 类来实现弱引用。 + +```java +Object obj = new Object(); +WeakReference wf = new WeakReference(obj); +obj = null; +``` + +- **虚引用** + +又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。 + +为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。 + +使用 PhantomReference 来实现虚引用。 + +```java +Object obj = new Object(); +PhantomReference pf = new PhantomReference(obj); +obj = null; +``` + +#### 有哪些基本的垃圾回收算法? + +- **标记 - 清除** + +![image](https://pdai.tech/images/pics/a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg) + +将存活的对象进行标记,然后清理掉未被标记的对象。 + +不足: + +- 标记和清除过程效率都不高; +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。 + +- **标记 - 整理** + +![image](https://pdai.tech/images/pics/902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg) + +让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 + +- **复制** + +![image](https://pdai.tech/images/pics/e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg) + +将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 + +主要不足是只使用了内存的一半。 + +现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。 + +HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。 + +- **分代收集** + +现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 + +一般将堆分为新生代和老年代。 + +- 新生代使用: 复制算法 +- 老年代使用: 标记 - 清除 或者 标记 - 整理 算法 + +#### 分代收集算法和分区收集算法区别? + +![img](https://b2files.173114.xyz/blogimg/2025/03/4e3fa436564caa5ddbc428ea8ee785c7.jpg) + +- **分代收集算法** + +当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据 对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据 各年代特点分别采用最适当的 GC 算法 + +在新生代-复制算法: + +每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量 存活对象的复制成本就可以完成收集 + +在老年代-标记整理算法: + +因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存. + +1. **ParNew**: 一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 `-XX:ParallelGCThreads` 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。 +2. **CMS**: 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除。 + +- **分区收集算法** + +分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次 GC 所产生的停顿。 + +1. **G1**: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。 +2. **ZGC**: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。 + +#### 什么是Minor GC、Major GC、Full GC? + +JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。 + +针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC) + +- 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为: + - 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集 + - 老年代收集(Major GC/Old GC):只是老年代的垃圾收集 + - 目前,只有 CMS GC 会有单独收集老年代的行为 + - 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收 + - 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集 + - 目前只有 G1 GC 会有这种行为 +- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾 + +#### 说说JVM内存分配策略? + +- **对象优先在 Eden 分配** + +大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。 + +- **大对象直接进入老年代** + +大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 + +经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 + +-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 + +- **长期存活的对象进入老年代** + +为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。 + +-XX:MaxTenuringThreshold 用来定义年龄的阈值。 + +- **动态对象年龄判定** + +虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 + +- **空间分配担保** + +在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。 + +如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。 + +#### 什么情况下会触发Full GC? + +对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: + +- **调用 System.gc()** + +只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 + +- **老年代空间不足** + +老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。 + +为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 + +- **空间分配担保失败** + +使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。 + +- **JDK 1.7 及以前的永久代空间不足** + +在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 + +当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。 + +为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 + +- **Concurrent Mode Failure** + +执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 + +#### Hotspot中有哪些垃圾回收器? + +![image](https://b2files.173114.xyz/blogimg/2025/03/cc154755877a9d45e5e41b36e16190b4.jpg) + +以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 + +- 单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程; +- 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 + +1. **Serial 收集器** + +![image](https://b2files.173114.xyz/blogimg/2025/03/01e5081f061083e9988f50b843bdce08.jpg) + +Serial 翻译为串行,也就是说它以串行的方式执行。 + +它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 + +它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 + +它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 + +1. **ParNew 收集器** + +![image](https://b2files.173114.xyz/blogimg/2025/03/b9bf6e1727fb42311c1eb7a0926c0d57.jpg) + +它是 Serial 收集器的多线程版本。 + +是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。 + +默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。 + +1. **Parallel Scavenge 收集器** + +与 ParNew 一样是多线程收集器。 + +其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 + +停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 + +缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 + +可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。 + +1. **Serial Old 收集器** + +![image](https://b2files.173114.xyz/blogimg/2025/03/16993beb849661df16e5eec8c15d2320.jpg) + +是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途: + +- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 +- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 + +1. **Parallel Old 收集器** + +![image](https://b2files.173114.xyz/blogimg/2025/03/fe7c03208f3828c014e4a724537c61ac.jpg) + +是 Parallel Scavenge 收集器的老年代版本。 + +在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 + +1. **CMS 收集器** + +![image](https://b2files.173114.xyz/blogimg/2025/03/f338848f362071a01c826380011407ef.jpg) + +CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 + +分为以下四个流程: + +- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 +- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 +- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 +- 并发清除: 不需要停顿。 + +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 + +具有以下缺点: + +- 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 +- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 + +1. **G1 收集器** + +G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 + +堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 **G1 可以直接对新生代和老年代一起回收**。 + +![image](https://b2files.173114.xyz/blogimg/2025/03/d23ff703a0796e77bb09239fe2493028.png) + +G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。 + +![image](https://b2files.173114.xyz/blogimg/2025/03/18e82055a2eaa3e5a365e35eb7865b7f.png) + +**通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收**。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。 + +每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。 + +![image](https://b2files.173114.xyz/blogimg/2025/03/ddb97eb22a6627fec00cb07331b7eb81.jpg) + +如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤: + +- 初始标记 +- 并发标记 +- 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 +- 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 + +具备如下特点: + +- 空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 +- 可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。 + +### 5.4 问题排查 + +#### 常见的Linux定位问题的工具? + +- 文本操作 + - 文本查找 - grep + - 文本分析 - awk + - 文本处理 - sed +- 文件操作 + - 文件监听 - tail + - 文件查找 - find +- 网络和进程 + - 网络接口 - ifconfig + - 防火墙 - iptables -L + - 路由表 - route -n + - netstat +- 其它常用 + - 进程 ps -ef | grep java + - 分区大小 df -h + - 内存 free -m + - 硬盘大小 fdisk -l |grep Disk + - top + - 环境变量 env + +#### JDK自带的定位问题的工具? + +- **jps** jps是jdk提供的一个查看当前java进程的小工具, 可以看做是JavaVirtual Machine Process Status Tool的缩写。 + +```bash +jps –l # 输出输出完全的包名,应用主类名,jar的完全路径名 +``` + +- **jstack** jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息。 + +```bash +# 基本 +jstack 2815 +jstack -m 2815 # java和native c/c++框架的所有栈信息 +jstack -l 2815 # 额外的锁信息列表,查看是否死锁 +``` + +- **jinfo** jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括Java System属性和JVM命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息 + +```bash +jinfo 2815 # 输出当前 jvm 进程的全部参数和系统属性 +``` + +- **jmap** 命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。 + +```bash +# 查看堆的情况 +jmap -heap 2815 + +# dump +jmap -dump:live,format=b,file=/tmp/heap2.bin 2815 +``` + +- **jstat** jstat参数众多,但是使用一个就够了 + +```bash +jstat -gcutil 2815 1000 +``` + +#### 如何使用在线调试工具Arthas? + +举几个例子 + +- **查看最繁忙的线程,以及是否有阻塞情况发生**? + +场景:我想看下查看最繁忙的线程,以及是否有阻塞情况发生? 常规查看线程,一般我们可以通过 top 等系统命令进行查看,但是那毕竟要很多个步骤,很麻烦。 + +```bash +thread -n 3 # 查看最繁忙的三个线程栈信息 +thread # 以直观的方式展现所有的线程情况 +thread -b #找出当前阻塞其他线程的线程 +``` + +- **确认某个类是否已被系统加载**? + +场景:我新写了一个类或者一个方法,我想知道新写的代码是否被部署了? + +```bash +# 即可以找到需要的类全路径,如果存在的话 +sc *MyServlet + +# 查看这个某个类所有的方法 +sm pdai.tech.servlet.TestMyServlet * + +# 查看某个方法的信息,如果存在的话 +sm pdai.tech.servlet.TestMyServlet testMethod +``` + +- **如何查看一个class类的源码信息**? + +场景:我新修改的内容在方法内部,而上一个步骤只能看到方法,这时候可以反编译看下源码 + +```bash +# 直接反编译出java 源代码,包含一此额外信息的 +jad pdai.tech.servlet.TestMyServlet +``` + +- **如何跟踪某个方法的返回值、入参**? + +场景:我想看下我新加的方法在线运行的参数和返回值? + +```bash +# 同时监控入参,返回值,及异常 +watch pdai.tech.servlet.TestMyServlet testMethod "{params, returnObj, throwExp}" -e -x 2 +``` + +- **如何看方法调用栈的信息**? + +场景:我想看下某个方法的调用栈的信息? + +```bash +stack pdai.tech.servlet.TestMyServlet testMethod +``` + +运行此命令之后需要即时触发方法才会有响应的信息打印在控制台上 + +- **找到最耗时的方法调用**? + +场景:testMethod这个方法入口响应很慢,如何找到最耗时的子调用? + +```bash +# 执行的时候每个子调用的运行时长,可以找到最耗时的子调用。 +stack pdai.tech.servlet.TestMyServlet testMethod +``` + +运行此命令之后需要即时触发方法才会有响应的信息打印在控制台上,然后一层一层看子调用。 + +- **如何临时更改代码运行**? + +场景:我找到了问题所在,能否线上直接修改测试,而不需要在本地改了代码后,重新打包部署,然后重启观察效果? + +```bash +# 先反编译出class源码 +jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java + +# 然后使用外部工具编辑内容 +mc /tmp/UserController.java -d /tmp # 再编译成class + +# 最后,重新载入定义的类,就可以实时验证你的猜测了 +redefine /tmp/com/example/demo/arthas/user/UserController.class +``` + +如上,是直接更改线上代码的方式,但是一般好像是编译不成功的。所以,最好是本地ide编译成 class文件后,再上传替换为好! + +总之,已经完全不用重启和发布了!这个功能真的很方便,比起重启带来的代价,真的是不可比的。比如,重启时可能导致负载重分配,选主等等问题,就不是你能控制的了。 + +- **我如何测试某个方法的性能问题**? + +```bash +monitor -c 5 demo.MathGame primeFactors +``` + +#### 如何使用Idea的远程调试? + +要让远程服务器运行的代码支持远程调试,则启动的时候必须加上特定的JVM参数,这些参数是: + +```bash +-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=127.0.0.1:5555 +``` + +#### 复杂综合类型问题的定位思路? + +![img](https://b2files.173114.xyz/blogimg/2025/03/c01589f0995701911e937de084138f2b.png) + +## \ No newline at end of file diff --git a/src/interview/Java/Java基础.md b/src/interview/Java/Java基础.md new file mode 100644 index 0000000..6f53d20 --- /dev/null +++ b/src/interview/Java/Java基础.md @@ -0,0 +1,706 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Java基础 +index: true +headerDepth: 3 +order: 1 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +## 1 Java基础 + +> Java基础部分,包括语法基础,泛型,注解,异常,反射和其它(如SPI机制等)。 + +### 1.1 语法基础 +#### 面向对象特性? +- **封装** + +利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 + +优点: + +- 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改 +- 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 +- 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能 +- 提高软件的可重用性 +- 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的 + +以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 + +注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。 + +```java +public class Person { + + private String name; + private int gender; + private int age; + + public String getName() { + return name; + } + + public String getGender() { + return gender == 0 ? "man" : "woman"; + } + + public void work() { + if (18 <= age && age <= 50) { + System.out.println(name + " is working very hard!"); + } else { + System.out.println(name + " can't work any more!"); + } + } +} +``` + +- **继承** + +继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。 + +继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。 + +Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型** 。 + +```java +Animal animal = new Cat(); +``` + +- **多态** + +多态分为编译时多态和运行时多态: + +- 编译时多态主要指方法的重载 +- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定 + +运行时多态有三个条件: + +- 继承 +- 覆盖(重写) +- 向上转型 + +下面的代码中,乐器类(Instrument)有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。 + +```java +public class Instrument { + public void play() { + System.out.println("Instrument is playing..."); + } +} + +public class Wind extends Instrument { + public void play() { + System.out.println("Wind is playing..."); + } +} + +public class Percussion extends Instrument { + public void play() { + System.out.println("Percussion is playing..."); + } +} + +public class Music { + public static void main(String[] args) { + List instruments = new ArrayList<>(); + instruments.add(new Wind()); + instruments.add(new Percussion()); + for(Instrument instrument : instruments) { + instrument.play(); + } + } +} +``` + +#### a = a + b 与 a += b 的区别 + ++= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。 + +```java +byte a = 127; +byte b = 127; +b = a + b; // error : cannot convert from int to byte +b += a; // ok +``` + +(因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错) + +#### 3*0.1 == 0.3 将会返回什么? true 还是 false? + +false,因为有些浮点数不能完全精确的表示出来。 + +#### 能在 Switch 中使用 String 吗? + +从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。 + +#### 对equals()和hashCode()的理解? + +- **为什么在重写 equals 方法的时候需要重写 hashCode 方法**? + +因为有强制的规范指定需要同时重写 hashcode 与 equals 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。 + +- **有没有可能两个不相等的对象有相同的 hashcode**? + +有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定。 + +- **两个相同的对象会有不同的 hash code 吗**? + +不能,根据 hash code 的规定,这是不可能的。 + +#### final、finalize 和 finally 的不同之处? + +final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。 + +Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,但是什么时候调用 finalize 没有保证。 + +finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。 + +#### String、StringBuffer与StringBuilder的区别? + +第一点: 可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。 + +第二点: 线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。 + +#### 接口与抽象类的区别? + +- 一个子类只能继承一个抽象类, 但能实现多个接口 +- 抽象类可以有构造方法, 接口没有构造方法 +- 抽象类可以有普通成员变量, 接口没有普通成员变量 +- 抽象类和接口都可有静态成员变量, 抽象类中静态成员变量访问类型任意,接口只能public static final(默认) +- 抽象类可以没有抽象方法, 抽象类可以有普通方法;接口在JDK8之前都是抽象方法,在JDK8可以有default方法,在JDK9中允许有私有普通方法 +- 抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用) +- 抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法,在JDK9中允许有private方法 + +#### this() & super()在构造方法中的区别? + +- 调用super()必须写在子类构造方法的第一行, 否则编译不通过 +- super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行 +- 尽管可以用this调用一个构造器, 却不能调用2个 +- this和super不能出现在同一个构造器中, 否则编译不通过 +- this()、super()都指的对象,不可以在static环境中使用 +- 本质this指向本对象的指针。super是一个关键字 + +#### Java移位运算符? + +java中有三种移位运算符 + +- `<<` :左移运算符,`x << 1`,相当于x乘以2(不溢出的情况下),低位补0 +- `>>` :带符号右移,`x >> 1`,相当于x除以2,正数高位补0,负数高位补1 +- `>>>` :无符号右移,忽略符号位,空位都以0补齐 + +### 1.2 泛型 + +#### 为什么需要泛型? + +1. **适用于多种数据类型执行相同的代码** + +```java +private static int add(int a, int b) { + System.out.println(a + "+" + b + "=" + (a + b)); + return a + b; +} + +private static float add(float a, float b) { + System.out.println(a + "+" + b + "=" + (a + b)); + return a + b; +} + +private static double add(double a, double b) { + System.out.println(a + "+" + b + "=" + (a + b)); + return a + b; +} + +``` + +如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法: + +```java +private static double add(T a, T b) { + System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue())); + return a.doubleValue() + b.doubleValue(); +} +``` + +- **泛型中的类型在使用时指定,不需要强制类型转换**(**类型安全**,编译器会**检查类型**) + +看下这个例子: + +```java +List list = new ArrayList(); +list.add("xxString"); +list.add(100d); +list.add(new Person()); +``` + +我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现`java.lang.ClassCastException`异常。 + +引入泛型,它将提供类型的约束,提供编译前的检查: + +```java +List list = new ArrayList(); + +// list中只能放String, 不能放其它类型的元素 +``` + +#### 泛型类如何定义使用? + +- 从一个简单的泛型类看起: + +```java +class Point{ // 此处可以随便写标识符号,T是type的简称 + private T var ; // var的类型由T指定,即:由外部指定 + public T getVar(){ // 返回值的类型由外部决定 + return var ; + } + public void setVar(T var){ // 设置的类型也由外部决定 + this.var = var ; + } +} +public class GenericsDemo06{ + public static void main(String args[]){ + Point p = new Point() ; // 里面的var类型为String类型 + p.setVar("it") ; // 设置字符串 + System.out.println(p.getVar().length()) ; // 取得字符串的长度 + } +} +``` + +- 多元泛型 + +```java +class Notepad{ // 此处指定了两个泛型类型 + private K key ; // 此变量的类型由外部决定 + private V value ; // 此变量的类型由外部决定 + public K getKey(){ + return this.key ; + } + public V getValue(){ + return this.value ; + } + public void setKey(K key){ + this.key = key ; + } + public void setValue(V value){ + this.value = value ; + } +} +public class GenericsDemo09{ + public static void main(String args[]){ + Notepad t = null ; // 定义两个泛型类型的对象 + t = new Notepad() ; // 里面的key为String,value为Integer + t.setKey("汤姆") ; // 设置第一个内容 + t.setValue(20) ; // 设置第二个内容 + System.out.print("姓名;" + t.getKey()) ; // 取得信息 + System.out.print(",年龄;" + t.getValue()) ; // 取得信息 + + } +} + +``` + +#### 泛型接口如何定义使用? + +- 简单的泛型接口 + +```java +interface Info{ // 在接口上定义泛型 + public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型 +} +class InfoImpl implements Info{ // 定义泛型接口的子类 + private T var ; // 定义属性 + public InfoImpl(T var){ // 通过构造方法设置属性内容 + this.setVar(var) ; + } + public void setVar(T var){ + this.var = var ; + } + public T getVar(){ + return this.var ; + } +} +public class GenericsDemo24{ + public static void main(String arsg[]){ + Info i = null; // 声明接口对象 + i = new InfoImpl("汤姆") ; // 通过子类实例化对象 + System.out.println("内容:" + i.getVar()) ; + } +} +``` + +#### 泛型方法如何定义使用? + +泛型方法,是在调用方法的时候指明泛型的具体类型。 + +- 定义泛型方法语法格式 + +![img](https://b2files.173114.xyz/blogimg/2025/03/4eebe47eb4f2f9d0833c0d687d44b940.png) + +- 调用泛型方法语法格式 + +![img](https://b2files.173114.xyz/blogimg/2025/03/2e383a7c2bbbf60c5bb35c95f128edf9.png) + +说明一下,定义泛型方法时,必须在返回值前边加一个``,来声明这是一个泛型方法,持有一个泛型`T`,然后才可以用泛型T作为方法的返回值。 + +`Class`的作用就是指明泛型的具体类型,而`Class`类型的变量c,可以用来创建泛型类的对象。 + +为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。 + +泛型方法要求的参数是`Class`类型,而`Class.forName()`方法的返回值也是`Class`,因此可以用`Class.forName()`作为参数。其中,`forName()`方法中的参数是何种类型,返回的`Class`就是何种类型。在本例中,`forName()`方法中传入的是User类的完整路径,因此返回的是`Class`类型的对象,因此调用泛型方法时,变量c的类型就是`Class`,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。 + +当然,泛型方法不是仅仅可以有一个参数`Class`,可以根据需要添加其他参数。 + +**为什么要使用泛型方法呢**?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。 + +#### 泛型的上限和下限? + +在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。 + +上限 + +```java +class Info{ // 此处泛型只能是数字类型 + private T var ; // 定义泛型变量 + public void setVar(T var){ + this.var = var ; + } + public T getVar(){ + return this.var ; + } + public String toString(){ // 直接打印 + return this.var.toString() ; + } +} +public class demo1{ + public static void main(String args[]){ + Info i1 = new Info() ; // 声明Integer的泛型对象 + } +} +``` + +下限 + +```java +class Info{ + private T var ; // 定义泛型变量 + public void setVar(T var){ + this.var = var ; + } + public T getVar(){ + return this.var ; + } + public String toString(){ // 直接打印 + return this.var.toString() ; + } +} +public class GenericsDemo21{ + public static void main(String args[]){ + Info i1 = new Info() ; // 声明String的泛型对象 + Info i2 = new Info() ; // 声明Object的泛型对象 + i1.setVar("hello") ; + i2.setVar(new Object()) ; + fun(i1) ; + fun(i2) ; + } + public static void fun(Info temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类 + System.out.print(temp + ", ") ; + } +} +``` + +#### 如何理解Java中的泛型是伪泛型? + +泛型中类型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 + +### 1.3 注解 + +#### 注解的作用? + +注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面: + +- 生成文档,通过代码里标识的元数据生成javadoc文档。 +- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。 +- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。 +- 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。 + +#### 注解的常见分类? + +- **Java自带的标准注解**,包括`@Override`、`@Deprecated`和`@SuppressWarnings`,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。 + +- **元注解**,元注解是用于定义注解的注解,包括`@Retention`、`@Target`、`@Inherited`、`@Documented` + + - `@Retention`用于标明注解被保留的阶段 + + - `@Target`用于标明注解使用的范围 + + - `@Documented`用于标明是否生成javadoc文档 + +- **自定义注解**,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。 + +### 1.4 异常 + +#### Java异常类层次结构? + +- **Throwable** 是 Java 语言中所有错误与异常的超类。 + + - **Error** 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 + + - **Exception** 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/5b8dd265a5f94619125f81a2829c14c1.png) + +- **运行时异常** + +都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 + +运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 + +- **非运行时异常** (编译异常) + +是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。 + +#### 可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)区别? + +- **可查异常**(编译器要求必须处置的异常): + +正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。 + +除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。 + +- **不可查异常**(编译器不要求强制处置的异常) + +包括运行时异常(RuntimeException与其子类)和错误(Error)。 + +#### throw和throws的区别? + +- **异常的申明(throws)** + +在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示: + +```java +public static void method() throws IOException, FileNotFoundException{ + //something statements +} +``` + +- **异常的抛出(throw)** + +```java +public static double method(int value) { + if(value == 0) { + throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常 + } + return 5.0 / value; +} +``` + +#### Java 7 的 try-with-resource? + +如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。 + +```java +public void automaticallyCloseResource() { + File file = new File("./tmp.txt"); + try (FileInputStream inputStream = new FileInputStream(file);) { + // use the inputStream to read a file + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +``` + +#### 异常的底层? + +提到JVM处理异常的机制,就需要提及Exception Table,以下称为异常表。我们暂且不急于介绍异常表,先看一个简单的 Java 处理异常的小例子。 + +```java +public static void simpleTryCatch() { + try { + testNPE(); + } catch (Exception e) { + e.printStackTrace(); + } +} +``` + +使用javap来分析这段代码(需要先使用javac编译) + +```java +//javap -c Main + public static void simpleTryCatch(); + Code: + 0: invokestatic #3 // Method testNPE:()V + 3: goto 11 + 6: astore_0 + 7: aload_0 + 8: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V + 11: return + Exception table: + from to target type + 0 3 6 Class java/lang/Exception +``` + +看到上面的代码,应该会有会心一笑,因为终于看到了Exception table,也就是我们要研究的异常表。 + +异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下 + +- **from** 可能发生异常的起始点 +- **to** 可能发生异常的结束点 +- **target** 上述from和to之前发生异常后的异常处理者的位置 +- **type** 异常处理者处理的异常的类信息 + +### 1.5 反射 + +#### 什么是反射? + +JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/3b98c1ae126cb173dc1245761f418573.png) + +#### 反射的使用? + +在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private) + +- Class类对象的获取 + +```java + @Test + public void classTest() throws Exception { + // 获取Class对象的三种方式 + logger.info("根据类名: \t" + User.class); + logger.info("根据对象: \t" + new User().getClass()); + logger.info("根据全限定类名:\t" + Class.forName("com.test.User")); + // 常用的方法 + logger.info("获取全限定类名:\t" + userClass.getName()); + logger.info("获取类名:\t" + userClass.getSimpleName()); + logger.info("实例化:\t" + userClass.newInstance()); + } +``` + +- Constructor类及其用法 +- Field类及其用法 +- Method类及其用法 + +#### getName、getCanonicalName与getSimpleName的区别? + +- getSimpleName:只获取类名 +- getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。 +- getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。 + +### 1.6 SPI机制 + +#### 什么是SPI机制? + +SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 **解耦**。 + +SPI整体机制图如下: + +![img](https://b2files.173114.xyz/blogimg/2025/03/4c506154da87b62d5de62b4d8d05130f.jpg) + +当服务的提供者提供了一种接口的实现之后,需要在classpath下的`META-INF/services/`目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的`META-INF/services/`中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:`java.util.ServiceLoader`。 + +#### SPI机制的应用? + +- SPI机制 - JDBC DriverManager + +在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。**而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现**。 + +- JDBC接口定义 + +首先在java中定义了接口`java.sql.Driver`,并没有具体的实现,具体的实现都是由不同厂商来提供的。 + +- mysql实现 + +在mysql的jar包`mysql-connector-java-6.0.6.jar`中,可以找到`META-INF/services`目录,该目录下会有一个名字为`java.sql.Driver`的文件,文件内容是`com.mysql.cj.jdbc.Driver`,这里面的内容就是针对Java中定义的接口的实现。 + +- postgresql实现 + +同样在postgresql的jar包`postgresql-42.0.0.jar`中,也可以找到同样的配置文件,文件内容是`org.postgresql.Driver`,这是postgresql对Java的`java.sql.Driver`的实现。 + +- 使用方法 + +上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用`Class.forName("com.mysql.jdbc.Driver")`来加载驱动了,而是直接使用如下代码: + +```java +String url = "jdbc:xxxx://xxxx:xxxx/xxxx"; +Connection conn = DriverManager.getConnection(url,username,password); +..... +``` + +#### SPI机制的简单示例? + +我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。 + +- 先定义好接口 + +```java +public interface Search { + public List searchDoc(String keyword); +} +``` + +- 文件搜索实现 + +```java +public class FileSearch implements Search{ + @Override + public List searchDoc(String keyword) { + System.out.println("文件搜索 "+keyword); + return null; + } +} +``` + +- 数据库搜索实现 + +```java +public class DatabaseSearch implements Search{ + @Override + public List searchDoc(String keyword) { + System.out.println("数据搜索 "+keyword); + return null; + } +} +``` + +- resources 接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:`com.cainiao.ys.spi.learn.Search`,里面加上我们需要用到的实现类 + +```xml +com.cainiao.ys.spi.learn.FileSearch +``` + +- 测试方法 + +```java +public class TestCase { + public static void main(String[] args) { + ServiceLoader s = ServiceLoader.load(Search.class); + Iterator iterator = s.iterator(); + while (iterator.hasNext()) { + Search search = iterator.next(); + search.searchDoc("hello world"); + } + } +} +``` + +可以看到输出结果:文件搜索 hello world + +如果在`com.cainiao.ys.spi.learn.Search`文件里写上两个实现类,那最后的输出结果就是两行了。 + +这就是因为`ServiceLoader.load(Search.class)`在加载某接口时,会去`META-INF/services`下找接口的全限定名文件,再根据里面的内容加载相应的实现类。 + +这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的`META-INF/services`下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。 + + + diff --git a/src/interview/java/java.md b/src/interview/Java/Java并发.md similarity index 59% rename from src/interview/java/java.md rename to src/interview/Java/Java并发.md index 1fef0d1..166c97e 100644 --- a/src/interview/java/java.md +++ b/src/interview/Java/Java并发.md @@ -6,799 +6,15 @@ # expanded: true # link: true # index: true -title: Java全栈面试 +title: Java 并发 index: true headerDepth: 3 +order: 3 # icon: laptop-code # sidebar: true # toc: true # editLink: false --- - -## 1 Java基础 - -> Java基础部分,包括语法基础,泛型,注解,异常,反射和其它(如SPI机制等)。 - -### 1.1 语法基础 -#### 面向对象特性? -- **封装** - -利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 - -优点: - -- 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改 -- 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 -- 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能 -- 提高软件的可重用性 -- 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的 - -以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 - -注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。 - -```java -public class Person { - - private String name; - private int gender; - private int age; - - public String getName() { - return name; - } - - public String getGender() { - return gender == 0 ? "man" : "woman"; - } - - public void work() { - if (18 <= age && age <= 50) { - System.out.println(name + " is working very hard!"); - } else { - System.out.println(name + " can't work any more!"); - } - } -} -``` - -- **继承** - -继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。 - -继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。 - -Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型** 。 - -```java -Animal animal = new Cat(); -``` - -- **多态** - -多态分为编译时多态和运行时多态: - -- 编译时多态主要指方法的重载 -- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定 - -运行时多态有三个条件: - -- 继承 -- 覆盖(重写) -- 向上转型 - -下面的代码中,乐器类(Instrument)有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。 - -```java -public class Instrument { - public void play() { - System.out.println("Instrument is playing..."); - } -} - -public class Wind extends Instrument { - public void play() { - System.out.println("Wind is playing..."); - } -} - -public class Percussion extends Instrument { - public void play() { - System.out.println("Percussion is playing..."); - } -} - -public class Music { - public static void main(String[] args) { - List instruments = new ArrayList<>(); - instruments.add(new Wind()); - instruments.add(new Percussion()); - for(Instrument instrument : instruments) { - instrument.play(); - } - } -} -``` - -#### a = a + b 与 a += b 的区别 - -+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。 - -```java -byte a = 127; -byte b = 127; -b = a + b; // error : cannot convert from int to byte -b += a; // ok -``` - -(因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错) - -#### 3*0.1 == 0.3 将会返回什么? true 还是 false? - -false,因为有些浮点数不能完全精确的表示出来。 - -#### 能在 Switch 中使用 String 吗? - -从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。 - -#### 对equals()和hashCode()的理解? - -- **为什么在重写 equals 方法的时候需要重写 hashCode 方法**? - -因为有强制的规范指定需要同时重写 hashcode 与 equals 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。 - -- **有没有可能两个不相等的对象有相同的 hashcode**? - -有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定。 - -- **两个相同的对象会有不同的 hash code 吗**? - -不能,根据 hash code 的规定,这是不可能的。 - -#### final、finalize 和 finally 的不同之处? - -final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。 - -Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,但是什么时候调用 finalize 没有保证。 - -finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。 - -#### String、StringBuffer与StringBuilder的区别? - -第一点: 可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。 - -第二点: 线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。 - -#### 接口与抽象类的区别? - -- 一个子类只能继承一个抽象类, 但能实现多个接口 -- 抽象类可以有构造方法, 接口没有构造方法 -- 抽象类可以有普通成员变量, 接口没有普通成员变量 -- 抽象类和接口都可有静态成员变量, 抽象类中静态成员变量访问类型任意,接口只能public static final(默认) -- 抽象类可以没有抽象方法, 抽象类可以有普通方法;接口在JDK8之前都是抽象方法,在JDK8可以有default方法,在JDK9中允许有私有普通方法 -- 抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用) -- 抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法,在JDK9中允许有private方法 - -#### this() & super()在构造方法中的区别? - -- 调用super()必须写在子类构造方法的第一行, 否则编译不通过 -- super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行 -- 尽管可以用this调用一个构造器, 却不能调用2个 -- this和super不能出现在同一个构造器中, 否则编译不通过 -- this()、super()都指的对象,不可以在static环境中使用 -- 本质this指向本对象的指针。super是一个关键字 - -#### Java移位运算符? - -java中有三种移位运算符 - -- `<<` :左移运算符,`x << 1`,相当于x乘以2(不溢出的情况下),低位补0 -- `>>` :带符号右移,`x >> 1`,相当于x除以2,正数高位补0,负数高位补1 -- `>>>` :无符号右移,忽略符号位,空位都以0补齐 - -### 1.2 泛型 - -#### 为什么需要泛型? - -1. **适用于多种数据类型执行相同的代码** - -```java -private static int add(int a, int b) { - System.out.println(a + "+" + b + "=" + (a + b)); - return a + b; -} - -private static float add(float a, float b) { - System.out.println(a + "+" + b + "=" + (a + b)); - return a + b; -} - -private static double add(double a, double b) { - System.out.println(a + "+" + b + "=" + (a + b)); - return a + b; -} - -``` - -如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法: - -```java -private static double add(T a, T b) { - System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue())); - return a.doubleValue() + b.doubleValue(); -} -``` - -- **泛型中的类型在使用时指定,不需要强制类型转换**(**类型安全**,编译器会**检查类型**) - -看下这个例子: - -```java -List list = new ArrayList(); -list.add("xxString"); -list.add(100d); -list.add(new Person()); -``` - -我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现`java.lang.ClassCastException`异常。 - -引入泛型,它将提供类型的约束,提供编译前的检查: - -```java -List list = new ArrayList(); - -// list中只能放String, 不能放其它类型的元素 -``` - -#### 泛型类如何定义使用? - -- 从一个简单的泛型类看起: - -```java -class Point{ // 此处可以随便写标识符号,T是type的简称 - private T var ; // var的类型由T指定,即:由外部指定 - public T getVar(){ // 返回值的类型由外部决定 - return var ; - } - public void setVar(T var){ // 设置的类型也由外部决定 - this.var = var ; - } -} -public class GenericsDemo06{ - public static void main(String args[]){ - Point p = new Point() ; // 里面的var类型为String类型 - p.setVar("it") ; // 设置字符串 - System.out.println(p.getVar().length()) ; // 取得字符串的长度 - } -} -``` - -- 多元泛型 - -```java -class Notepad{ // 此处指定了两个泛型类型 - private K key ; // 此变量的类型由外部决定 - private V value ; // 此变量的类型由外部决定 - public K getKey(){ - return this.key ; - } - public V getValue(){ - return this.value ; - } - public void setKey(K key){ - this.key = key ; - } - public void setValue(V value){ - this.value = value ; - } -} -public class GenericsDemo09{ - public static void main(String args[]){ - Notepad t = null ; // 定义两个泛型类型的对象 - t = new Notepad() ; // 里面的key为String,value为Integer - t.setKey("汤姆") ; // 设置第一个内容 - t.setValue(20) ; // 设置第二个内容 - System.out.print("姓名;" + t.getKey()) ; // 取得信息 - System.out.print(",年龄;" + t.getValue()) ; // 取得信息 - - } -} - -``` - -#### 泛型接口如何定义使用? - -- 简单的泛型接口 - -```java -interface Info{ // 在接口上定义泛型 - public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型 -} -class InfoImpl implements Info{ // 定义泛型接口的子类 - private T var ; // 定义属性 - public InfoImpl(T var){ // 通过构造方法设置属性内容 - this.setVar(var) ; - } - public void setVar(T var){ - this.var = var ; - } - public T getVar(){ - return this.var ; - } -} -public class GenericsDemo24{ - public static void main(String arsg[]){ - Info i = null; // 声明接口对象 - i = new InfoImpl("汤姆") ; // 通过子类实例化对象 - System.out.println("内容:" + i.getVar()) ; - } -} -``` - -#### 泛型方法如何定义使用? - -泛型方法,是在调用方法的时候指明泛型的具体类型。 - -- 定义泛型方法语法格式 - -![img](https://b2files.173114.xyz/blogimg/2025/03/4eebe47eb4f2f9d0833c0d687d44b940.png) - -- 调用泛型方法语法格式 - -![img](https://b2files.173114.xyz/blogimg/2025/03/2e383a7c2bbbf60c5bb35c95f128edf9.png) - -说明一下,定义泛型方法时,必须在返回值前边加一个``,来声明这是一个泛型方法,持有一个泛型`T`,然后才可以用泛型T作为方法的返回值。 - -`Class`的作用就是指明泛型的具体类型,而`Class`类型的变量c,可以用来创建泛型类的对象。 - -为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。 - -泛型方法要求的参数是`Class`类型,而`Class.forName()`方法的返回值也是`Class`,因此可以用`Class.forName()`作为参数。其中,`forName()`方法中的参数是何种类型,返回的`Class`就是何种类型。在本例中,`forName()`方法中传入的是User类的完整路径,因此返回的是`Class`类型的对象,因此调用泛型方法时,变量c的类型就是`Class`,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。 - -当然,泛型方法不是仅仅可以有一个参数`Class`,可以根据需要添加其他参数。 - -**为什么要使用泛型方法呢**?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。 - -#### 泛型的上限和下限? - -在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。 - -上限 - -```java -class Info{ // 此处泛型只能是数字类型 - private T var ; // 定义泛型变量 - public void setVar(T var){ - this.var = var ; - } - public T getVar(){ - return this.var ; - } - public String toString(){ // 直接打印 - return this.var.toString() ; - } -} -public class demo1{ - public static void main(String args[]){ - Info i1 = new Info() ; // 声明Integer的泛型对象 - } -} -``` - -下限 - -```java -class Info{ - private T var ; // 定义泛型变量 - public void setVar(T var){ - this.var = var ; - } - public T getVar(){ - return this.var ; - } - public String toString(){ // 直接打印 - return this.var.toString() ; - } -} -public class GenericsDemo21{ - public static void main(String args[]){ - Info i1 = new Info() ; // 声明String的泛型对象 - Info i2 = new Info() ; // 声明Object的泛型对象 - i1.setVar("hello") ; - i2.setVar(new Object()) ; - fun(i1) ; - fun(i2) ; - } - public static void fun(Info temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类 - System.out.print(temp + ", ") ; - } -} -``` - -#### 如何理解Java中的泛型是伪泛型? - -泛型中类型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 - -### 1.3 注解 - -#### 注解的作用? - -注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面: - -- 生成文档,通过代码里标识的元数据生成javadoc文档。 -- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。 -- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。 -- 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。 - -#### 注解的常见分类? - -- **Java自带的标准注解**,包括`@Override`、`@Deprecated`和`@SuppressWarnings`,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。 - -- **元注解**,元注解是用于定义注解的注解,包括`@Retention`、`@Target`、`@Inherited`、`@Documented` - - - `@Retention`用于标明注解被保留的阶段 - - - `@Target`用于标明注解使用的范围 - - - `@Documented`用于标明是否生成javadoc文档 - -- **自定义注解**,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。 - -### 1.4 异常 - -#### Java异常类层次结构? - -- **Throwable** 是 Java 语言中所有错误与异常的超类。 - - - **Error** 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 - - - **Exception** 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/5b8dd265a5f94619125f81a2829c14c1.png) - -- **运行时异常** - -都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 - -运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 - -- **非运行时异常** (编译异常) - -是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。 - -#### 可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)区别? - -- **可查异常**(编译器要求必须处置的异常): - -正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。 - -除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。 - -- **不可查异常**(编译器不要求强制处置的异常) - -包括运行时异常(RuntimeException与其子类)和错误(Error)。 - -#### throw和throws的区别? - -- **异常的申明(throws)** - -在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示: - -```java -public static void method() throws IOException, FileNotFoundException{ - //something statements -} -``` - -- **异常的抛出(throw)** - -```java -public static double method(int value) { - if(value == 0) { - throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常 - } - return 5.0 / value; -} -``` - -#### Java 7 的 try-with-resource? - -如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。 - -```java -public void automaticallyCloseResource() { - File file = new File("./tmp.txt"); - try (FileInputStream inputStream = new FileInputStream(file);) { - // use the inputStream to read a file - } catch (FileNotFoundException e) { - log.error(e); - } catch (IOException e) { - log.error(e); - } -} -``` - -#### 异常的底层? - -提到JVM处理异常的机制,就需要提及Exception Table,以下称为异常表。我们暂且不急于介绍异常表,先看一个简单的 Java 处理异常的小例子。 - -```java -public static void simpleTryCatch() { - try { - testNPE(); - } catch (Exception e) { - e.printStackTrace(); - } -} -``` - -使用javap来分析这段代码(需要先使用javac编译) - -```java -//javap -c Main - public static void simpleTryCatch(); - Code: - 0: invokestatic #3 // Method testNPE:()V - 3: goto 11 - 6: astore_0 - 7: aload_0 - 8: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V - 11: return - Exception table: - from to target type - 0 3 6 Class java/lang/Exception -``` - -看到上面的代码,应该会有会心一笑,因为终于看到了Exception table,也就是我们要研究的异常表。 - -异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下 - -- **from** 可能发生异常的起始点 -- **to** 可能发生异常的结束点 -- **target** 上述from和to之前发生异常后的异常处理者的位置 -- **type** 异常处理者处理的异常的类信息 - -### 1.5 反射 - -#### 什么是反射? - -JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/3b98c1ae126cb173dc1245761f418573.png) - -#### 反射的使用? - -在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private) - -- Class类对象的获取 - -```java - @Test - public void classTest() throws Exception { - // 获取Class对象的三种方式 - logger.info("根据类名: \t" + User.class); - logger.info("根据对象: \t" + new User().getClass()); - logger.info("根据全限定类名:\t" + Class.forName("com.test.User")); - // 常用的方法 - logger.info("获取全限定类名:\t" + userClass.getName()); - logger.info("获取类名:\t" + userClass.getSimpleName()); - logger.info("实例化:\t" + userClass.newInstance()); - } -``` - -- Constructor类及其用法 -- Field类及其用法 -- Method类及其用法 - -#### getName、getCanonicalName与getSimpleName的区别? - -- getSimpleName:只获取类名 -- getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。 -- getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。 - -### 1.6 SPI机制 - -#### 什么是SPI机制? - -SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 **解耦**。 - -SPI整体机制图如下: - -![img](https://b2files.173114.xyz/blogimg/2025/03/4c506154da87b62d5de62b4d8d05130f.jpg) - -当服务的提供者提供了一种接口的实现之后,需要在classpath下的`META-INF/services/`目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的`META-INF/services/`中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:`java.util.ServiceLoader`。 - -#### SPI机制的应用? - -- SPI机制 - JDBC DriverManager - -在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。**而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现**。 - -- JDBC接口定义 - -首先在java中定义了接口`java.sql.Driver`,并没有具体的实现,具体的实现都是由不同厂商来提供的。 - -- mysql实现 - -在mysql的jar包`mysql-connector-java-6.0.6.jar`中,可以找到`META-INF/services`目录,该目录下会有一个名字为`java.sql.Driver`的文件,文件内容是`com.mysql.cj.jdbc.Driver`,这里面的内容就是针对Java中定义的接口的实现。 - -- postgresql实现 - -同样在postgresql的jar包`postgresql-42.0.0.jar`中,也可以找到同样的配置文件,文件内容是`org.postgresql.Driver`,这是postgresql对Java的`java.sql.Driver`的实现。 - -- 使用方法 - -上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用`Class.forName("com.mysql.jdbc.Driver")`来加载驱动了,而是直接使用如下代码: - -```java -String url = "jdbc:xxxx://xxxx:xxxx/xxxx"; -Connection conn = DriverManager.getConnection(url,username,password); -..... -``` - -#### SPI机制的简单示例? - -我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。 - -- 先定义好接口 - -```java -public interface Search { - public List searchDoc(String keyword); -} -``` - -- 文件搜索实现 - -```java -public class FileSearch implements Search{ - @Override - public List searchDoc(String keyword) { - System.out.println("文件搜索 "+keyword); - return null; - } -} -``` - -- 数据库搜索实现 - -```java -public class DatabaseSearch implements Search{ - @Override - public List searchDoc(String keyword) { - System.out.println("数据搜索 "+keyword); - return null; - } -} -``` - -- resources 接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:`com.cainiao.ys.spi.learn.Search`,里面加上我们需要用到的实现类 - -```xml -com.cainiao.ys.spi.learn.FileSearch -``` - -- 测试方法 - -```java -public class TestCase { - public static void main(String[] args) { - ServiceLoader s = ServiceLoader.load(Search.class); - Iterator iterator = s.iterator(); - while (iterator.hasNext()) { - Search search = iterator.next(); - search.searchDoc("hello world"); - } - } -} -``` - -可以看到输出结果:文件搜索 hello world - -如果在`com.cainiao.ys.spi.learn.Search`文件里写上两个实现类,那最后的输出结果就是两行了。 - -这就是因为`ServiceLoader.load(Search.class)`在加载某接口时,会去`META-INF/services`下找接口的全限定名文件,再根据里面的内容加载相应的实现类。 - -这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的`META-INF/services`下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。 - -## 2 Java 集合 - -> 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 - -### 2.1 Collection - -#### 集合有哪些类? - -- Set - - TreeSet 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 - - HashSet 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 - - LinkedHashSet 具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 -- List - - ArrayList 基于动态数组实现,支持随机访问。 - - Vector 和 ArrayList 类似,但它是线程安全的。 - - LinkedList 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 -- Queue - - LinkedList 可以用它来实现双向队列。 - - PriorityQueue 基于堆结构实现,可以用它来实现优先队列。 - -#### ArrayList的底层? - -*ArrayList*实现了*List*接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入`null`元素,底层通过**数组实现**。除该类未实现同步外,其余跟*Vector*大致相同。每个*ArrayList*都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。 - -![ArrayList_base](https://b2files.173114.xyz/blogimg/2025/03/7a76ac539fb5f30a4e92e3a0041365e7.png) - -#### ArrayList自动扩容? - -每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 - -数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。 - -![ArrayList_add](https://b2files.173114.xyz/blogimg/2025/03/eda4b8c24ff6fc96ef99dafe70445f15.png) - -#### ArrayList的Fail-Fast机制? - -ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。 - -### 2.2 Map - -#### Map有哪些类? - -- `TreeMap` 基于红黑树实现。 -- `HashMap` 1.7基于哈希表实现,1.8基于数组+链表+红黑树。 -- `HashTable` 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁, 1.8 引入了红黑树)。 -- `LinkedHashMap` 使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 - -#### JDK7 HashMap如何实现? - -哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。**Java7 \*HashMap\*采用的是冲突链表方式**。 - -![HashMap_base](https://b2files.173114.xyz/blogimg/2025/03/77e35c8cf322cc1413b76694b15d47e0.png) - -从上图容易看出,如果选择合适的哈希函数,`put()`和`get()`方法可以在常数时间内完成。但在对*HashMap*进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将*HashMap*的初始大小设的过大。 - -有两个参数可以影响*HashMap*的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始`table`的大小,负载系数用来指定自动扩容的临界值。当`entry`的数量超过`capacity*load_factor`时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。 - -#### JDK8 HashMap如何实现? - -根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。 - -为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/f4920fea0c56de4c7f5c9f6b9318a426.png) - -#### HashSet是如何实现的? - -*HashSet*是对*HashMap*的简单包装,对*HashSet*的函数调用都会转换成合适的*HashMap*方法 - -```Java -//HashSet是对HashMap的简单包装 -public class HashSet -{ - ...... - private transient HashMap map;//HashSet里面有一个HashMap - // Dummy value to associate with an Object in the backing Map - private static final Object PRESENT = new Object(); - public HashSet() { - map = new HashMap<>(); - } - ...... - public boolean add(E e) {//简单的方法转换 - return map.put(e, PRESENT)==null; - } - ...... -} -``` - -#### 什么是WeakHashMap? - -我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,**是否有有效的引用指向该对象**。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的**有效引用** 并不包括**弱引用**。也就是说,**虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收**。 - -WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢? - -*WeakHashMap* 里的`entry`可能会被GC自动删除,即使程序员没有调用`remove()`或者`clear()`方法。 - -***WeakHashMap\* 的这个特点特别适用于需要缓存的场景**。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。 - ## 3 Java 并发 > 并发和多线程 @@ -4078,1478 +3294,4 @@ public class DateUtils { DateUtils.df.get().format(new Date()); ``` -## 4 Java IO - -> Java IO相关 - -### 4.1 基础IO - -#### [#](https://pdai.tech/md/interview/x-interview.html#如何从数据传输方式理解io流)如何从数据传输方式理解IO流? - -从数据传输方式或者说是运输方式角度看,可以将 IO 类分为: - -1. **字节流**, 字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节。) -2. **字符流**, 字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。 - -**字节是给计算机看的,字符才是给人看的** - -- **字节流** - -![img](https://b2files.173114.xyz/blogimg/2025/03/1457114da1be9556eebadff672d78afc.png) - -- **字符流** - -![img](https://b2files.173114.xyz/blogimg/2025/03/3eba60e14c0777f6815da413d78ffecf.png) - -- **字节转字符**? - -![img](https://b2files.173114.xyz/blogimg/2025/03/974fed10a0d03a29a8ff92ac69e07194.png) - -#### 如何从数据操作上理解IO流? - -从数据来源或者说是操作对象角度看,IO 类可以分为: - -![img](https://b2files.173114.xyz/blogimg/2025/03/b6fba9b0e5f13ac9f1aada1914eab2ad.png) - -#### Java IO设计上使用了什么设计模式? - -**装饰者模式**: 所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。 - -- **装饰者举例** - -设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。 - -下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/ac7c7f4030e037c0fcb919f1111bd3f7.jpg) - -- 以 InputStream 为例 - - InputStream 是抽象组件; - - FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; - - FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 - -![image](https://b2files.173114.xyz/blogimg/2025/03/73713cdf33a3beb26aaa02a0cc8a1de2.png) - -实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 - -```java -FileInputStream fileInputStream = new FileInputStream(filePath); -BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); -``` - -DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 - -### 4.2 5种IO模型 - -#### 什么是阻塞?什么是同步? - -- **阻塞IO 和 非阻塞IO** - -这两个概念是**程序级别**的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了) - -- **同步IO 和 非同步IO** - -这两个概念是**操作系统级别**的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。 - -#### 什么是Linux的IO模型? - -网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。刚才说了,对于一次IO访问(以read举例),**数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间**。所以说,当一个read操作发生时,它会经历两个阶段: - -- 第一阶段:等待数据准备 (Waiting for the data to be ready)。 -- 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。 - -对于socket流而言, - -- 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。 -- 第二步:把数据从内核缓冲区复制到应用进程缓冲区。 - -网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。网络IO的模型大致有如下几种: - -1. 同步阻塞IO(bloking IO) -2. 同步非阻塞IO(non-blocking IO) -3. 多路复用IO(multiplexing IO) -4. 信号驱动式IO(signal-driven IO) -5. 异步IO(asynchronous IO) - -![img](https://b2files.173114.xyz/blogimg/2025/03/d513714dd8a87c7fd2b0bea7d3ae284e.png) - -PS: 这块略复杂,在后面的提供了问答,所以用了最简单的举例结合Linux IO图例帮你快速理解。 - -#### 什么是同步阻塞IO? - -应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。 - -- **举例理解** - -你早上去买有现炸油条,你点单,之后一直等店家做好,期间你啥其它事也做不了。(你就是应用级别,店家就是操作系统级别, 应用被阻塞了不能做其它事) - -- **Linux 中IO图例** - -![img](https://b2files.173114.xyz/blogimg/2025/03/72bebd621376c95b46750ba2a2636528.png) - -#### 什么是同步非阻塞IO? - -应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。 - -- **举例理解** - -你早上去买现炸油条,你点单,点完后每隔一段时间询问店家有没有做好,期间你可以做点其它事情。(你就是应用级别,店家就是操作系统级别,应用可以做其它事情并通过轮询来看操作系统是否完成) - -- **Linux 中IO图例** - -![img](https://b2files.173114.xyz/blogimg/2025/03/1f59eda3a1d50459108d26a60964a72d.png) - -#### 什么是多路复用IO? - -系统调用可能是由多个任务组成的,所以可以拆成多个任务,这就是多路复用。 - -- **举例理解** - -你早上去买现炸油条,点单收钱和炸油条原来都是由一个人完成的,现在他成了瓶颈,所以专门找了个收银员下单收钱,他则专注在炸油条。(本质上炸油条是耗时的瓶颈,将他职责分离出不是瓶颈的部分,比如下单收银,对应到系统级别也时一样的意思) - -- **Linux 中IO图例** - -使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。 - -它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/3f71f559ff70411e3d33d28161b907bf.png) - -#### 有哪些多路复用IO? - -目前流程的多路复用IO实现主要包括四种: `select`、`poll`、`epoll`、`kqueue`。下表是他们的一些重要特性的比较: - -| IO模型 | 相对性能 | 关键思路 | 操作系统 | JAVA支持情况 | -| ------ | -------- | ---------------- | ------------- | ------------------------------------------------------------ | -| select | 较高 | Reactor | windows/Linux | 支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型 | -| poll | 较高 | Reactor | Linux | Linux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式 | -| epoll | 高 | Reactor/Proactor | Linux | Linux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO | -| kqueue | 高 | Proactor | Linux | 目前JAVA的版本不支持 | - -多路复用IO技术最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接请求准备好。其他情况下多路复用IO技术发挥不出来它的优势。另一方面,使用JAVA NIO进行功能实现,相对于传统的Socket套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。 - -#### 什么是信号驱动IO? - -应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。 - -相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。 - -- **举例理解** - -你早上去买现炸油条,门口排队的人多,现在引入了一个叫号系统,点完单后你就可以做自己的事情了,然后等叫号就去拿就可以了。(所以不用再去自己频繁跑去问有没有做好了) - -- **Linux 中IO图例** - -![img](https://b2files.173114.xyz/blogimg/2025/03/52dddf2144b63584f2c9347d5bdd7f68.png) - -#### 什么是异步IO? - -相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。 - -- **举例理解** - -你早上去买现炸油条, 不用去排队了,打开美团外卖下单,然后做其它事,一会外卖自己送上门。(你就是应用级别,店家就是操作系统级别, 应用无需阻塞,这就是非阻塞;系统还可能在处理中,但是立刻响应了应用,这就是异步) - -- **Linux 中IO图例** - -(Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv) - -![img](https://b2files.173114.xyz/blogimg/2025/03/014e3f14da0fc522196450e74463f684.png) - -#### 什么是Reactor模型? - -大多数网络框架都是基于Reactor模型进行设计和开发,Reactor模型基于事件驱动,特别适合处理海量的I/O事件。 - -- **传统的IO模型**? - -这种模式是传统设计,每一个请求到来时,大致都会按照:请求读取->请求解码->服务执行->编码响应->发送答复 这个流程去处理。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/e3435d682d6c6455355cb30eb261c1e8.png) - -服务器会分配一个线程去处理,如果请求暴涨起来,那么意味着需要更多的线程来处理该请求。若请求出现暴涨,线程池的工作线程数量满载那么其它请求就会出现等待或者被抛弃。若每个小任务都可以使用非阻塞的模式,然后基于异步回调模式。这样就大大提高系统的吞吐量,这便引入了Reactor模型。 - -- **Reactor模型中定义的三种角色**: - -1. **Reactor**:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。 -2. **Acceptor**:处理客户端新连接,并分派请求到处理器链中。 -3. **Handler**:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。可用资源池来管理。 - -- **单Reactor单线程模型** - -Reactor线程负责多路分离套接字,accept新连接,并分派请求到handler。Redis使用单Reactor单进程的模型。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/56c683b77082c846b3fff1a5e711589d.png) - -消息处理流程: - -1. Reactor对象通过select监控连接事件,收到事件后通过dispatch进行转发。 -2. 如果是连接建立的事件,则由acceptor接受连接,并创建handler处理后续事件。 -3. 如果不是建立连接事件,则Reactor会分发调用Handler来响应。 -4. handler会完成read->业务处理->send的完整业务流程。 - -- **单Reactor多线程模型** - -将handler的处理池化。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/396a1707faa152da5227fc196ea8427e.png) - -- **多Reactor多线程模型** - -主从Reactor模型: 主Reactor用于响应连接请求,从Reactor用于处理IO操作请求,读写分离了。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/1fe78cdc15fc2131011095592a76fd5c.png) - -#### 什么是Java NIO? - -NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。**传统IO基于字节流和字符流进行操作**,而**NIO基于Channel和Buffer(缓冲区)进行操作**,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。 - -NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/f01a75dce5062ac8b31e5fb78eec67fa.png) - -### 4.3 零拷贝 - -#### [#](https://pdai.tech/md/interview/x-interview.html#传统的io存在什么问题-为什么引入零拷贝的)传统的IO存在什么问题?为什么引入零拷贝的? - -如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。 - -传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。 - -代码通常如下,一般会需要两个系统调用: - -```c -read(file, tmp_buf, len); -write(socket, tmp_buf, len); -``` - -代码很简单,虽然就两行代码,但是这里面发生了不少的事情。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/7f4423a9b07b6efec62da9b8ead8cb03.png) - -首先,**期间共发生了 4 次用户态与内核态的上下文切换**,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。 - -上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。 - -其次,还发生了 **4 次数据拷贝**,其中**两次是 DMA 的拷贝**,另外**两次则是通过 CPU 拷贝**的,下面说一下这个过程: - -- **第一次拷贝**,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。 -- **第二次拷贝**,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。 -- **第三次拷贝**,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。 -- **第四次拷贝**,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。 - -我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。 - -这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。 - -所以,**要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数**。 - -#### mmap + write怎么实现的零拷贝? - -在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。 - -```c -buf = mmap(file, len); -write(sockfd, buf, len); -``` - -mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。 - -![img](https://b2files.173114.xyz/blogimg/2025/03/665b6b7951011033f570d44fdc42bc74.png) - -具体过程如下: - -- 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区; -- 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据; -- 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。 - -我们可以得知,通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。 - -但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。 - -#### sendfile怎么实现的零拷贝? - -在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下: - -```c -#include -ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); -``` - -它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。 - -首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。 - -其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图: - -![img](https://b2files.173114.xyz/blogimg/2025/03/2c6ecf312b82d5865673e094c67f4870.png) - -但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(**The Scatter-Gather Direct Memory Access**)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。 - -你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性: - -```c -$ ethtool -k eth0 | grep scatter-gather -scatter-gather: on -``` - -于是,从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下: - -- 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里; -- 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝; - -所以,这个过程之中,只进行了 2 次数据拷贝,如下图: - -![img](https://b2files.173114.xyz/blogimg/2025/03/c5dc7c70150fe98c131bbfb2a6f1ef73.png) - -这就是所谓的**零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的**。 - -零拷贝技术的文件传输方式相比传统文件传输的方式,**减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运**。 - -## 5 JVM和调优 - -> JVM虚拟机和调优相关。 - -### 5.1 类加载机制 - -#### [#](https://pdai.tech/md/interview/x-interview.html#类加载的生命周期)类加载的生命周期? - -其中类加载的过程包括了**加载、验证、准备、解析、初始化**五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)*。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。 - - -![img](https://b2files.173114.xyz/blogimg/2025/03/938e32576ac2e8a5330674ec1afd5d93.png) - -- 类的加载: 查找并加载类的二进制数据 -- 连接 - - 验证: 确保被加载的类的正确性 - - 准备: 为类的静态变量分配内存,并将其初始化为默认值 - - 解析: 把类中的符号引用转换为直接引用 -- 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。 -- 使用: 类访问方法区内的数据结构的接口, 对象是Heap区的数据 -- 卸载: 结束生命周期 - -#### 类加载器的层次? - -![img](https://b2files.173114.xyz/blogimg/2025/03/fb3ff60c80c5b96d109be59e46e7fb30.png) - -- **启动类加载器**: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 -- **扩展类加载器**: Extension ClassLoader,该加载器由`sun.misc.Launcher$ExtClassLoader`实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。 -- **应用程序类加载器**: Application ClassLoader,该类加载器由`sun.misc.Launcher$AppClassLoader`来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 -- **自定义类加载器**: 因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点: - - 在执行非置信代码之前,自动验证数字签名。 - - 动态地创建符合用户特定需要的定制化构建类。 - - 从特定的场所取得java class,例如数据库中和网络中。 - -#### Class.forName()和ClassLoader.loadClass()区别? - -- `Class.forName()`: 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块; -- `ClassLoader.loadClass()`: 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。 -- `Class.forName(name, initialize, loader)`带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。 - -#### JVM有哪些类加载机制? - -- **JVM类加载机制有哪些**? - -1. **全盘负责**,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入 -2. **父类委托**,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类 -3. **缓存机制**,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效 -4. **双亲委派机制**, 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。 - -- **双亲委派机制过程?** - -1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。 -2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。 -3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载; -4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。 - -### 5.2 内存结构 - -#### 说说JVM内存整体的结构?线程私有还是共享的? - -JVM 整体架构,中间部分就是 Java 虚拟机定义的各种运行时数据区域。 - -![jvm-memory-structure](https://b2files.173114.xyz/blogimg/2025/03/349ba45d008c3979602df9157b9597d7.jpg) - -Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。 - -- **线程私有**:程序计数器、虚拟机栈、本地方法区 -- **线程共享**:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存) - -#### 什么是程序计数器(线程私有)? - -PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。 - -- **PC寄存器为什么会被设定为线程私有的?** - -多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。 - -#### 什么是虚拟机栈(线程私有)? - -主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。 - -- **特点?** - -1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器 -2. JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着**入栈**(进栈/压栈),方法执行结束**出栈** -3. 栈不存在垃圾回收问题 -4. 可以通过参数`-Xss`来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度 - -- **该区域有哪些异常**? - -1. 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 **StackOverflowError** 异常 -2. 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个**OutOfMemoryError**异常 - -- **栈帧的内部结构?** - -1. 局部变量表(Local Variables) -2. 操作数栈(Operand Stack)(或称为表达式栈) -3. 动态链接(Dynamic Linking):指向运行时常量池的方法引用 -4. 方法返回地址(Return Address):方法正常退出或异常退出的地址 -5. 一些附加信息 - -![jvm-stack](https://b2files.173114.xyz/blogimg/2025/03/d56db4f1e02265ca20870fa5ca5dbc11.jpg) - -#### Java虚拟机栈如何进行方法计算的? - -以如下代码为例: - -```java -private static int add(int a, int b) { - int c = 0; - c = a + b; - return c; -} -``` - -可以通过jsclass 等工具查看bytecode - -![img](https://b2files.173114.xyz/blogimg/2025/03/f3b9c1353fdd2e9f9e7723f1ff6f8f25.png) - -压栈的步骤如下: - -```java -0: iconst_0 // 0压栈 -1: istore_2 // 弹出int,存放于局部变量2 -2: iload_0 // 把局部变量0压栈 -3: iload_1 // 局部变量1压栈 -4: iadd //弹出2个变量,求和,结果压栈 -5: istore_2 //弹出结果,放于局部变量2 -6: iload_2 //局部变量2压栈 -7: ireturn //返回 -``` - -如果计算100+98的值,那么操作数栈的变化如下图 - -![img](https://b2files.173114.xyz/blogimg/2025/03/a968e2c20822af3419cceea3fe681b9e.png) - -#### 什么是本地方法栈(线程私有)? - -- **本地方法接口** - -一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法。 - -- **本地方法栈(Native Method Stack)** - -Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用 - -#### 什么是方法区(线程共享)? - -方法区(method area)只是 **JVM 规范**中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而**永久代(PermGen)\**是 \*\*Hotspot\*\* 虚拟机特有的概念, Java8 的时候又被\**元空间**取代了,永久代和元空间都可以理解为方法区的落地实现。 - -JDK1.8之前调节方法区大小: - -```bash --XX:PermSize=N //方法区(永久代)初始大小 --XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError -``` - -JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是本机内存。参数设置: - -```bash --XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小) --XX:MaxMetaspaceSize=N //设置Metaspace的最大大小 -``` - -**栈、堆、方法区的交互关系** - -![img](https://b2files.173114.xyz/blogimg/2025/03/e70400fd99edf6f7301d60e20f04a6a0.png) - -#### 永久代和元空间内存使用上的差异? - -Java虚拟机规范中只定义了方法区用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译后的代码等数据 - -1. jdk1.7开始符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,但分离的并不彻底,此时永久代中还保存另一些与类的元数据无关的杂项 -2. jdk8后HotSpot 原永久代中存储的类的**元数据将存储在metaspace**中,而**类的静态变量和字符串常量将放在Java堆中**,metaspace是方法区的一种实现,只不过它使用的不是虚拟机内的内存,而是本地内存。在元空间中保存的数据比永久代中纯粹很多,就只是类的元数据,这些信息只对编译期或JVM的运行时有用。 -3. 永久代有一个JVM本身设置固定大小上线,无法进行调整,而**元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError**。 -4. **符号引用没有存在元空间中,而是存在native heap中**,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM。 - -#### 堆区内存是怎么细分的? - -对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。 - -为了进行高效的垃圾回收,虚拟机把堆内存**逻辑上**划分成三块区域(分代的唯一理由就是优化 GC 性能): - -1. 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代 -2. 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大 - -![jvm-memory](https://b2files.173114.xyz/blogimg/2025/03/2fbbc559cdf20242c200b7c85c941ec0.png) - -Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过 `-Xmx` 和 `-Xms` 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 `OutOfMemoryError` 异常。 - -- **年轻代 (Young Generation)** - -年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 **Minor GC**。年轻一代被分为三个部分——伊甸园(**Eden Memory**)和两个幸存区(**Survivor Memory**,被称为from/to或s0/s1),默认比例是`8:1:1` - -1. 大多数新创建的对象都位于 Eden 内存空间中 -2. 当 Eden 空间被对象填充时,执行**Minor GC**,并将所有幸存者对象移动到一个幸存者空间中 -3. Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的 -4. 经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代 - -- **老年代(Old Generation)** - -旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。 - -大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝 - -#### JVM中对象在堆中的生命周期? - -1. 在 JVM 内存模型的堆中,堆被划分为新生代和老年代 - - 新生代又被进一步划分为 **Eden区** 和 **Survivor区**,Survivor 区由 **From Survivor** 和 **To Survivor** 组成 -2. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区 - - 此时 JVM 会给对象定义一个**对象年轻计数器**(`-XX:MaxTenuringThreshold`) -3. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC) - - JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1 - - 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1 -4. 如果分配的对象超过了`-XX:PetenureSizeThreshold`,对象会**直接被分配到老年代** - -#### JVM中对象的分配过程? - -为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。 - -1. new 的对象先放在伊甸园区,此区有大小限制 -2. 当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区 -3. 然后将伊甸园中的剩余对象移动到幸存者 0 区 -4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1 区 -5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区 -6. 什么时候才会去养老区呢? 默认是 15 次回收标记 -7. 在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理 -8. 若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常 - -#### 什么是 TLAB (Thread Local Allocation Buffer)? - -- 从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内 -- 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为**快速分配策略** -- OpenJDK 衍生出来的 JVM 大都提供了 TLAB 设计 - -#### 为什么要有 TLAB ? - -- 堆区是线程共享的,任何线程都可以访问到堆区中的共享数据 -- 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的 -- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 - -尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。 - -在程序中,可以通过 `-XX:UseTLAB` 设置是否开启 TLAB 空间。 - -默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过 `-XX:TLABWasteTargetPercent` 设置 TLAB 空间所占用 Eden 空间的百分比大小。 - -一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。 - -### 5.3 GC垃圾回收 - -#### 如何判断一个对象是否可以回收? - -- **引用计数算法** - -给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 - -两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 - -正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。 - -- **可达性分析算法** - -通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。 - -![image](https://b2files.173114.xyz/blogimg/2025/03/e9e1029af98b1b016472a96b5e684718.png) - -Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容: - -- 虚拟机栈中引用的对象 -- 本地方法栈中引用的对象 -- 方法区中类静态属性引用的对象 -- 方法区中的常量引用的对象 - -#### 对象有哪些引用类型? - -无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。 - -Java 具有四种强度不同的引用类型。 - -- **强引用** - -被强引用关联的对象不会被回收。 - -使用 new 一个新对象的方式来创建强引用。 - -```java -Object obj = new Object(); -``` - -- **软引用** - -被软引用关联的对象只有在内存不够的情况下才会被回收。 - -使用 SoftReference 类来创建软引用。 - -```java -Object obj = new Object(); -SoftReference sf = new SoftReference(obj); -obj = null; // 使对象只被软引用关联 -``` - -- **弱引用** - -被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。 - -使用 WeakReference 类来实现弱引用。 - -```java -Object obj = new Object(); -WeakReference wf = new WeakReference(obj); -obj = null; -``` - -- **虚引用** - -又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。 - -为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。 - -使用 PhantomReference 来实现虚引用。 - -```java -Object obj = new Object(); -PhantomReference pf = new PhantomReference(obj); -obj = null; -``` - -#### 有哪些基本的垃圾回收算法? - -- **标记 - 清除** - -![image](https://pdai.tech/images/pics/a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg) - -将存活的对象进行标记,然后清理掉未被标记的对象。 - -不足: - -- 标记和清除过程效率都不高; -- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。 - -- **标记 - 整理** - -![image](https://pdai.tech/images/pics/902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg) - -让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 - -- **复制** - -![image](https://pdai.tech/images/pics/e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg) - -将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 - -主要不足是只使用了内存的一半。 - -现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。 - -HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。 - -- **分代收集** - -现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 - -一般将堆分为新生代和老年代。 - -- 新生代使用: 复制算法 -- 老年代使用: 标记 - 清除 或者 标记 - 整理 算法 - -#### 分代收集算法和分区收集算法区别? - -![img](https://b2files.173114.xyz/blogimg/2025/03/4e3fa436564caa5ddbc428ea8ee785c7.jpg) - -- **分代收集算法** - -当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据 对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据 各年代特点分别采用最适当的 GC 算法 - -在新生代-复制算法: - -每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量 存活对象的复制成本就可以完成收集 - -在老年代-标记整理算法: - -因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存. - -1. **ParNew**: 一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 `-XX:ParallelGCThreads` 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。 -2. **CMS**: 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除。 - -- **分区收集算法** - -分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次 GC 所产生的停顿。 - -1. **G1**: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。 -2. **ZGC**: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。 - -#### 什么是Minor GC、Major GC、Full GC? - -JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。 - -针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC) - -- 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为: - - 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集 - - 老年代收集(Major GC/Old GC):只是老年代的垃圾收集 - - 目前,只有 CMS GC 会有单独收集老年代的行为 - - 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收 - - 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集 - - 目前只有 G1 GC 会有这种行为 -- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾 - -#### 说说JVM内存分配策略? - -- **对象优先在 Eden 分配** - -大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。 - -- **大对象直接进入老年代** - -大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 - -经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 - --XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 - -- **长期存活的对象进入老年代** - -为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。 - --XX:MaxTenuringThreshold 用来定义年龄的阈值。 - -- **动态对象年龄判定** - -虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 - -- **空间分配担保** - -在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。 - -如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。 - -#### 什么情况下会触发Full GC? - -对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: - -- **调用 System.gc()** - -只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 - -- **老年代空间不足** - -老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。 - -为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 - -- **空间分配担保失败** - -使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。 - -- **JDK 1.7 及以前的永久代空间不足** - -在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 - -当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。 - -为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 - -- **Concurrent Mode Failure** - -执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 - -#### Hotspot中有哪些垃圾回收器? - -![image](https://b2files.173114.xyz/blogimg/2025/03/cc154755877a9d45e5e41b36e16190b4.jpg) - -以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 - -- 单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程; -- 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 - -1. **Serial 收集器** - -![image](https://b2files.173114.xyz/blogimg/2025/03/01e5081f061083e9988f50b843bdce08.jpg) - -Serial 翻译为串行,也就是说它以串行的方式执行。 - -它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 - -它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 - -它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 - -1. **ParNew 收集器** - -![image](https://b2files.173114.xyz/blogimg/2025/03/b9bf6e1727fb42311c1eb7a0926c0d57.jpg) - -它是 Serial 收集器的多线程版本。 - -是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。 - -默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。 - -1. **Parallel Scavenge 收集器** - -与 ParNew 一样是多线程收集器。 - -其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 - -停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 - -缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 - -可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。 - -1. **Serial Old 收集器** - -![image](https://b2files.173114.xyz/blogimg/2025/03/16993beb849661df16e5eec8c15d2320.jpg) - -是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途: - -- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 -- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 - -1. **Parallel Old 收集器** - -![image](https://b2files.173114.xyz/blogimg/2025/03/fe7c03208f3828c014e4a724537c61ac.jpg) - -是 Parallel Scavenge 收集器的老年代版本。 - -在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 - -1. **CMS 收集器** - -![image](https://b2files.173114.xyz/blogimg/2025/03/f338848f362071a01c826380011407ef.jpg) - -CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 - -分为以下四个流程: - -- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 -- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 -- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 -- 并发清除: 不需要停顿。 - -在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 - -具有以下缺点: - -- 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 -- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 -- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 - -1. **G1 收集器** - -G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 - -堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 **G1 可以直接对新生代和老年代一起回收**。 - -![image](https://b2files.173114.xyz/blogimg/2025/03/d23ff703a0796e77bb09239fe2493028.png) - -G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。 - -![image](https://b2files.173114.xyz/blogimg/2025/03/18e82055a2eaa3e5a365e35eb7865b7f.png) - -**通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收**。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。 - -每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。 - -![image](https://b2files.173114.xyz/blogimg/2025/03/ddb97eb22a6627fec00cb07331b7eb81.jpg) - -如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤: - -- 初始标记 -- 并发标记 -- 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 -- 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 - -具备如下特点: - -- 空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 -- 可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。 - -### 5.4 问题排查 - -#### 常见的Linux定位问题的工具? - -- 文本操作 - - 文本查找 - grep - - 文本分析 - awk - - 文本处理 - sed -- 文件操作 - - 文件监听 - tail - - 文件查找 - find -- 网络和进程 - - 网络接口 - ifconfig - - 防火墙 - iptables -L - - 路由表 - route -n - - netstat -- 其它常用 - - 进程 ps -ef | grep java - - 分区大小 df -h - - 内存 free -m - - 硬盘大小 fdisk -l |grep Disk - - top - - 环境变量 env - -#### JDK自带的定位问题的工具? - -- **jps** jps是jdk提供的一个查看当前java进程的小工具, 可以看做是JavaVirtual Machine Process Status Tool的缩写。 - -```bash -jps –l # 输出输出完全的包名,应用主类名,jar的完全路径名 -``` - -- **jstack** jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息。 - -```bash -# 基本 -jstack 2815 -jstack -m 2815 # java和native c/c++框架的所有栈信息 -jstack -l 2815 # 额外的锁信息列表,查看是否死锁 -``` - -- **jinfo** jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括Java System属性和JVM命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息 - -```bash -jinfo 2815 # 输出当前 jvm 进程的全部参数和系统属性 -``` - -- **jmap** 命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。 - -```bash -# 查看堆的情况 -jmap -heap 2815 - -# dump -jmap -dump:live,format=b,file=/tmp/heap2.bin 2815 -``` - -- **jstat** jstat参数众多,但是使用一个就够了 - -```bash -jstat -gcutil 2815 1000 -``` - -#### 如何使用在线调试工具Arthas? - -举几个例子 - -- **查看最繁忙的线程,以及是否有阻塞情况发生**? - -场景:我想看下查看最繁忙的线程,以及是否有阻塞情况发生? 常规查看线程,一般我们可以通过 top 等系统命令进行查看,但是那毕竟要很多个步骤,很麻烦。 - -```bash -thread -n 3 # 查看最繁忙的三个线程栈信息 -thread # 以直观的方式展现所有的线程情况 -thread -b #找出当前阻塞其他线程的线程 -``` - -- **确认某个类是否已被系统加载**? - -场景:我新写了一个类或者一个方法,我想知道新写的代码是否被部署了? - -```bash -# 即可以找到需要的类全路径,如果存在的话 -sc *MyServlet - -# 查看这个某个类所有的方法 -sm pdai.tech.servlet.TestMyServlet * - -# 查看某个方法的信息,如果存在的话 -sm pdai.tech.servlet.TestMyServlet testMethod -``` - -- **如何查看一个class类的源码信息**? - -场景:我新修改的内容在方法内部,而上一个步骤只能看到方法,这时候可以反编译看下源码 - -```bash -# 直接反编译出java 源代码,包含一此额外信息的 -jad pdai.tech.servlet.TestMyServlet -``` - -- **如何跟踪某个方法的返回值、入参**? - -场景:我想看下我新加的方法在线运行的参数和返回值? - -```bash -# 同时监控入参,返回值,及异常 -watch pdai.tech.servlet.TestMyServlet testMethod "{params, returnObj, throwExp}" -e -x 2 -``` - -- **如何看方法调用栈的信息**? - -场景:我想看下某个方法的调用栈的信息? - -```bash -stack pdai.tech.servlet.TestMyServlet testMethod -``` - -运行此命令之后需要即时触发方法才会有响应的信息打印在控制台上 - -- **找到最耗时的方法调用**? - -场景:testMethod这个方法入口响应很慢,如何找到最耗时的子调用? - -```bash -# 执行的时候每个子调用的运行时长,可以找到最耗时的子调用。 -stack pdai.tech.servlet.TestMyServlet testMethod -``` - -运行此命令之后需要即时触发方法才会有响应的信息打印在控制台上,然后一层一层看子调用。 - -- **如何临时更改代码运行**? - -场景:我找到了问题所在,能否线上直接修改测试,而不需要在本地改了代码后,重新打包部署,然后重启观察效果? - -```bash -# 先反编译出class源码 -jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java - -# 然后使用外部工具编辑内容 -mc /tmp/UserController.java -d /tmp # 再编译成class - -# 最后,重新载入定义的类,就可以实时验证你的猜测了 -redefine /tmp/com/example/demo/arthas/user/UserController.class -``` - -如上,是直接更改线上代码的方式,但是一般好像是编译不成功的。所以,最好是本地ide编译成 class文件后,再上传替换为好! - -总之,已经完全不用重启和发布了!这个功能真的很方便,比起重启带来的代价,真的是不可比的。比如,重启时可能导致负载重分配,选主等等问题,就不是你能控制的了。 - -- **我如何测试某个方法的性能问题**? - -```bash -monitor -c 5 demo.MathGame primeFactors -``` - -#### 如何使用Idea的远程调试? - -要让远程服务器运行的代码支持远程调试,则启动的时候必须加上特定的JVM参数,这些参数是: - -```bash --Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=127.0.0.1:5555 -``` - -#### 复杂综合类型问题的定位思路? - -![img](https://b2files.173114.xyz/blogimg/2025/03/c01589f0995701911e937de084138f2b.png) - -## 6 Java 新版本 - -> Java 8版本特性,及Java8+版本特性。 - -### 6.1 Java 8 特性 - -#### 什么是函数式编程?Lambda表达式? - -- **函数式编程** - -面向对象编程是对数据进行抽象;函数式编程是对行为进行抽象。 - -核心思想: 使用不可变值和函数,函数对一个值进行处理,映射成另一个值。 - -- **Lambda表达式** - -lambda表达式仅能放入如下代码: 预定义使用了 `@Functional` 注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传lambda表达式 - -#### Stream中常用方法? - -- `stream()`, `parallelStream()` -- `filter()` -- `findAny()` `findFirst()` -- `sort` -- `forEach` void -- `map(), reduce()` -- `flatMap()` - 将多个Stream连接成一个Stream -- `collect(Collectors.toList())` -- `distinct`, `limit` -- `count` -- `min`, `max`, `summaryStatistics` - -#### 什么是FunctionalInterface? - -```java -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface FunctionalInterface{} -``` - -- interface做注解的注解类型,被定义成java语言规 -- 一个被它注解的接口只能有一个抽象方法,有两种例外 -- 第一是接口允许有实现的方法,这种实现的方法是用default关键字来标记的(java反射中java.lang.reflect.Method#isDefault()方法用来判断是否是default方法) -- 第二如果声明的方法和java.lang.Object中的某个方法一样,它可以不当做未实现的方法,不违背这个原则: 一个被它注解的接口只能有一个抽象方法, 比如: `java public interface Comparator { int compare(T o1, T o2); boolean equals(Object obj); }` -- 如果一个类型被这个注解修饰,那么编译器会要求这个类型必须满足如下条件: - - 这个类型必须是一个interface,而不是其他的注解类型、枚举enum或者类class - - 这个类型必须满足function interface的所有要求,如你个包含两个抽象方法的接口增加这个注解,会有编译错误。 -- 编译器会自动把满足function interface要求的接口自动识别为function interface。 - -#### 如何自定义函数接口? - -```java -@FunctionalInterface -public interface IMyInterface { - void study(); -} - -public class TestIMyInterface { - public static void main(String[] args) { - IMyInterface iMyInterface = () -> System.out.println("I like study"); - iMyInterface.study(); - } -} -``` - -#### 内置的四大函数接口及使用? - -- **消费型接口: Consumer< T> void accept(T t)有参数,无返回值的抽象方法**; - -```java -Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); -greeter.accept(new Person("Luke", "Skywalker")); -``` - -- **供给型接口: Supplier < T> T get() 无参有返回值的抽象方法**; - -以stream().collect(Collector collector)为例: - -比如: - -```java -Supplier personSupplier = Person::new; -personSupplier.get(); // new Person -``` - - - -```java -断定型接口: Predicate boolean test(T t):有参,但是返回值类型是固定的boolean -比如: steam().filter()中参数就是Predicate - -Predicate predicate = (s) -> s.length() > 0; - -predicate.test("foo"); // true -predicate.negate().test("foo"); // false - -Predicate nonNull = Objects::nonNull; -Predicate isNull = Objects::isNull; - -Predicate isEmpty = String::isEmpty; -Predicate isNotEmpty = isEmpty.negate(); - -函数型接口: Function R apply(T t)有参有返回值的抽象方法; -比如: steam().map() 中参数就是Function;reduce()中参数BinaryOperator (ps: BinaryOperator extends BiFunction) - -Function toInteger = Integer::valueOf; -Function backToString = toInteger.andThen(String::valueOf); - -backToString.apply("123"); // "123" -``` - -#### Optional要解决什么问题? - -在调用一个方法得到了返回值却不能直接将返回值作为参数去调用别的方法,我们首先要判断这个返回值是否为null,只有在非空的前提下才能将其作为其他方法的参数。Java 8引入了一个新的Optional类:这是一个可以为null的容器对象,如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。 - -#### 如何使用Optional来解决嵌套对象的判空问题? - -假设我们有一个像这样的类层次结构: - -```java -class Outer { - Nested nested; - Nested getNested() { - return nested; - } -} -class Nested { - Inner inner; - Inner getInner() { - return inner; - } -} -class Inner { - String foo; - String getFoo() { - return foo; - } -} -``` - -解决这种结构的深层嵌套路径是有点麻烦的。我们必须编写一堆 null 检查来确保不会导致一个 NullPointerException: - -```java -Outer outer = new Outer(); -if (outer != null && outer.nested != null && outer.nested.inner != null) { - System.out.println(outer.nested.inner.foo); -} -``` - -我们可以通过利用 Java 8 的 Optional 类型来摆脱所有这些 null 检查。map 方法接收一个 Function 类型的 lambda 表达式,并自动将每个 function 的结果包装成一个 Optional 对象。这使我们能够在一行中进行多个 map 操作。Null 检查是在底层自动处理的。 - -```java -Optional.of(new Outer()) - .map(Outer::getNested) - .map(Nested::getInner) - .map(Inner::getFoo) - .ifPresent(System.out::println); -``` - -还有一种实现相同作用的方式就是通过利用一个 supplier 函数来解决嵌套路径的问题: - -```java -Outer obj = new Outer(); -resolve(() -> obj.getNested().getInner().getFoo()) - .ifPresent(System.out::println); -``` - -#### 什么是默认方法,为什么要有默认方法? - -就是接口可以有实现方法,而且不需要实现类去实现其方法。只需在方法名前面加个default关键字即可。 - -```java -public interface A { - default void foo(){ - System.out.println("Calling A.foo()"); - } -} - -public class Clazz implements A { - public static void main(String[] args){ - Clazz clazz = new Clazz(); - clazz.foo();//调用A.foo() - } -} -``` - -- **为什么出现默认方法**? - -首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。 - -#### 什么是类型注解? - -类型注解**被用来支持在Java的程序中做强类型检查。配合插件式的check framework,可以在编译的时候检测出runtime error,以提高代码质量**。这就是类型注解的作用了。 - -1. 在java 8之前,注解只能是在声明的地方所使用,比如类,方法,属性; -2. java 8里面,注解可以应用在任何地方,比如: - -创建类实例 - -```java -new @Interned MyObject(); -``` - -类型映射 - -```java -myString = (@NonNull String) str; -``` - -implements 语句中 - -```java -class UnmodifiableList implements @Readonly List<@Readonly T> { … } -``` - -throw exception声明 - -```java -void monitorTemperature() throws @Critical TemperatureException { … } -``` - -需要注意的是,**类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解**。 - -#### 什么是重复注解? - -允许在同一申明类型(类,属性,或方法)的多次使用同一个注解 - -- **JDK8之前** - -java 8之前也有重复使用注解的解决方案,但可读性不是很好,比如下面的代码: - -```java -public @interface Authority { - String role(); -} - -public @interface Authorities { - Authority[] value(); -} - -public class RepeatAnnotationUseOldVersion { - - @Authorities({@Authority(role="Admin"),@Authority(role="Manager")}) - public void doSomeThing(){ - } -} -``` - -由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。 - -- **Jdk8重复注解** - -我们再来看看java 8里面的做法: - -```java -@Repeatable(Authorities.class) -public @interface Authority { - String role(); -} - -public @interface Authorities { - Authority[] value(); -} - -public class RepeatAnnotationUseNewVersion { - @Authority(role="Admin") - @Authority(role="Manager") - public void doSomeThing(){ } -} -``` - -不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点 - -### 6.2 Java 9+ 特性 - -#### Java 9后续版本发布是按照什么样的发布策略呢? - -Java现在发布的版本很快,每年两个,但是真正会被大规模使用的是三年一个的TLS版本。@pdai - -- 每3年发布一个TLS,长期维护版本。意味着Java 8 ,Java 11, Java 17 才可能被大规模使用。 -- 每年发布两个正式版本,分别是3月份和9月份。 - -#### Java 9后续新版本中你知道哪些? - -能够举几个即可: - -- **Java10 - 并行全垃圾回收器 G1** - -大家如果接触过 Java 性能调优工作,应该会知道,调优的最终目标是通过参数设置来达到快速、低延时的内存垃圾回收以提高应用吞吐量,尽可能的避免因内存回收不及时而触发的完整 GC(Full GC 会带来应用出现卡顿)。 - -G1 垃圾回收器是 Java 9 中 Hotspot 的默认垃圾回收器,是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是当并发收集无法快速回收内存时,会触发垃圾回收器回退进行 Full GC。之前 Java 版本中的 G1 垃圾回收器执行 GC 时采用的是基于单线程标记扫描压缩算法(mark-sweep-compact)。为了最大限度地减少 Full GC 造成的应用停顿的影响,Java 10 中将为 G1 引入多线程并行 GC,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量。 - -Java 10 中将采用并行化 mark-sweep-compact 算法,并使用与年轻代回收和混合回收相同数量的线程。具体并行 GC 线程数量可以通过: `-XX:ParallelGCThreads` 参数来调节,但这也会影响用于年轻代和混合收集的工作线程数。 - -- **Java11 - ZGC:可伸缩低延迟垃圾收集器** - -ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),这应该是 Java 11 中最为瞩目的特性,没有之一。ZGC 是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计: - -1. GC 停顿时间不超过 10ms -2. 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆 -3. 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比) -4. 方便在此基础上引入新的 GC 特性和利用 colord -5. 针以及 Load barriers 优化奠定基础 -6. 当前只支持 Linux/x64 位平台 - -停顿时间在 10ms 以下,10ms 其实是一个很保守的数据,即便是 10ms 这个数据,也是 GC 调优几乎达不到的极值。根据 SPECjbb 2015 的基准测试,128G 的大堆下最大停顿时间才 1.68ms,远低于 10ms,和 G1 算法相比,改进非常明显。 - -![img](https://pdai.tech/images/java/java-11-1.png) - -- **Java 14 - Switch 表达式**(正式版) - -switch 表达式在之前的 Java 12 和 Java 13 中都是处于预览阶段,而在这次更新的 Java 14 中,终于成为稳定版本,能够正式可用。 - -switch 表达式带来的不仅仅是编码上的简洁、流畅,也精简了 switch 语句的使用方式,同时也兼容之前的 switch 语句的使用;之前使用 switch 语句时,在每个分支结束之前,往往都需要加上 break 关键字进行分支跳出,以防 switch 语句一直往后执行到整个 switch 语句结束,由此造成一些意想不到的问题。switch 语句一般使用冒号 :来作为语句分支代码的开始,而 switch 表达式则提供了新的分支切换方式,即 -> 符号右则表达式方法体在执行完分支方法之后,自动结束 switch 分支,同时 -> 右则方法块中可以是表达式、代码块或者是手动抛出的异常。以往的 switch 语句写法如下: - -```java -int dayOfWeek; -switch (day) { - case MONDAY: - case FRIDAY: - case SUNDAY: - dayOfWeek = 6; - break; - case TUESDAY: - dayOfWeek = 7; - break; - case THURSDAY: - case SATURDAY: - dayOfWeek = 8; - break; - case WEDNESDAY: - dayOfWeek = 9; - break; - default: - dayOfWeek = 0; - break; -} -``` - -而现在 Java 14 可以使用 switch 表达式正式版之后,上面语句可以转换为下列写法: - -```java -int dayOfWeek = switch (day) { - case MONDAY, FRIDAY, SUNDAY -> 6; - case TUESDAY -> 7; - case THURSDAY, SATURDAY -> 8; -case WEDNESDAY -> 9; - default -> 0; - -}; -``` - -很明显,switch 表达式将之前 switch 语句从编码方式上简化了不少,但是还是需要注意下面几点: - -1. 需要保持与之前 switch 语句同样的 case 分支情况。 -2. 之前需要用变量来接收返回值,而现在直接使用 yield 关键字来返回 case 分支需要返回的结果。 -3. 现在的 switch 表达式中不再需要显式地使用 return、break 或者 continue 来跳出当前分支。 -4. 现在不需要像之前一样,在每个分支结束之前加上 break 关键字来结束当前分支,如果不加,则会默认往后执行,直到遇到 break 关键字或者整个 switch 语句结束,在 Java 14 表达式中,表达式默认执行完之后自动跳出,不会继续往后执行。 -5. 对于多个相同的 case 方法块,可以将 case 条件并列,而不需要像之前一样,通过每个 case 后面故意不加 break 关键字来使用相同方法块。 - -使用 switch 表达式来替换之前的 switch 语句,确实精简了不少代码,提高了编码效率,同时也可以规避一些可能由于不太经意而出现的意想不到的情况,可见 Java 在提高使用者编码效率、编码体验和简化使用方面一直在不停的努力中,同时也期待未来有更多的类似 lambda、switch 表达式这样的新特性出来。 - -- **Java 14 - Records** - -在 Java 14 中引入了 Record 类型,其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不尽完全相同,它们的共同点都是类的部分或者全部可以直接在类头中定义、描述,并且这个类只用于存储数据而已。对于 Record 类型,具体可以用下面代码来说明: - -```java -public record Person(String name, int age) { - public static String address; - - public String getName() { - return name; - } -} -``` - -对上述代码进行编译,然后反编译之后可以看到如下结果: - -```java -public final class Person extends java.lang.Record { - private final java.lang.String name; - private final java.lang.String age; - - public Person(java.lang.String name, java.lang.String age) { /* compiled code */ } - - public java.lang.String getName() { /* compiled code */ } - - public java.lang.String toString() { /* compiled code */ } - - public final int hashCode() { /* compiled code */ } - - public final boolean equals(java.lang.Object o) { /* compiled code */ } - - public java.lang.String name() { /* compiled code */ } - - public java.lang.String age() { /* compiled code */ } -} -``` - +## \ No newline at end of file diff --git a/src/interview/Java/Java新版本.md b/src/interview/Java/Java新版本.md new file mode 100644 index 0000000..4717b29 --- /dev/null +++ b/src/interview/Java/Java新版本.md @@ -0,0 +1,417 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Java 新版本 +index: true +headerDepth: 3 +order: 6 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- +## 6 Java 新版本 + +> Java 8版本特性,及Java8+版本特性。 + +### 6.1 Java 8 特性 + +#### 什么是函数式编程?Lambda表达式? + +- **函数式编程** + +面向对象编程是对数据进行抽象;函数式编程是对行为进行抽象。 + +核心思想: 使用不可变值和函数,函数对一个值进行处理,映射成另一个值。 + +- **Lambda表达式** + +lambda表达式仅能放入如下代码: 预定义使用了 `@Functional` 注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传lambda表达式 + +#### Stream中常用方法? + +- `stream()`, `parallelStream()` +- `filter()` +- `findAny()` `findFirst()` +- `sort` +- `forEach` void +- `map(), reduce()` +- `flatMap()` - 将多个Stream连接成一个Stream +- `collect(Collectors.toList())` +- `distinct`, `limit` +- `count` +- `min`, `max`, `summaryStatistics` + +#### 什么是FunctionalInterface? + +```java +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface FunctionalInterface{} +``` + +- interface做注解的注解类型,被定义成java语言规 +- 一个被它注解的接口只能有一个抽象方法,有两种例外 +- 第一是接口允许有实现的方法,这种实现的方法是用default关键字来标记的(java反射中java.lang.reflect.Method#isDefault()方法用来判断是否是default方法) +- 第二如果声明的方法和java.lang.Object中的某个方法一样,它可以不当做未实现的方法,不违背这个原则: 一个被它注解的接口只能有一个抽象方法, 比如: `java public interface Comparator { int compare(T o1, T o2); boolean equals(Object obj); }` +- 如果一个类型被这个注解修饰,那么编译器会要求这个类型必须满足如下条件: + - 这个类型必须是一个interface,而不是其他的注解类型、枚举enum或者类class + - 这个类型必须满足function interface的所有要求,如你个包含两个抽象方法的接口增加这个注解,会有编译错误。 +- 编译器会自动把满足function interface要求的接口自动识别为function interface。 + +#### 如何自定义函数接口? + +```java +@FunctionalInterface +public interface IMyInterface { + void study(); +} + +public class TestIMyInterface { + public static void main(String[] args) { + IMyInterface iMyInterface = () -> System.out.println("I like study"); + iMyInterface.study(); + } +} +``` + +#### 内置的四大函数接口及使用? + +- **消费型接口: Consumer< T> void accept(T t)有参数,无返回值的抽象方法**; + +```java +Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); +greeter.accept(new Person("Luke", "Skywalker")); +``` + +- **供给型接口: Supplier < T> T get() 无参有返回值的抽象方法**; + +以stream().collect(Collector collector)为例: + +比如: + +```java +Supplier personSupplier = Person::new; +personSupplier.get(); // new Person +``` + + + +```java +断定型接口: Predicate boolean test(T t):有参,但是返回值类型是固定的boolean +比如: steam().filter()中参数就是Predicate + +Predicate predicate = (s) -> s.length() > 0; + +predicate.test("foo"); // true +predicate.negate().test("foo"); // false + +Predicate nonNull = Objects::nonNull; +Predicate isNull = Objects::isNull; + +Predicate isEmpty = String::isEmpty; +Predicate isNotEmpty = isEmpty.negate(); + +函数型接口: Function R apply(T t)有参有返回值的抽象方法; +比如: steam().map() 中参数就是Function;reduce()中参数BinaryOperator (ps: BinaryOperator extends BiFunction) + +Function toInteger = Integer::valueOf; +Function backToString = toInteger.andThen(String::valueOf); + +backToString.apply("123"); // "123" +``` + +#### Optional要解决什么问题? + +在调用一个方法得到了返回值却不能直接将返回值作为参数去调用别的方法,我们首先要判断这个返回值是否为null,只有在非空的前提下才能将其作为其他方法的参数。Java 8引入了一个新的Optional类:这是一个可以为null的容器对象,如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。 + +#### 如何使用Optional来解决嵌套对象的判空问题? + +假设我们有一个像这样的类层次结构: + +```java +class Outer { + Nested nested; + Nested getNested() { + return nested; + } +} +class Nested { + Inner inner; + Inner getInner() { + return inner; + } +} +class Inner { + String foo; + String getFoo() { + return foo; + } +} +``` + +解决这种结构的深层嵌套路径是有点麻烦的。我们必须编写一堆 null 检查来确保不会导致一个 NullPointerException: + +```java +Outer outer = new Outer(); +if (outer != null && outer.nested != null && outer.nested.inner != null) { + System.out.println(outer.nested.inner.foo); +} +``` + +我们可以通过利用 Java 8 的 Optional 类型来摆脱所有这些 null 检查。map 方法接收一个 Function 类型的 lambda 表达式,并自动将每个 function 的结果包装成一个 Optional 对象。这使我们能够在一行中进行多个 map 操作。Null 检查是在底层自动处理的。 + +```java +Optional.of(new Outer()) + .map(Outer::getNested) + .map(Nested::getInner) + .map(Inner::getFoo) + .ifPresent(System.out::println); +``` + +还有一种实现相同作用的方式就是通过利用一个 supplier 函数来解决嵌套路径的问题: + +```java +Outer obj = new Outer(); +resolve(() -> obj.getNested().getInner().getFoo()) + .ifPresent(System.out::println); +``` + +#### 什么是默认方法,为什么要有默认方法? + +就是接口可以有实现方法,而且不需要实现类去实现其方法。只需在方法名前面加个default关键字即可。 + +```java +public interface A { + default void foo(){ + System.out.println("Calling A.foo()"); + } +} + +public class Clazz implements A { + public static void main(String[] args){ + Clazz clazz = new Clazz(); + clazz.foo();//调用A.foo() + } +} +``` + +- **为什么出现默认方法**? + +首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。 + +#### 什么是类型注解? + +类型注解**被用来支持在Java的程序中做强类型检查。配合插件式的check framework,可以在编译的时候检测出runtime error,以提高代码质量**。这就是类型注解的作用了。 + +1. 在java 8之前,注解只能是在声明的地方所使用,比如类,方法,属性; +2. java 8里面,注解可以应用在任何地方,比如: + +创建类实例 + +```java +new @Interned MyObject(); +``` + +类型映射 + +```java +myString = (@NonNull String) str; +``` + +implements 语句中 + +```java +class UnmodifiableList implements @Readonly List<@Readonly T> { … } +``` + +throw exception声明 + +```java +void monitorTemperature() throws @Critical TemperatureException { … } +``` + +需要注意的是,**类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解**。 + +#### 什么是重复注解? + +允许在同一申明类型(类,属性,或方法)的多次使用同一个注解 + +- **JDK8之前** + +java 8之前也有重复使用注解的解决方案,但可读性不是很好,比如下面的代码: + +```java +public @interface Authority { + String role(); +} + +public @interface Authorities { + Authority[] value(); +} + +public class RepeatAnnotationUseOldVersion { + + @Authorities({@Authority(role="Admin"),@Authority(role="Manager")}) + public void doSomeThing(){ + } +} +``` + +由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。 + +- **Jdk8重复注解** + +我们再来看看java 8里面的做法: + +```java +@Repeatable(Authorities.class) +public @interface Authority { + String role(); +} + +public @interface Authorities { + Authority[] value(); +} + +public class RepeatAnnotationUseNewVersion { + @Authority(role="Admin") + @Authority(role="Manager") + public void doSomeThing(){ } +} +``` + +不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点 + +### 6.2 Java 9+ 特性 + +#### Java 9后续版本发布是按照什么样的发布策略呢? + +Java现在发布的版本很快,每年两个,但是真正会被大规模使用的是三年一个的TLS版本。@pdai + +- 每3年发布一个TLS,长期维护版本。意味着Java 8 ,Java 11, Java 17 才可能被大规模使用。 +- 每年发布两个正式版本,分别是3月份和9月份。 + +#### Java 9后续新版本中你知道哪些? + +能够举几个即可: + +- **Java10 - 并行全垃圾回收器 G1** + +大家如果接触过 Java 性能调优工作,应该会知道,调优的最终目标是通过参数设置来达到快速、低延时的内存垃圾回收以提高应用吞吐量,尽可能的避免因内存回收不及时而触发的完整 GC(Full GC 会带来应用出现卡顿)。 + +G1 垃圾回收器是 Java 9 中 Hotspot 的默认垃圾回收器,是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是当并发收集无法快速回收内存时,会触发垃圾回收器回退进行 Full GC。之前 Java 版本中的 G1 垃圾回收器执行 GC 时采用的是基于单线程标记扫描压缩算法(mark-sweep-compact)。为了最大限度地减少 Full GC 造成的应用停顿的影响,Java 10 中将为 G1 引入多线程并行 GC,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量。 + +Java 10 中将采用并行化 mark-sweep-compact 算法,并使用与年轻代回收和混合回收相同数量的线程。具体并行 GC 线程数量可以通过: `-XX:ParallelGCThreads` 参数来调节,但这也会影响用于年轻代和混合收集的工作线程数。 + +- **Java11 - ZGC:可伸缩低延迟垃圾收集器** + +ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),这应该是 Java 11 中最为瞩目的特性,没有之一。ZGC 是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计: + +1. GC 停顿时间不超过 10ms +2. 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆 +3. 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比) +4. 方便在此基础上引入新的 GC 特性和利用 colord +5. 针以及 Load barriers 优化奠定基础 +6. 当前只支持 Linux/x64 位平台 + +停顿时间在 10ms 以下,10ms 其实是一个很保守的数据,即便是 10ms 这个数据,也是 GC 调优几乎达不到的极值。根据 SPECjbb 2015 的基准测试,128G 的大堆下最大停顿时间才 1.68ms,远低于 10ms,和 G1 算法相比,改进非常明显。 + +![img](https://pdai.tech/images/java/java-11-1.png) + +- **Java 14 - Switch 表达式**(正式版) + +switch 表达式在之前的 Java 12 和 Java 13 中都是处于预览阶段,而在这次更新的 Java 14 中,终于成为稳定版本,能够正式可用。 + +switch 表达式带来的不仅仅是编码上的简洁、流畅,也精简了 switch 语句的使用方式,同时也兼容之前的 switch 语句的使用;之前使用 switch 语句时,在每个分支结束之前,往往都需要加上 break 关键字进行分支跳出,以防 switch 语句一直往后执行到整个 switch 语句结束,由此造成一些意想不到的问题。switch 语句一般使用冒号 :来作为语句分支代码的开始,而 switch 表达式则提供了新的分支切换方式,即 -> 符号右则表达式方法体在执行完分支方法之后,自动结束 switch 分支,同时 -> 右则方法块中可以是表达式、代码块或者是手动抛出的异常。以往的 switch 语句写法如下: + +```java +int dayOfWeek; +switch (day) { + case MONDAY: + case FRIDAY: + case SUNDAY: + dayOfWeek = 6; + break; + case TUESDAY: + dayOfWeek = 7; + break; + case THURSDAY: + case SATURDAY: + dayOfWeek = 8; + break; + case WEDNESDAY: + dayOfWeek = 9; + break; + default: + dayOfWeek = 0; + break; +} +``` + +而现在 Java 14 可以使用 switch 表达式正式版之后,上面语句可以转换为下列写法: + +```java +int dayOfWeek = switch (day) { + case MONDAY, FRIDAY, SUNDAY -> 6; + case TUESDAY -> 7; + case THURSDAY, SATURDAY -> 8; +case WEDNESDAY -> 9; + default -> 0; + +}; +``` + +很明显,switch 表达式将之前 switch 语句从编码方式上简化了不少,但是还是需要注意下面几点: + +1. 需要保持与之前 switch 语句同样的 case 分支情况。 +2. 之前需要用变量来接收返回值,而现在直接使用 yield 关键字来返回 case 分支需要返回的结果。 +3. 现在的 switch 表达式中不再需要显式地使用 return、break 或者 continue 来跳出当前分支。 +4. 现在不需要像之前一样,在每个分支结束之前加上 break 关键字来结束当前分支,如果不加,则会默认往后执行,直到遇到 break 关键字或者整个 switch 语句结束,在 Java 14 表达式中,表达式默认执行完之后自动跳出,不会继续往后执行。 +5. 对于多个相同的 case 方法块,可以将 case 条件并列,而不需要像之前一样,通过每个 case 后面故意不加 break 关键字来使用相同方法块。 + +使用 switch 表达式来替换之前的 switch 语句,确实精简了不少代码,提高了编码效率,同时也可以规避一些可能由于不太经意而出现的意想不到的情况,可见 Java 在提高使用者编码效率、编码体验和简化使用方面一直在不停的努力中,同时也期待未来有更多的类似 lambda、switch 表达式这样的新特性出来。 + +- **Java 14 - Records** + +在 Java 14 中引入了 Record 类型,其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不尽完全相同,它们的共同点都是类的部分或者全部可以直接在类头中定义、描述,并且这个类只用于存储数据而已。对于 Record 类型,具体可以用下面代码来说明: + +```java +public record Person(String name, int age) { + public static String address; + + public String getName() { + return name; + } +} +``` + +对上述代码进行编译,然后反编译之后可以看到如下结果: + +```java +public final class Person extends java.lang.Record { + private final java.lang.String name; + private final java.lang.String age; + + public Person(java.lang.String name, java.lang.String age) { /* compiled code */ } + + public java.lang.String getName() { /* compiled code */ } + + public java.lang.String toString() { /* compiled code */ } + + public final int hashCode() { /* compiled code */ } + + public final boolean equals(java.lang.Object o) { /* compiled code */ } + + public java.lang.String name() { /* compiled code */ } + + public java.lang.String age() { /* compiled code */ } +} +``` + diff --git a/src/interview/Java/Java集合.md b/src/interview/Java/Java集合.md new file mode 100644 index 0000000..379197a --- /dev/null +++ b/src/interview/Java/Java集合.md @@ -0,0 +1,115 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Java 集合 +index: true +headerDepth: 3 +order: 2 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +## 2 Java 集合 + +> 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 + +### 2.1 Collection + +#### 集合有哪些类? + +- Set + - TreeSet 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。 + - HashSet 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 + - LinkedHashSet 具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 +- List + - ArrayList 基于动态数组实现,支持随机访问。 + - Vector 和 ArrayList 类似,但它是线程安全的。 + - LinkedList 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 +- Queue + - LinkedList 可以用它来实现双向队列。 + - PriorityQueue 基于堆结构实现,可以用它来实现优先队列。 + +#### ArrayList的底层? + +*ArrayList*实现了*List*接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入`null`元素,底层通过**数组实现**。除该类未实现同步外,其余跟*Vector*大致相同。每个*ArrayList*都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。 + +![ArrayList_base](https://b2files.173114.xyz/blogimg/2025/03/7a76ac539fb5f30a4e92e3a0041365e7.png) + +#### ArrayList自动扩容? + +每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 + +数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。 + +![ArrayList_add](https://b2files.173114.xyz/blogimg/2025/03/eda4b8c24ff6fc96ef99dafe70445f15.png) + +#### ArrayList的Fail-Fast机制? + +ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。 + +### 2.2 Map + +#### Map有哪些类? + +- `TreeMap` 基于红黑树实现。 +- `HashMap` 1.7基于哈希表实现,1.8基于数组+链表+红黑树。 +- `HashTable` 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁, 1.8 引入了红黑树)。 +- `LinkedHashMap` 使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 + +#### JDK7 HashMap如何实现? + +哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。**Java7 \*HashMap\*采用的是冲突链表方式**。 + +![HashMap_base](https://b2files.173114.xyz/blogimg/2025/03/77e35c8cf322cc1413b76694b15d47e0.png) + +从上图容易看出,如果选择合适的哈希函数,`put()`和`get()`方法可以在常数时间内完成。但在对*HashMap*进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将*HashMap*的初始大小设的过大。 + +有两个参数可以影响*HashMap*的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始`table`的大小,负载系数用来指定自动扩容的临界值。当`entry`的数量超过`capacity*load_factor`时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。 + +#### JDK8 HashMap如何实现? + +根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。 + +为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。 + +![img](https://b2files.173114.xyz/blogimg/2025/03/f4920fea0c56de4c7f5c9f6b9318a426.png) + +#### HashSet是如何实现的? + +*HashSet*是对*HashMap*的简单包装,对*HashSet*的函数调用都会转换成合适的*HashMap*方法 + +```Java +//HashSet是对HashMap的简单包装 +public class HashSet +{ + ...... + private transient HashMap map;//HashSet里面有一个HashMap + // Dummy value to associate with an Object in the backing Map + private static final Object PRESENT = new Object(); + public HashSet() { + map = new HashMap<>(); + } + ...... + public boolean add(E e) {//简单的方法转换 + return map.put(e, PRESENT)==null; + } + ...... +} +``` + +#### 什么是WeakHashMap? + +我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,**是否有有效的引用指向该对象**。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的**有效引用** 并不包括**弱引用**。也就是说,**虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收**。 + +WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢? + +*WeakHashMap* 里的`entry`可能会被GC自动删除,即使程序员没有调用`remove()`或者`clear()`方法。 + +***WeakHashMap\* 的这个特点特别适用于需要缓存的场景**。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。 \ No newline at end of file diff --git a/src/interview/Java/README.md b/src/interview/Java/README.md new file mode 100644 index 0000000..23c187a --- /dev/null +++ b/src/interview/Java/README.md @@ -0,0 +1,11 @@ +--- +dir: + text: Java + icon: laptop-code + collapsible: true + expanded: false + link: false + index: true + order: 1 +index: false +--- \ No newline at end of file diff --git a/src/interview/Microservices/Kubernetes.md b/src/interview/Microservices/Kubernetes.md new file mode 100644 index 0000000..d9d0ce9 --- /dev/null +++ b/src/interview/Microservices/Kubernetes.md @@ -0,0 +1,292 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Kubernetes +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +### 14.2 Kubernetes + +#### 什么是Kubernetes? Kubernetes与Docker有什么关系? + +- **是什么**? + +Kubernetes是一个开源容器管理工具,负责容器部署,容器扩缩容以及负载平衡。作为Google的创意之作,它提供了出色的社区,并与所有云提供商合作。因此,我们可以说Kubernetes不是一个容器化平台,而是一个多容器管理解决方案。 + +众所周知,Docker提供容器的生命周期管理,Docker镜像构建运行时容器。但是,由于这些单独的容器必须通信,因此使用Kubernetes。因此,我们说Docker构建容器,这些容器通过Kubernetes相互通信。因此,可以使用Kubernetes手动关联和编排在多个主机上运行的容器。 + +- **有哪些特性**? + +1. **自我修复**: 在节点故障时可以删除失效容器,重新创建新的容器,替换和重新部署,保证预期的副本数量,kill掉健康检查失败的容器,并且在容器未准备好之前不会处理客户端情况,确保线上服务不会中断 +2. **弹性伸缩**: 使用命令、UI或者k8s基于cpu使用情况自动快速扩容和缩容应用程序实例,保证应用业务高峰并发时的高可用性,业务低峰时回收资源,以最小成本运行服务 +3. **自动部署和回滚**: k8s采用滚动更新策略更新应用,一次更新一个pod,而不是同时删除所有pod,如果更新过程中出现问题,将回滚恢复,确保升级不影响业务 +4. **服务发现和负载均衡**: k8s为多个容器提供一个统一访问入口(内部IP地址和一个dns名称)并且负载均衡关联的所有容器,使得用户无需考虑容器IP问题 +5. **机密和配置管理**: 管理机密数据和应用程序配置,而不需要把敏感数据暴露在径向力,提高敏感数据安全性,并可以将一些常用的配置存储在k8s中,方便应用程序调用 +6. **存储编排**: 挂载外部存储系统,无论时来自本地存储、公有云(aws)、还是网络存储(nfs、GFS、ceph),都作为集群资源的一部分使用,极大提高存储使用灵活性 +7. **批处理**: 提供一次性任务,定时任务:满足批量数据处理和分析的场景 + +#### Kubernetes的整体架构? + +![img](https://b2files.173114.xyz/blogimg/2025/03/f1b33c06dc232070dc4ace356412d663.png) + +Kubernetes主要由以下几个核心组件组成: + +1. **etcd**:提供数据库服务保存了整个集群的状态 +2. **kube-apiserver**:提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制 +3. **kube-controller-manager**:负责维护集群的状态,比如故障检测、自动扩展、滚动更新等 +4. **cloud-controller-manager**:是与底层云计算服务商交互的控制器 +5. **kub-scheduler**:负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上 +6. **kubelet**:负责维护容器的生命周期,同时也负责Volume(CVI)和网络(CNI)的管理; +7. **kube-proxy**:负责为Service提供内部的服务发现和负载均衡,并维护网络规则 +8. **container-runtime**:是负责管理运行容器的软件,比如docker + +除了核心组件,还有一些推荐的Add-ons: + +1. kube-dns负责为整个集群提供DNS服务 +2. Ingress Controller为服务提供外网入口 +3. Heapster提供资源监控 +4. Dashboard提供GUI +5. Federation提供跨可用区的集群 +6. Fluentd-elasticsearch提供集群日志采集、存储与查询 + +#### Kubernetes中有哪些核心概念? + +- **Cluster、Master、Node** + +1. Cluster + 1. Cluster(集群) 是计算、存储和网络资源的集合,Kubernetes 利用这些资源运行各种基于容器的应用。最简单的 Cluster 可以只有一台主机(它既是 Mater 也是 Node) +2. Master + 1. Master 是 Cluster 的大脑,它的主要职责是调度,即决定将应用放在哪里运行。 + 2. Master 运行 Linux 操作系统,可以是物理机或者虚拟机。 + 3. 为了实现高可用,可以运行多个 Master。 +3. Node + 1. Node 的职责是运行容器应用。 + 2. Node 由 Master 管理,Node 负责监控并汇报容器的状态,并根据 Master 的要求管理容器的生命周期。 + 3. Node 运行在 Linux 操作系统,可以是物理机或者是虚拟机。 + +- **Pod** + +1. 基本概念 + 1. Pod 是 Kubernetes 的最小工作单元。 + 2. 每个 Pod 包含一个或多个容器。Pod 中的容器会作为一个整体被 Master 调度到一个 Node 上运行。 +2. 引入 Pod 的目的 + 1. `可管理性`: 有些容器天生就是需要紧密联系,一起工作。Pod 提供了比容器更高层次的抽象,将它们封装到一个部署单元中。Kubernetes 以 Pod 为最小单位进行调度、扩展、共享资源、管理生命周期。 + 2. `通信和资源共享`: Pod 中的所有容器使用同一个网络 namespace,即相同的 IP 地址和 Port 空间。它们可以直接用 localhost 通信。同样的,这些容器可以共享存储,当 Kubernetes 挂载 volume 到 Pod,本质上是将 volume 挂载到 Pod 中的每一个容器。 +3. Pod 的使用方式 + 1. `运行单一容器`: one-container-per-Pod 是 Kubernetes 最常见的模型,这种情况下,只是将单个容器简单封装成 Pod。即便是只有一个容器,Kubernetes 管理的也是 Pod 而不是直接管理容器。 + 2. `运行多个容器`: 对于那些联系非常紧密,而且需要直接共享资源的容器,应该放在一个 Pod 中。比如下面这个 Pod 包含两个容器:一个 File Puller,一个是 Web Server。File Puller 会定期从外部的 Content Manager 中拉取最新的文件,将其存放在共享的 volume 中。Web Server 从 volume 读取文件,响应 Consumer 的请求。这两个容器是紧密协作的,它们一起为 Consumer 提供最新的数据;同时它们也通过 volume 共享数据。所以放到一个 Pod 是合适的。 + +- **Controller** + +1. **基本概念** + 1. Kubernetes 通常不会直接创建 Pod,而是通过 Controller 来管理 Pod 的。Controller 中定义了 Pod 的部署特性,比如有几个副本,在什么样的 Node 上运行等。为了满足不同的业务场景,Kubernetes 提供了多种 Controller,包括 Deployment、ReplicaSet、DaemonSet、StatefuleSet、Job 等。 +2. **各个 Controller** + 1. `Deployment`: Deployment 是最常用的 Controller,比如我们可以通过创建 Deployment 来部署应用的。Deployment 可以管理 Pod 的多个副本,并确保 Pod 按照期望的状态运行。 + 2. `ReplicaSet`: ReplicaSet 实现了 Pod 的多副本管理。使用 Deployment 时会自动创建 ReplicaSet,也就是说 Deployment 是通过 ReplicaSet 来管理 Pod 的多个副本,我们通常不需要直接使用 ReplicaSet。 + 3. `DaemonSet`: DaemonSet 用于每个 Node 最多只运行一个 Pod 副本的场景。正如其名称所揭示的,DaemonSet 通常用于运行 daemon。 + 4. `StatefuleSet`: StatefuleSet 能够保证 Pod 的每个副本在整个生命周期中名称是不变的。而其他 Controller 不提供这个功能,当某个 Pod 发生故障需要删除并重新启动时,Pod 的名称会发生变化。同时 StatefuleSet 会保证副本按照固定的顺序启动、更新或者删除。 + 5. `Job`: Job 用于运行结束就删除的应用。而其他 Controller 中的 Pod 通常是长期持续运行。 + +- **Service、Namespace** + +1. **Service** + 1. Deployment 可以部署多个副本,每个 Pod 都有自己的 IP。而 Pod 很可能会被频繁地销毁和重启,它们的 IP 会发生变化,用 IP 来访问 Deployment 副本不太现实。 + 2. Service 定义了外界访问一组特定 Pod 的方式。Service 有自己的 IP 和端口,Service 为 Pod 提供了负载均衡。 +2. **Namespace** + 1. Namespace 可以将一个物理的 Cluster 逻辑上划分成多个虚拟 Cluster,每个 Cluster 就是一个 Namespace。不同 Namespace 里的资源是完全隔离的。 + 2. Kubernetes 默认创建了两个 Namespace: + 1. default:创建资源时如果不指定,将被放到这个 Namespace 中。 + 2. kube-system:Kubernetes 自己创建的系统资源将放到这个 Namespace 中。 + +#### 什么是Heapster? + +Heapster是由每个节点上运行的Kubelet提供的集群范围的数据聚合器。此容器管理工具在Kubernetes集群上本机支持,并作为pod运行,就像集群中的任何其他pod一样。因此,它基本上发现集群中的所有节点,并通过机上Kubernetes代理查询集群中Kubernetes节点的使用信息。 + +#### 什么是Minikube? + +Minikube是一种工具,可以在本地轻松运行Kubernetes。这将在虚拟机中运行单节点Kubernetes群集。 + +#### 什么是Kubectl? + +Kubectl是一个平台,您可以使用该平台将命令传递给集群。因此,它基本上为CLI提供了针对Kubernetes集群运行命令的方法,以及创建和管理Kubernetes组件的各种方法。 + +#### ube-apiserver和kube-scheduler的作用是什么? + +kube -apiserver遵循横向扩展架构,是主节点控制面板的前端。这将公开Kubernetes主节点组件的所有API,并负责在Kubernetes节点和Kubernetes主组件之间建立通信。 + +kube-scheduler负责工作节点上工作负载的分配和管理。因此,它根据资源需求选择最合适的节点来运行未调度的pod,并跟踪资源利用率。它确保不在已满的节点上调度工作负载。 + +#### 请你说一下kubenetes针对pod资源对象的健康监测机制? + +K8s中对于pod资源对象的健康状态检测,提供了三类probe(探针)来执行对pod的健康监测: + +- livenessProbe探针 + +可以根据用户自定义规则来判定pod是否健康,如果livenessProbe探针探测到容器不健康,则kubelet会根据其重启策略来决定是否重启,如果一个容器不包含livenessProbe探针,则kubelet会认为容器的livenessProbe探针的返回值永远成功。 + +- ReadinessProbe探针 同样是可以根据用户自定义规则来判断pod是否健康,如果探测失败,控制器会将此pod从对应service的endpoint列表中移除,从此不再将任何请求调度到此Pod上,直到下次探测成功。 +- startupProbe探针 启动检查机制,应用一些启动缓慢的业务,避免业务长时间启动而被上面两类探针kill掉,这个问题也可以换另一种方式解决,就是定义上面两类探针机制时,初始化时间定义的长一些即可。 + +#### K8s中镜像的下载策略是什么? + +可通过命令`kubectl explain pod.spec.containers`来查看imagePullPolicy这行的解释。 + +K8s的镜像下载策略有三种:Always、Never、IFNotPresent; + +- Always:镜像标签为latest时,总是从指定的仓库中获取镜像; +- Never:禁止从仓库中下载镜像,也就是说只能使用本地镜像; +- IfNotPresent:仅当本地没有对应镜像时,才从目标仓库中下载。 + +默认的镜像下载策略是:当镜像标签是latest时,默认策略是Always;当镜像标签是自定义时(也就是标签不是latest),那么默认策略是IfNotPresent。 + +#### image的状态有哪些? + +- Running:Pod所需的容器已经被成功调度到某个节点,且已经成功运行, +- Pending:APIserver创建了pod资源对象,并且已经存入etcd中,但它尚未被调度完成或者仍然处于仓库中下载镜像的过程 +- Unknown:APIserver无法正常获取到pod对象的状态,通常是其无法与所在工作节点的kubelet通信所致。 + +#### 如何控制滚动更新过程? + +可以通过下面的命令查看到更新时可以控制的参数: + +```bash +[root@master yaml]# kubectl explain deploy.spec.strategy.rollingUpdate +``` + +- **maxSurge** :此参数控制滚动更新过程,副本总数超过预期pod数量的上限。可以是百分比,也可以是具体的值。默认为1。 (上述参数的作用就是在更新过程中,值若为3,那么不管三七二一,先运行三个pod,用于替换旧的pod,以此类推) +- **maxUnavailable**:此参数控制滚动更新过程中,不可用的Pod的数量。 (这个值和上面的值没有任何关系,举个例子:我有十个pod,但是在更新的过程中,我允许这十个pod中最多有三个不可用,那么就将这个参数的值设置为3,在更新的过程中,只要不可用的pod数量小于或等于3,那么更新过程就不会停止)。 + +#### DaemonSet资源对象的特性? + +DaemonSet这种资源对象会在每个k8s集群中的节点上运行,并且每个节点只能运行一个pod,这是它和deployment资源对象的最大也是唯一的区别。所以,在其yaml文件中,不支持定义replicas,除此之外,与Deployment、RS等资源对象的写法相同。 + +它的一般使用场景如下: + +1. 在去做每个节点的日志收集工作; +2. 监控每个节点的的运行状态; + +#### 说说你对Job这种资源对象的了解? + +Job与其他服务类容器不同,Job是一种工作类容器(一般用于做一次性任务)。使用常见不多,可以忽略这个问题。 + +```yaml +#提高Job执行效率的方法: +spec: + parallelism: 2 #一次运行2个 + completions: 8 #最多运行8个 + template: +metadata: +``` + +#### pod的重启策略是什么? + +可以通过命令`kubectl explain pod.spec`查看pod的重启策略。(restartPolicy字段) + +- Always:但凡pod对象终止就重启,此为默认策略。 +- OnFailure:仅在pod对象出现错误时才重启 + +#### 描述一下pod的生命周期有哪些状态? + +- Pending:表示pod已经被同意创建,正在等待kube-scheduler选择合适的节点创建,一般是在准备镜像; +- Running:表示pod中所有的容器已经被创建,并且至少有一个容器正在运行或者是正在启动或者是正在重启; +- Succeeded:表示所有容器已经成功终止,并且不会再启动; +- Failed:表示pod中所有容器都是非0(不正常)状态退出; +- Unknown:表示无法读取Pod状态,通常是kube-controller-manager无法与Pod通信。 + +#### 创建一个pod的流程是什么? + +1) 客户端提交Pod的配置信息(可以是yaml文件定义好的信息)到kube-apiserver; 2) Apiserver收到指令后,通知给controller-manager创建一个资源对象; 3) Controller-manager通过api-server将pod的配置信息存储到ETCD数据中心中; 4) Kube-scheduler检测到pod信息会开始调度预选,会先过滤掉不符合Pod资源配置要求的节点,然后开始调度调优,主要是挑选出更适合运行pod的节点,然后将pod的资源配置单发送到node节点上的kubelet组件上。 5) Kubelet根据scheduler发来的资源配置单运行pod,运行成功后,将pod的运行信息返回给scheduler,scheduler将返回的pod运行状况的信息存储到etcd数据中心。 + +#### 删除一个Pod会发生什么事情? + +Kube-apiserver会接受到用户的删除指令,默认有30秒时间等待优雅退出,超过30秒会被标记为死亡状态,此时Pod的状态Terminating,kubelet看到pod标记为Terminating就开始了关闭Pod的工作; + +关闭流程如下: + +1. pod从service的endpoint列表中被移除; +2. 如果该pod定义了一个停止前的钩子,其会在pod内部被调用,停止钩子一般定义了如何优雅的结束进程; +3. 进程被发送TERM信号(kill -14) +4. 当超过优雅退出的时间后,Pod中的所有进程都会被发送SIGKILL信号(kill -9)。 + +#### K8s的Service是什么? + +Pod每次重启或者重新部署,其IP地址都会产生变化,这使得pod间通信和pod与外部通信变得困难,这时候,就需要Service为pod提供一个固定的入口。 + +Service的Endpoint列表通常绑定了一组相同配置的pod,通过负载均衡的方式把外界请求分配到多个pod上 + +#### k8s是怎么进行服务注册的? + +Pod启动后会加载当前环境所有Service信息,以便不同Pod根据Service名进行通信。 + +#### k8s集群外流量怎么访问Pod? + +可以通过Service的NodePort方式访问,会在所有节点监听同一个端口,比如:30000,访问节点的流量会被重定向到对应的Service上面。 + +#### k8s数据持久化的方式有哪些? + +- **EmptyDir(空目录)**: + +没有指定要挂载宿主机上的某个目录,直接由Pod内保部映射到宿主机上。类似于docker中的manager volume。 + +主要使用场景: + +1. 只需要临时将数据保存在磁盘上,比如在合并/排序算法中; +2. 作为两个容器的共享存储,使得第一个内容管理的容器可以将生成的数据存入其中,同时由同一个webserver容器对外提供这些页面。 + +emptyDir的特性: 同个pod里面的不同容器,共享同一个持久化目录,当pod节点删除时,volume的数据也会被删除。如果仅仅是容器被销毁,pod还在,则不会影响volume中的数据。 总结来说:emptyDir的数据持久化的生命周期和使用的pod一致。一般是作为临时存储使用。 + +- **Hostpath**: + +将宿主机上已存在的目录或文件挂载到容器内部。类似于docker中的bind mount挂载方式。 + +这种数据持久化方式,运用场景不多,因为它增加了pod与节点之间的耦合。 + +一般对于k8s集群本身的数据持久化和docker本身的数据持久化会使用这种方式,可以自行参考apiService的yaml文件,位于:/etc/kubernetes/main…目录下。 + +- **PersistentVolume**(简称PV): + +基于NFS服务的PV,也可以基于GFS的PV。它的作用是统一数据持久化目录,方便管理。 + +在一个PV的yaml文件中,可以对其配置PV的大小, + +指定PV的访问模式: + +1. ReadWriteOnce:只能以读写的方式挂载到单个节点; +2. ReadOnlyMany:能以只读的方式挂载到多个节点; +3. ReadWriteMany:能以读写的方式挂载到多个节点。, + +以及指定pv的回收策略(这里的回收策略指的是在PV被删除后,在这个PV下所存储的源文件是否删除): + +1. recycle:清除PV的数据,然后自动回收; +2. Retain:需要手动回收; +3. delete:删除云存储资源,云存储专用; + +若需使用PV,那么还有一个重要的概念:PVC,PVC是向PV申请应用所需的容量大小,K8s集群中可能会有多个PV,PVC和PV若要关联,其定义的访问模式必须一致。定义的storageClassName也必须一致,若群集中存在相同的(名字、访问模式都一致)两个PV,那么PVC会选择向它所需容量接近的PV去申请,或者随机申请。 + +#### Replica Set 和 Replication Controller 之间有什么区别? + +Replica Set 和 Replication Controller 几乎完全相同。它们都确保在任何给定时间运行指定数量的 Pod 副本。不同之处在于复制 Pod 使用的选择器。Replica Set 使用基于集合的选择器,而 Replication Controller 使用基于权限的选择器。 + +Equity-Based 选择器:这种类型的选择器允许按标签键和值进行过滤。因此,在外行术语中,基于 Equity 的选择器将仅查找与标签具有完全相同短语的 Pod。示例:假设您的标签键表示 app = nginx,那么使用此选择器,您只能查找标签应用程序等于 nginx 的那些 Pod。 + +Selector-Based 选择器:此类型的选择器允许根据一组值过滤键。因此,换句话说,基于 Selector 的选择器将查找已在集合中提及其标签的 Pod。示例:假设您的标签键在(nginx、NPS、Apache)中显示应用程序。然后,使用此选择器,如果您的应用程序等于任何 nginx、NPS或 Apache,则选择器将其视为真实结果。 + +#### 其它 + +基础篇 基础篇主要面向的初级、中级开发工程师职位,主要考察对k8s本身的理解。 + +kubernetes包含几个组件。各个组件的功能是什么。组件之间是如何交互的。 k8s的pause容器有什么用。是否可以去掉。 k8s中的pod内几个容器之间的关系是什么。 一个经典pod的完整生命周期。 k8s的service和ep是如何关联和相互影响的。 详述kube-proxy原理,一个请求是如何经过层层转发落到某个pod上的整个过程。请求可能来自pod也可能来自外部。 rc/rs功能是怎么实现的。详述从API接收到一个创建rc/rs的请求,到最终在节点上创建pod的全过程,尽可能详细。另外,当一个pod失效时,kubernetes是如何发现并重启另一个pod的? deployment/rs有什么区别。其使用方式、使用条件和原理是什么。 cgroup中的cpu有哪几种限制方式。k8s是如何使用实现request和limit的。 + +拓展实践篇 拓展实践篇主要面向的高级开发工程师、架构师职位,主要考察实践经验和技术视野。 + +设想一个一千台物理机,上万规模的容器的kubernetes集群,请详述使用kubernetes时需要注意哪些问题?应该怎样解决?(提示可以从高可用,高性能等方向,覆盖到从镜像中心到kubernetes各个组件等) 设想kubernetes集群管理从一千台节点到五千台节点,可能会遇到什么样的瓶颈。应该如何解决。 kubernetes的运营中有哪些注意的要点。 集群发生雪崩的条件,以及预防手段。 设计一种可以替代kube-proxy的实现 sidecar的设计模式如何在k8s中进行应用。有什么意义。 灰度发布是什么。如何使用k8s现有的资源实现灰度发布。 介绍k8s实践中踩过的比较大的一个坑和解决方式。 diff --git a/src/interview/Microservices/ServiceMesh.md b/src/interview/Microservices/ServiceMesh.md new file mode 100644 index 0000000..9782c0a --- /dev/null +++ b/src/interview/Microservices/ServiceMesh.md @@ -0,0 +1,30 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: Service Mesh +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +### 14.3 Service Mesh + +#### 什么是Service Mesh(服务网格)? + +Service Mesh是专用的基础设施层,轻量级高性能网络代理。提供安全的、快速的、可靠地服务间通讯,与实际应用部署一起,但对应用透明。 + +为了帮助理解, 下图展示了服务网格的典型边车部署方式: + +https://www.jianshu.com/p/cc5b54ad8d5f + +#### 什么是Istio? + +#### Istio的架构? diff --git a/src/interview/Microservices/SpringCloud.md b/src/interview/Microservices/SpringCloud.md new file mode 100644 index 0000000..caa10b1 --- /dev/null +++ b/src/interview/Microservices/SpringCloud.md @@ -0,0 +1,321 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: SpringCloud +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +## 14 微服务 + +### 14.1 Spring Cloud + +#### 什么是微服务?谈谈你对微服务的理解? + +- **微服务** + +以前所有的代码都放在同一个工程中、部署在同一个服务器、同一项目的不同模块不同功能互相抢占资源,微服务就是将工程根据不同的业务规则拆分成微服务,部署在不同的服务器上,服务之间相互调用,java中有的微服务有dubbo(只能用来做微服务)、springcloud( 提供了服务的发现、断路器等)。 + +- **微服务的特点**: + +1. 按业务划分为一个独立运行的程序,即服务单元 +2. 服务之间通过HTTP协议相互通信 +3. 自动化部署 +4. 可以用不同的编程语言 +5. 可以用不同的存储技术 +6. 服务集中化管理 +7. 微服务是一个分布式系统 + +- **微服务的优势** + +1. 将一个复杂的业务拆分为若干小的业务,将复杂的业务简单化,新人只需要了解他所接管的服务的代码,减少了新人的学习成本。 +2. 由于微服务是分布式服务,服务于服务之间没有任何耦合。微服务系统的微服务单元具有很强的横向拓展能力。 +3. 服务于服务之间采用HTTP网络通信协议来通信,单个服务内部高度耦合,服务与服务之间完全独立,无耦合。这使得微服务可以采用任何的开发语言和技术来实现,提高开发效率、降低开发成本。 +4. 微服务是按照业务进行拆分的,并有坚实的服务边界,若要重写某一业务代码,不需了解所有业务,重写简单。 +5. 微服务的每个服务单元是独立部署的,即独立运行在某个进程中,微服务的修改和部署对其他服务没有影响。 +6. 微服务在CAP理论中采用的AP架构,具有高可用分区容错特点。高可用主要体现在系统7x24不间断服务,他要求系统有大量的服务器集群,从而提高系统的负载能力。分区容错也使得系统更加健壮。 + +- **微服务的不足** + +1. 微服务的复杂度:构建一个微服务比较复杂,服务与服务之间通过HTTP协议或其他消息传递机制通信,开发者要选出最佳的通信机制,并解决网络服务差时带来的风险。 2.分布式事物:将事物分成多阶段提交,如果一阶段某一节点失败仍会导致数据不正确。如果事物涉及的节点很多,某一节点的网络出现异常会导致整个事务处于阻塞状态,大大降低数据库的性能。 +2. 服务划分:将一个完整的系统拆分成很多个服务,是一件非常困难的事,因为这涉及了具体的业务场景 +3. 服务部署:最佳部署容器Docker + +- **微服务和SOA的关系** + +微服务相对于和ESB联系在一起的SOA轻便敏捷的多,微服务将复杂的业务组件化,也是一种面向服务思想的体现。对于微服务来说,它是SOA的一种体现,但是它比ESB实现的SOA更加轻便、敏捷和简单。 + +#### 什么是Spring Cloud? + +Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、智能路由、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。 + +Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。 + +- **SpringCloud的优点** + +1. 耦合度比较低。不会影响其他模块的开发。 +2. 减轻团队的成本,可以并行开发,不用关注其他人怎么开发,先关注自己的开发。 +3. 配置比较简单,基本用注解就能实现,不用使用过多的配置文件。 +4. 微服务跨平台的,可以用任何一种语言开发。 +5. 每个微服务可以有自己的独立的数据库也有用公共的数据库。 +6. 直接写后端的代码,不用关注前端怎么开发,直接写自己的后端代码即可,然后暴露接口,通过组件进行服务通信。 + +- **SpringCloud的缺点** + +1. 部署比较麻烦,给运维工程师带来一定的麻烦。 +2. 针对数据的管理比麻烦,因为微服务可以每个微服务使用一个数据库。 +3. 系统集成测试比较麻烦 +4. 性能的监控比较麻烦。 + +#### springcloud中的组件有那些? + +说出主要的组件: + +- Spring Cloud Eureka,服务注册中心,特性有失效剔除、服务保护 +- Spring Cloud Zuul,API服务网关,功能有路由分发和过滤 +- Spring Cloud Config,分布式配置中心,支持本地仓库、SVN、Git、Jar包内配置等模式 +- Spring Cloud Ribbon,客户端负载均衡,特性有区域亲和,重试机制 +- Spring Cloud Hystrix,客户端容错保护,特性有服务降级、服务熔断、请求缓存、请求合并、依赖隔离 +- Spring Cloud Feign,声明式服务调用本质上就是Ribbon+Hystrix +- Spring Cloud Stream,消息驱动,有Sink、Source、Processor三种通道,特性有订阅发布、消费组、消息分区 +- Spring Cloud Bus,消息总线,配合Config仓库修改的一种Stream实现, +- Spring Cloud Sleuth,分布式服务追踪,需要搞清楚TraceID和SpanID以及抽样,如何与ELK整合 + +#### 具体说说SpringCloud主要项目? + +Spring Cloud的子项目,大致可分成两类,一类是对现有成熟框架"Spring Boot化"的封装和抽象,也是数量最多的项目;第二类是开发了一部分分布式系统的基础设施的实现,如Spring Cloud Stream扮演的就是kafka, ActiveMQ这样的角色。 + +- **Spring Cloud Config** Config能够管理所有微服务的配置文件 + +集中配置管理工具,分布式系统中统一的外部配置管理,默认使用Git来存储配置,可以支持客户端配置的刷新及加密、解密操作。 + +- **Spring Cloud Netflix** + +Netflix OSS 开源组件集成,包括Eureka、Hystrix、Ribbon、Feign、Zuul等核心组件。 + +1. Eureka:服务治理组件,包括服务端的注册中心和客户端的服务发现机制; +2. Ribbon:负载均衡的服务调用组件,具有多种负载均衡调用策略; +3. Hystrix:服务容错组件,实现了断路器模式,为依赖服务的出错和延迟提供了容错能力; +4. Feign:基于Ribbon和Hystrix的声明式服务调用组件; +5. Zuul:API网关组件,对请求提供路由及过滤功能。 + +- **Spring Cloud Bus** + +1. 用于传播集群状态变化的消息总线,使用轻量级消息代理链接分布式系统中的节点,可以用来动态刷新集群中的服务配置信息。 +2. 简单来说就是修改了配置文件,发送一次请求,所有客户端便会重新读取配置文件(需要利用中间插件MQ)。 + +- **Spring Cloud Consul** + +Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案,Consul 的方案更“一站式”,内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其它工具(比如 ZooKeeper 等)。使用起来也较为简单。Consul 使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与 Docker 等轻量级容器可无缝配合。 + +- **Spring Cloud Security** + +Spring Cloud Security提供了一组原语,用于构建安全的应用程序和服务,而且操作简便。可以在外部(或集中)进行大量配置的声明性模型有助于实现大型协作的远程组件系统,通常具有中央身份管理服务。它也非常易于在Cloud Foundry等服务平台中使用。在Spring Boot和Spring Security OAuth2的基础上,可以快速创建实现常见模式的系统,如单点登录,令牌中继和令牌交换。 + +- **Spring Cloud Sleuth** + +在微服务中,通常根据业务模块分服务,项目中前端发起一个请求,后端可能跨几个服务调用才能完成这个请求(如下图)。如果系统越来越庞大,服务之间的调用与被调用关系就会变得很复杂,假如一个请求中需要跨几个服务调用,其中一个服务由于网络延迟等原因挂掉了,那么这时候我们需要分析具体哪一个服务出问题了就会显得很困难。Spring Cloud Sleuth服务链路跟踪功能就可以帮助我们快速的发现错误根源以及监控分析每条请求链路上的性能等等。 + +- **Spring Cloud Stream** + +轻量级事件驱动微服务框架,可以使用简单的声明式模型来发送及接收消息,主要实现为Apache Kafka及RabbitMQ。 + +- **Spring Cloud Task** + +Spring Cloud Task的目标是为Spring Boot应用程序提供创建短运行期微服务的功能。在Spring Cloud Task中,我们可以灵活地动态运行任何任务,按需分配资源并在任务完成后检索结果。Tasks是Spring Cloud Data Flow中的一个基础项目,允许用户将几乎任何Spring Boot应用程序作为一个短期任务执行。 + +- **Spring Cloud Zookeeper** + +1. SpringCloud支持三种注册方式Eureka, Consul(go语言编写),zookeeper +2. Spring Cloud Zookeeper是基于Apache Zookeeper的服务治理组件。 + +- **Spring Cloud Gateway** + +Spring cloud gateway是spring官方基于Spring 5.0、Spring Boot2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。 + +- **Spring Cloud OpenFeign** + +Feign是一个声明性的Web服务客户端。它使编写Web服务客户端变得更容易。要使用Feign,我们可以将调用的服务方法定义成抽象方法保存在本地添加一点点注解就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。 + +#### Spring Cloud项目部署架构? + +![img](https://b2files.173114.xyz/blogimg/2025/03/95e090c049f5ea39247b9373849e6f34.jpeg) + +#### Spring Cloud 和dubbo区别? + +- 服务调用方式:dubbo是RPC springcloud Rest Api +- 注册中心:dubbo 是zookeeper springcloud是eureka,也可以是zookeeper +- 服务网关,dubbo本身没有实现,只能通过其他第三方技术整合,springcloud有Zuul路由网关,作为路由服务器,进行消费者的请求分发,springcloud支持断路器,与git完美集成配置文件支持版本控制,事物总线实现配置文件的更新与服务自动装配等等一系列的微服务架构要素。 + +#### 服务注册和发现是什么意思?Spring Cloud 如何实现? + +当我们开始一个项目时,我们通常在属性文件中进行所有的配置。随着越来越多的服务开发和部署,添加和修改这些属性变得更加复杂。有些服务可能会下降,而某些位置可能会发生变化。手动更改属性可能会产生问题。 Eureka 服务注册和发现可以在这种情况下提供帮助。由于所有服务都在 Eureka 服务器上注册并通过调用 Eureka 服务器完成查找,因此无需处理服务地点的任何更改和处理。 + +#### 什么是Eureka? + +Eureka作为SpringCloud的服务注册功能服务器,他是服务注册中心,系统中的其他服务使用Eureka的客户端将其连接到Eureka Service中,并且保持心跳,这样工作人员可以通过Eureka Service来监控各个微服务是否运行正常。 + +#### Eureka怎么实现高可用? + +集群吧,注册多台Eureka,然后把SpringCloud服务互相注册,客户端从Eureka获取信息时,按照Eureka的顺序来访问。 + +#### 什么是Eureka的自我保护模式? + +默认情况下,如果Eureka Service在一定时间内没有接收到某个微服务的心跳,Eureka Service会进入自我保护模式,在该模式下Eureka Service会保护服务注册表中的信息,不再删除注册表中的数据,当网络故障恢复后,Eureka Servic 节点会自动退出自我保护模式 + +#### DiscoveryClient的作用? + +可以从注册中心中根据服务别名获取注册的服务器信息。 + +#### Eureka和ZooKeeper都可以提供服务注册与发现的功能,请说说两个的区别? + +1. ZooKeeper中的节点服务挂了就要选举,在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用的,选举就是改微服务做了集群,必须有一台主其他的都是从 +2. Eureka各个节点是平等关系,服务器挂了没关系,只要有一台Eureka就可以保证服务可用,数据都是最新的。如果查询到的数据并不是最新的,就是因为Eureka的自我保护模式导致的 +3. Eureka本质上是一个工程,而ZooKeeper只是一个进程 +4. Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像ZooKeeper 一样使得整个注册系统瘫痪 +5. ZooKeeper保证的是CP,Eureka保证的是AP + +#### 什么是网关? + +网关相当于一个网络服务架构的入口,所有网络请求必须通过网关转发到具体的服务。 + +#### 网关的作用是什么? + +统一管理微服务请求,权限控制、负载均衡、路由转发、监控、安全控制黑名单和白名单等 + +#### 什么是Spring Cloud Zuul(服务网关)? + +Zuul是对SpringCloud提供的成熟对的路由方案,他会根据请求的路径不同,网关会定位到指定的微服务,并代理请求到不同的微服务接口,他对外隐蔽了微服务的真正接口地址。 + +- 三个重要概念:动态路由表,路由定位,反向代理: + - 动态路由表:Zuul支持Eureka路由,手动配置路由,这俩种都支持自动更新 + - 路由定位:根据请求路径,Zuul有自己的一套定位服务规则以及路由表达式匹配 + - 反向代理:客户端请求到路由网关,网关受理之后,在对目标发送请求,拿到响应之后在 给客户端 +- 它可以和Eureka,Ribbon,Hystrix等组件配合使用, +- Zuul的应用场景: + - 对外暴露,权限校验,服务聚合,日志审计等 + +#### 网关与过滤器有什么区别? + +网关是对所有服务的请求进行分析过滤,过滤器是对单个服务而言。 + +#### 常用网关框架有那些? + +Nginx、Zuul、Gateway + +#### Zuul与Nginx有什么区别? + +Zuul是java语言实现的,主要为java服务提供网关服务,尤其在微服务架构中可以更加灵活的对网关进行操作。Nginx是使用C语言实现,性能高于Zuul,但是实现自定义操作需要熟悉lua语言,对程序员要求较高,可以使用Nginx做Zuul集群。 + +#### 既然Nginx可以实现网关?为什么还需要使用Zuul框架? + +Zuul是SpringCloud集成的网关,使用Java语言编写,可以对SpringCloud架构提供更灵活的服务。 + +#### ZuulFilter常用有那些方法? + +- Run():过滤器的具体业务逻辑 +- shouldFilter():判断过滤器是否有效 +- filterOrder():过滤器执行顺序 +- filterType():过滤器拦截位置 + +#### 如何实现动态Zuul网关路由转发? + +通过path配置拦截请求,通过ServiceId到配置中心获取转发的服务列表,Zuul内部使用Ribbon实现本地负载均衡和转发。 + +#### Zuul网关如何搭建集群? + +使用Nginx的upstream设置Zuul服务集群,通过location拦截请求并转发到upstream,默认使用轮询机制对Zuul集群发送请求。 + +#### Ribbon是什么? + +Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法 + +Ribbon客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件中列出后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。(有点类似Nginx) + +#### Nginx与Ribbon的区别? + +Nginx是反向代理同时可以实现负载均衡,nginx拦截客户端请求采用负载均衡策略根据upstream配置进行转发,相当于请求通过nginx服务器进行转发。Ribbon是客户端负载均衡,从注册中心读取目标服务器信息,然后客户端采用轮询策略对服务直接访问,全程在客户端操作。 + +#### Ribbon底层实现原理? + +Ribbon使用discoveryClient从注册中心读取目标服务信息,对同一接口请求进行计数,使用%取余算法获取目标服务集群索引,返回获取到的目标服务信息。 + +#### @LoadBalanced注解的作用? + + 开启客户端负载均衡。 + +#### 什么是断路器 + +当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应 当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应) + +断路器有三种状态 + +- 打开状态:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务 +- 半开状态:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭 +- 关闭状态:当服务一直处于正常状态 能正常调用 + +#### 什么是 Hystrix? + +在分布式系统,我们一定会依赖各种服务,那么这些个服务一定会出现失败的情况,就会导致雪崩,Hystrix就是这样的一个工具,防雪崩利器,它具有服务降级,服务熔断,服务隔离,监控等一些防止雪崩的技术。 + +Hystrix有四种防雪崩方式: + +- 服务降级:接口调用失败就调用本地的方法返回一个空 +- 服务熔断:接口调用失败就会进入调用接口提前定义好的一个熔断的方法,返回错误信息 +- 服务隔离:隔离服务之间相互影响 +- 服务监控:在服务发生调用时,会将每秒请求数、成功请求数等运行指标记录下来。 + +#### 什么是Feign? + +Feign 是一个声明web服务客户端,这使得编写web服务客户端更容易 + +他将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致 + +#### SpringCloud有几种调用接口方式? + +- Feign +- RestTemplate + +#### Ribbon和Feign调用服务的区别? + +调用方式同:Ribbon需要我们自己构建Http请求,模拟Http请求然后通过RestTemplate发给其他服务,步骤相当繁琐 + +而Feign则是在Ribbon的基础上进行了一次改进,采用接口的形式,将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。 + +#### 什么是 Spring Cloud Bus? + +- Spring Cloud Bus就像一个分布式执行器,用于扩展的Spring Boot应用程序的配置文件,但也可以用作应用程序之间的通信通道。 +- Spring Cloud Bus 不能单独完成通信,需要配合MQ支持 +- Spring Cloud Bus一般是配合Spring Cloud Config做配置中心的 +- Springcloud config实时刷新也必须采用SpringCloud Bus消息总线 + +#### 什么是Spring Cloud Config? + +Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持,可以方便的对微服务各个环境下的配置进行集中式管理。Spring Cloud Config分为Config Server和Config Client两部分。Config Server负责读取配置文件,并且暴露Http API接口,Config Client通过调用Config Server的接口来读取配置文件。 + +#### 分布式配置中心有那些框架? + +Apollo、zookeeper、springcloud config。 + +#### 分布式配置中心的作用? + +动态变更项目配置信息而不必重新部署项目。 + +#### SpringCloud Config 可以实现实时刷新吗? + +springcloud config实时刷新采用SpringCloud Bus消息总线。 + +#### 什么是Spring Cloud Gateway? + +Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。 + +使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。 diff --git a/src/interview/Other/Other.md b/src/interview/Other/Other.md new file mode 100644 index 0000000..7adc064 --- /dev/null +++ b/src/interview/Other/Other.md @@ -0,0 +1,287 @@ +--- +# dir: +# text: Java全栈面试 +# icon: laptop-code +# collapsible: true +# expanded: true +# link: true +# index: true +title: 其他 +index: true +headerDepth: 3 +# icon: laptop-code +# sidebar: true +# toc: true +# editLink: false +--- + +## 16 其它 + +### 16.1 设计模式 + +### 16.2 开源协议 + +#### 说说常见的开源协议? + +最流行的六种:MIT、Apache、BSD、GPL和LGPL、Mozilla。 + +![img](https://pdai.tech/images/dev_opensource_1.png) + +#### GPL协议、LGPL协议与BSD协议的法律区别? + +简而言之,GPL协议就是一个开放源代码协议,软件的初始开发者使用了GPL协议并公开软件的源程序后,后续使用该软件源程序开发软件者亦应当根据GPL协议把自己编写的源程序进行公开。GPL协议要求的关键在于开放源程序,但并不排斥软件作者向用户收费。虽然如此,很多大公司对GPL协议还是又爱又恨,爱的是这个协议项下的软件历经众多程序员千锤百炼的修改,已经非常成熟完善,恨的是必须开放自己后续的源程序,导致竞争对手也可以根据自己修改的源程序开发竞争产品。 + +正因大公司对GPL协议在商业上存在顾虑,因此,另两种协议被采用的更多,第一种是LGPL(亦称GPL V2)协议,可以翻译为更宽松的GPL协议。与GPL协议的区别为,后者如果只是对LGPL软件的程序库的程序进行调用而不是包含其源代码时,相关的源程序无需开源。调用和包含的区别类似在互联网网网页上对他人网页内容的引用: 如果把他人的内容全部或部分复制到自己的网页上,就类似包含,如果只是贴一个他人网页的网址链接而不引用内容,就类似调用。有了这个协议,很多大公司就可以把很多自己后续开发内容的源程序隐藏起来。 + +第二种是BSD协议(类似的还有MIT协议)。BSD协议鼓励软件的作者公开自己后续开发的源代码,但不强求。在BSD协议项下开发的软件,原始的源程序是开放源代码的,但使用者修改以后,可以自行选择发布源程序或者二进制程序(即目标程序),当然,使用者有义务把自己原来使用的源程序与BSD协议在软件对外发布时一并发布。因为比较灵活,所以BSD深受大公司的欢迎。 + +#### MongoDB修改开源协议? + +2018年10月,MongoDB宣布其开源许可证将从GNU AGPLv3,切换到SSPL,新许可证将适用于新版本的MongoDB Community Server以及打过补丁的旧版本。 + +根据 MongoDB 之前的 GNU AGPLv3 协议,想要将 MongoDB 作为公共服务运行的公司必须将他们的软件开源,或需要从 MongoDB 获得商业许可,”该公司解释说,“然而,MongoDB 的普及使一些组织在违反 GNU AGPLv3 协议的边缘疯狂试探,甚至直接违反了协议。” + +尽管 SSPL 与 GNU AGPLv3 没有什么不同,但 SSPL 会明确要求托管 MongoDB 实例的云计算公司要么从 MongoDB 获取商业许可证,要么向社区开源其服务代码。 + +随后Red Hat宣布,将不会在Red Hat Enterprise Linux或Fedora中使用MongoDB。事实上,MongoDB修改开源协议之后,Red Hat并不是首家弃用的Linux社区。2018年12月5日,Linux发行版Debian在邮件列表中讨论并决定不使用SSPL协议下的软件。2019年1月,Fedora Legal也对SSPL v1协议做出了相关决定,Fedora已确定服务器端公共许可证v1(SSPL)不是自由软件许可证。 + +### 16.3 软件理论 + +#### 什么是CAP理论? + +CAP原理指的是,在分布式系统中这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数Web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。 + +1. 一致性(Consistency):数据在多个副本之间是否能够保持一致的特性。(当一个系统在一致状态下更新后,应保持系统中所有数据仍处于一致的状态) +2. 可用性(Availability):系统提供的服务必须一直处于可用状态,对每一个操作的请求必须在有限时间内返回结果。 +3. 分区容错性(Tolerance of network Partition):分布式系统在遇到网络分区故障时,仍然需要保证对外提供一致性和可用性的服务,除非整个网络都发生故障。 + +**为什么只能同时满足两个**? + +例如,服务器中原本存储的value=0,当客户端A修改value=1时,为了保证数据的一致性,要写到3个服务器中,当服务器C故障时,数据无法写入服务器C,则导致了此时服务器A、B和C的value是不一致的。这时候要保证分区容错性,即当服务器C故障时,仍然能保持良好的一致性和可用性服务,则Consistency和Availability不能同时满足。为什么呢? + +如果满足了一致性,则客户端A的写操作value=1不能成功,这时服务器中所有value=0。 如果满足可用性,即所有客户端都可以提交操作并得到返回的结果,则此时允许客户端A写入服务器A和B,客户端C将得到未修改之前的value=0结果。 + +#### 什么是BASE理论? + +1. **Basically Available**(基本可用)分布式系统在出现不可预知故障的时候,允许损失部分可用性 +2. **Soft state**(软状态)软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 +3. **Eventually consistent**(最终一致性)最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性 + +**CAP 与 BASE 关系**? + +BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),更具体地说,是对 CAP 中 AP 方案的一个补充。其基本思路就是:通过业务,牺牲强一致性而获得可用性,并允许数据在一段时间内是不一致的,但是最终达到一致性状态。 + +**CAP 与 ACID 关系**? + +ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。 + +ACID 和 BASE 代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此 ACID 和 BASE 又会结合使用。 + +#### 什么是SOLID原则? + +- **S单一职责SRP Single-Responsibility Principle** + +一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合,高内聚在面向对象原则的引申,将职责定义为引起变化的原因,以提高内聚性减少引起变化的原因。 + +比如: SpringMVC 中Entity,DAO,Service,Controller, Util等的分离。 + +- **O开放封闭原则OCP Open - Closed Principle** + +对扩展开放,对修改关闭(设计模式的核心原则) + +比如: 设计模式中模板方法模式和观察者模式都是开闭原则的极好体现 + +- **L里氏替换原则LSP Liskov Substitution Principle** + +任何基类可以出现的地方,子类也可以出现;这一思想表现为对继承机制的约束规范,只有子类能够替换其基类时,才能够保证系统在运行期内识别子类,这是保证继承复用的基础。 + +比如:正方形是长方形是理解里氏代换原则的经典例子。(讲的是基类和子类的关系,只有这种关系存在时,里氏代换原则才存在) + +- **I接口隔离法则ISL Interface Segregation Principle** + +客户端不应该依赖那些它不需要的接口。(接口隔离原则是指使用多个专门的接口,而不使用单一的总接口; 这个法则与迪米特法则是相通的) + +- **D依赖倒置原则DIP Dependency-Inversion Principle** + +要依赖抽象,而不要依赖具体的实现, 具体而言就是高层模块不依赖于底层模块,二者共同依赖于抽象。抽象不依赖于具体, 具体依赖于抽象。 + +#### 什么是合成/聚合复用原则? + +Composite/Aggregate ReusePrinciple ,CARP: 要尽量使用对象组合,而不是继承关系达到软件复用的目的。 + +组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。 + +此原则和里氏代换原则氏相辅相成的,两者都是具体实现"开-闭"原则的规范。违反这一原则,就无法实现"开-闭"原则。 + +#### 什么是迪米特法则? + +Law of Demeter,LoD: 系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度. + +又叫最少知识原则(Least Knowledge Principle或简写为LKP). + +- 不要和“陌生人”说话。英文定义为: Don't talk to strangers. +- 只与你的直接朋友通信。英文定义为: Talk only to your immediate friends. + +比如:外观模式Facade(结构型) + +#### 什么是康威定律? + +康威在一篇文章中描述: 设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。 + +- 定律一: 组织沟通方式会通过系统设计表达出来,就是说架构的布局和组织结构会有相似。 +- 定律二: 时间再多一件事情也不可能做的完美,但总有时间做完一件事情。一口气吃不成胖子,先搞定能搞定的。 +- 定律三: 线型系统和线型组织架构间有潜在的异质同态特性。种瓜得瓜,做独立自治的子系统减少沟通成本。 +- 定律四: 大的系统组织总是比小系统更倾向于分解。合久必分,分而治之。 + +### 16.4 软件成熟度模型 + +#### 什么是CMM? + +由美国卡内基梅隆大学的软件工程研究所(SEI)创立的CMM(Capability Maturity Model 软件能力成熟度模型)认证评估,在过去的十几年中,对全球的软件产业产生了非常深远的影响。CMM共有五个等级,分别标志着软件企业能力成熟度的五个层次。从低到高,软件开发生产计划精度逐级升高,单位工程生产周期逐级缩短,单位工程成本逐级降低。据SEI统计,通过评估的软件公司对项目的估计与控制能力约提升40%到50%;生产率提高10%到20%,软件产品出错率下降超过1/3。 + +对一个软件企业来说,达到CMM2就基本上进入了规模开发,基本具备了一个现代化软件企业的基本架构和方法,具备了承接外包项目的能力。CMM3评估则需要对大软件集成的把握,包括整体架构的整合。一般来说,通过CMM认证的级别越高,其越容易获得用户的信任,在国内、国际市场上的竞争力也就越强。因此,是否能够通过CMM认证也成为国际上衡量软件企业工程开发能力的一个重要标志。 + +CMM是目前世界公认的软件产品进入国际市场的通行证,它不仅仅是对产品质量的认证,更是一种软件过程改善的途径。参与CMM评估的博科负责人表示,通过CMM的评估认证不是目标,它只是推动软件企业在产品的研发、生产、服务和管理上不断成熟和进步的手段,是一种持续提升和完善企业自身能力的过程。此次由美国PIA咨询公司负责评估并最终通过CMM3认证,标志着博科在质量管理的能力已经上升到一个新的高度。 + +#### 什么是CMMI5 呢? + +CMMI全称是Capability Maturity Model Integration, 即软件能力成熟度模型集成模型,是由美国国防部与卡内基-梅隆大学和美国国防工业协会共同开发和研制的。CMMI是一套融合多学科的、可扩充的产品集合, 其研制的初步动机是为了利用两个或多个单一学科的模型实现一个组织的集成化过程改进 + +CMMI分为五个等级,二十五个过程区域(PA)。 + +1. **初始级** 软件过程是**无序的**,有时甚至是混乱的,对过程几乎没有定义,成功取决于个人努力。管理是反应式的。 + +2. 已管理级 建立了**基本的项目管理**过程来跟踪费用、进度和功能特性。制定了必要的过程纪律,能重复早先类似应用项目取得的成功经验。 + +3. 已定义级 已将软件管理和工程两方面的过程**文档化、标准化**,并综合成该组织的标准软件过程。所有项目均使用经批准、剪裁的标准软件过程来开发和维护软件,软件产品的生产在整个软件过程是可见的。 + +4. 量化管理级 分析对软件过程和产品质量的详细度量数据,对软件过程和产品都有定量的理解与控制。管理有一个作出结论的客观依据,管理能够在定量的范围内**预测**性能。 + +5. 优化管理级 过程的**量化反馈**和先进的新思想、新技术促使过程持续不断改进。 + +每个等级都被分解为过程域,特殊目标和特殊实践,通用目标、通用实践和共同特性: + +每个等级都有几个过程区域组成,这几个过程域共同形成一种软件过程能力。每个过程域,都有一些特殊目标和通用目标,通过相应的特殊实践和通用实践来实现这些目标。当一个过程域的所有特殊实践和通用实践都按要求得到实施,就能实现该过程域的目标。 + +#### CMMI与CMM的区别呢? + +CMM是指“能力成熟度模型”,其英文全称为Capability Maturity Model for Software; + +CMMI 是指“能力成熟度模型集成”,全称为:Capability Maturity Model Integration; + +CMMI是系统工程和软件工程的集成成熟度模型,CMMI更适合于信息系统集成企业。CMMI是在CMM基础上发展起来的,它继承并发扬了CMM的优良特性,借鉴了其他模型的优点,融入了新的理论和实际研究成果。它不仅能够应用在软件工程领域,而且可以用于系统工程及其他工程领域。 + +#### CMM与ISO9000的主要区别? + +1.CMM是专门针对软件产品开发和服务的,而ISO9000涉及的范围则相当宽。 + +2.CMM强调软件开发过程的成熟度,即过程的不断改进和提高。而ISO9000则强调可接收的质量体系的最低标准。 + +### 16.5 等级保护 + +#### 为什么是做等级保护? + +1. **法律法规要求** + +《网络安全法》明确规定信息系统运营、使用单位应当按照网络安全等级保护制度要求,履行安全保护义务,如果拒不履行,将会受到相应处罚。 + +第二十一条:国家实行网络安全等级保护制度。网络运营者应当按照网络安全等级保护制度的要求,履行下列安全保护义务,保障网络免受干扰、破坏或者未经授权的访问,防止网络数据泄露或者被窃取、篡改。 + +1. **行业要求** + +在金融、电力、广电、医疗、教育等行业,主管单位明确要求从业机构的信息系统(APP)要开展等级保护工作。 + +1. **企业系统安全的需求** + +信息系统运营、使用单位通过开展等级保护工作可以发现系统内部的安全隐患与不足之处,可通过安全整改提升系统的安全防护能力,降低被攻击的风险。 + +简单来说,《网络安全法》一直对网站、信息系统、APP有等级保护要求,中小型企业通常是行业要求才意识到问题。 + +#### 等级保护分为哪些等级? + +- **第一级 自主保护级**: + +(无需备案,对测评周期无要求)此类信息系统受到破坏后,会对公民、法人和其他组织的合法权益造成一般损害,不损害国家安全、社会秩序和公共利益。 + +- **第二级 指导保护级**: + +(公安部门备案,建议两年测评一次)此类信息系统受到破坏后,会对公民、法人和其他组织的合法权益造成严重损害。会对社会秩序、公共利益造成一般损害,不损害国家安全。 + +- **第三级 监督保护级**: + +(公安部门备案,要求每年测评一次)此类信息系统受到破坏后,会对国家安全、社会秩序造成损害,对公共利益造成严重损害,对公民、法人和其他组织的合法权益造成特别严重的损害。 + +- **第四级 强制保护级**: + +(公安部门备案,要求半年一次)此类信息系统受到破坏后,会对国家安全造成严重损害,对社会秩序、公共利益造成特别严重损害。 + +- **第五级 专控保护级**: + +(公安部门备案,依据特殊安全需求进行)此类信息系统受到破坏后会对国家安全造成特别严重损害。 + +#### 怎么做等级保护? + +- **等级保护通常需要5个步骤**: + +1. 定级(企业自主定级-专家评审-主管部门审核-公安机关审核) +2. 备案(企业提交备案材料-公安机关审核-发放备案证明) +3. 测评(等级测评-三级每年测评一次) +4. 建设整改(安全建设-安全整改) +5. 监督检查(公安机关每年监督检查) + +- **企业自己如何做等级保护**? + +1. 在定级备案的步骤,一级不需要备案仅需企业自主定级。二级、三级是大部分普通企业的信息系统定级。四级、五级普通企业不会涉及,通常是与国家相关(如等保四级-涉及民生的,如铁路、能源、电力等)的重要系统。根据地区不同备案文件修改递交通常需要1个月左右的时间。 +2. 定级备案后,寻找本地区测评机构进行等级测评。 +3. 根据测评评分(GBT22239-2019信息安全技术网络安全等级保护基本要求。具体分数需要测评后才能给出)对信息系统(APP)进行安全整改,如果企业没有专业的安全团队,需要寻找安全公司进行不同项目的整改。等级保护2.0三级有211项内容,通常企业需要根据自身情况采购安全产品完成整改。 +4. 进行安全建设整改后,通过测评。当地公安机关会进行监督检查包含定级备案测评、测评后抽查。 + +整个流程企业自行做等级保护,顺利的话3-4个月完成,如果不熟悉需要半年甚至更久。 + +#### 等保三的基本要求? + +说说等级保护三级的技术要求,主要包含五个部门 + +- **物理安全** + +保证物理的安全,比如物理位置,机房的访问安全;涉及访问控制,防火防盗防雷防电磁,保备用电等 + +- **网络安全** + +保证网络层面安全,比如访问控制,安全审计,入侵防范,恶意代码防范,设备防范等。 + +- **主机安全** + +比如,身份鉴别,访问控制,安全审计,剩余信息保护(比如退出时清理信息),安全审计,入侵防范等。 + +- **应用安全** + +比如,数据完整性,数据保密性(加密),数据备份和回复。 + +### 16.6 ISO27001 + +#### 什么是ISO27001? + +信息安全管理体系标准(ISO27001)可有效保护信息资源,保护信息化进程健康、有序、可持续发展。ISO27001是信息安全领域的管理体系标准,类似于质量管理体系认证的 ISO9000标准。当您的组织通过了ISO27001的认证,就相当于通过ISO9000的质量认证一般,表示您的组织信息安全管理已建立了一套科学有效的管理体系作为保障。 + +#### ISO27001认证流程? + +第一阶段:现状调研 + +从日常运维、管理机制、系统配置等方面对贵公司信息安全管理安全现状进行调研,通过培训使贵公司相关人员全面了解信息安全管理的基本知识。 + +第二阶段:风险评估 + +对贵公司信息资产进行资产价值、威胁因素、脆弱性分析,从而评估贵公司信息安全风险,选择适当的措施、方法实现管理风险的目的。 + +第三阶段:管理策划 + +根据贵公司对信息安全风险的策略,制定相应信息安全整体规划、管理规划、技术规划等,形成完整的信息安全管理系统。 + +第四阶段:体系实施 + +ISMS建立起来(体系文件正式发布实施)之后,要通过一定时间的试运行来检验其有效性和稳定性。 + +第五阶段:认证审核 + +经过一定时间运行,ISMS达到一个稳定的状态,各项文档和记录已经建立完备,此时,可以提请进行认证。 \ No newline at end of file diff --git a/src/interview/README.md b/src/interview/README.md index 4e11471..76019b6 100644 --- a/src/interview/README.md +++ b/src/interview/README.md @@ -1,6 +1,7 @@ --- title: 面试 icon: lightbulb +index: true --- \ No newline at end of file