整理了80道高频面试八股文(下篇)

丶龙八夷
发布于 2023-11-15 15:09
浏览
0收藏

COUNT (*) 和COUNT (列名)的区别

阿里强制规范不要使用 count(列名) 或 count(常量) 来替代 count(*) 。

count(*) 是SQL92定义的标准统计行数的语法,和数据库无关,和NULL、非NULL无关。count(列名) 不会统计为NULL值的行。

Varchar的最大长度,varchar255与varchar256区别

最长65535字节,255使用一字节记录字节数,256使用两字节记录字节数。

MVCC原理,解决的是什么

MVCC是基于快照的多版本并发控制。原理是生成一个数据请求时间点的一致性数据快照(snapshot),并用这个快照来提供一定级别(数据级或事物级)的一致性读取。

简单来说,innoDB中,有三个隐藏字段:

  • 事务ID:DB_TRX_ID
  • 回滚ID(或删除指针):DB_ROLL_PTR
  • 行ID:DB_ROW_ID

一个非常重要的思想:

  • 只能查找创建时间小于等于当前事务ID的行(已提交的)
  • 只能查找删除时间大于等于当前事务ID的行(或未删除)

国产数据库有哪些

支持SQL

性能

支持分布式

高可用性

适用场景

其他

Tdengine

类SQL查询语言来插入或查询数据,采用标准SQL语法的子集

创新的存储引擎设计,无论是数据写入还是查询,TDengine 的性能比通用数据库快10倍以上,远超其他时序数据库

时序数据与元数据处理全部采用分布式技术,实现计算和存储分离,具备水平扩展能力,存储和计算资源可动态扩容或缩容

支持多副本,采用了 RAFT 一致性协议,保证系统的高可用。另外通过 WAL 来保证数据存储的高可靠

金融行业:交易记录、存取记录、ATM、POS机监测;互联网:服务器/应用监测、用户访问日志、广告点击日

支持消息队列,可指定各种过滤条件,应用可以仅仅订阅满足条件的数据;支持流式计算和缓存

达梦

支持

采用优化器和独立的虚拟机执行器,通过批量数据向量化执行和 MVCC 多版本并发控制等技术提升性能,实现双路 CPU 单节点 TPMC 达到百万级

支持大规模并行处理集群,用于解决海量数据存储和处理,提供高端数据仓库解决方案

可配置数据守护集群,适用于高可靠环境场景

适用于密集交易型场景的数据库集群

支持图数据库;行列融合存储技术,支持混合负载,一套系统上同时支撑 OLAP 和 OLTP 业务场景

OceanBase

支持

分布式查询能力性能,而且具备线性可扩展

支持,OceanBase Cloud可建立在公有云基础设施上,弹性扩展、高性能

弹性膨胀,负载均衡。金融级高可用,“三地五中心”城市级故障自动无损容灾新标准

电商、证券行业有广泛应用(招商证券、中华财险、赣服通等)

支持多租户,有一体机产品,配置灵活

GaussDB

高度兼容SQL,但没有text类型,有clob类型替代

采用计算与存储分离,日志即数据架构,RDMA网络,性能达到开源数据库的7倍

支持分布式存储,容量高达128TB

支持同城跨机房、异地、异地多活高可用,支持分布式强一致

泛互联网日志数据存储

基于KV接口,100%兼容Redis

TiDB

高度兼容 MySQL 协议和生态,应用无需或者修改少量代码

通过简单地增加新节点即可实现 TiDB 的水平扩展,按需扩展吞吐或存储,轻松应对高并发、海量数据场景

支持亚马逊 AWS 和 Google Cloud 云上托管

计算与存储分离的多副本存储,保证系统持续高可用

金融级高可用

支持实时 OLAP,支持分布式事务

简述一下NIO

NIO(Non-Blocking I/O,也被称为 New I/O),引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式。

它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

  • BIO(Blocking I/O):同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
  • NIO(Non-blocking I/O / New I/O):NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio 包,提供了Channel、Selector、Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持​​面向缓冲的​​​,​​基于通道的 I/O 操作方法​​。 NIO 提供了与传统 BIO 模型中的 Socket 和ServerSocket 想对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。是一种事件驱动的I/O模型。
  • AIO(Asynchronous I/O):AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 ​​NIO 的 IO 行为还是同步的​​。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

引用一个举例,中午去吃饭:

  • 自选餐线,我们点餐的时候都得在队伍里排队等待,必须等待前面的同学打好菜才到我们,这就是同步阻塞模型BIO。
  • 麻辣烫餐线,会给我们发个叫号器,我们拿到叫号器后不需排队原地等待,我们可以找个地方去做其他事情,等麻辣烫准备好,我们收到呼叫之后,自行取餐,这就是同步非阻塞。
  • 包厢模式,我们只要点好菜,坐在包厢可以自己玩,等到饭做好,服务员亲自送,无需自己取,这就是异步非阻塞I/O模型AIO。

开发中如何做异常处理,异常分类

在springboot项目中,通常会自定义业务类异常,抛出后统一由​​@RestControllerAdvice​​处理并返回给前端。

Java中的错误类型主要分为两类:

  • Error 是编译和系统错误,不允许被捕获
  • Exception 是java方法抛出的异常,具体分为RuntimeException运行时异常和非运行时异常

泛型方法,泛型类的字母与泛型方法的字母要一致吗

泛型的好处是在编译的时候检查类型安全。泛型方法后面的字母,要和泛型类中的字母一致,如果不一致,全部用“?”代替,不能有其它字母代替。如果不一致,编译器就会报错,例如idea会报“无法解析符号”。

泛型数组

泛型数组不能初始化,因为数组在new时不能确定类型,无法申请内存空间。

例如:​​T[] t = new T[];​​ 是错误的。

此外,静态方法不能使用类定义的泛型,原因是静态成员是和类相关的,在类加载时,对象还没有创建所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化。

有哪些内部类

  • HashMap内部的Node、TreeNode
  • LinkedHashMap内部的Entry
  • LinkedBlockingQueue内部的Node

StringBuffer和StringBuilder区别

  • StringBuffer线程安全,同步方法性能低。toString会直接使用缓存区的​​toStringCache​​ 值来构造一个字符串。
  • StringBuilder线程不安全,性能高。toString每次都需要复制一次字符数组,再构造一个字符串。

String为什么是不可变的

内部是​​private final char[]​​,私有且被final修饰。

数字在java中如何存储,原码、补码、反码

Java中,int是4个字节,取值范围是 ​​-2^31 ~ 2^31-1​​。

对于正数来说,原码,补码,反码都一样,例如25,都是 ​​00011001​​。再看-25:

原码:10011001 (最高位设置为1)
反码:11100110 (在原码的基础上,最高位不变,其余位取反)
补码:11100111 (在反码的基础上加1)

有哪些集合,List和Set有什么区别

常用集合:ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap、LinkedHashMap、Queue、PriorityQueue、Stack等

List和Set的区别:

  • List是有序的,Set无序
  • List可以存储相同元素,Set不能存重复元素。
  • List常用ArrayList、LinkedList、Vector,Set常用HashSet、TreeSet、LinkedHashSet
  • List支持添加、删除和修改,Set不支持修改,只能添加和删除元素

线程安全的集合有哪些

ConcurrentHashMap、ConcurrentSkipListSet、ConcurrentSkipListMap 等

ArrayList的默认长度

如果创建时不指定长度,那么是0。

private static final Object[] EMPTY_ELEMENTDATA = {};

如果指定长度大于0,则为指定长度。最大长度是​​Integer.MAX_VALUE - 8​

在执行add方法时,会先进行扩容,这时候如果第一次执行的话,那么是空的,会把数组长度变成10

之后每次扩容大概增加0.5倍左右,原有容量右移一次。2的n次方右移0.5倍,其他不到0.5倍。比如8右移一位为4,0.5倍。7右移一位为3,不到0.5倍)

int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
   newCapacity = minCapacity;

如果扩容后的长度小于需要的长度,那么设置为需要的长度。

TreeSet使用过程中需要注意的点

TreeMap

底层是红黑树,红黑树默认的Node排序是根据key值进行比较。每次更新节点时,都可能导致红黑树的重排。

TreeMap是按照key进行排序,而LinkedHashMap可以按照插入顺序进行访问(除了HashMap的基础结构外,还维护了一个链表结构)

TreeSet

TreeSet是一个有序且没有重复元素的集合,通过TreeMap实现,底层还是红黑树。

  • 不能存储相同的元素
  • 可以将元素按照规则进行排序
  • TreeSet():按照自然排序
  • TreeSet(Comparator comparator):根据指定的比较器进行排序
  • TreeMap有的特点TreeSet都有,在它的基础上增加了保证元素唯一性的特点

Sync开头的集合

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

HashMap底层原理

1.7使用链表+数组,1.8使用链表+数组或红黑树。

ConcurrentHashMap原理

jdk 1.7

使用分段锁实现,Segment数组+链表,相当于将一个大的table分成很多小的段来加锁。每一个Segment元素存储的是HashEntry数组+链表。

Segment继承了ReentrantLock,使用重入锁tryLock()方法,获取锁成功则可以插入,失败会自旋继续尝试,直到超过指定次数被挂起。

jdk 1.8

抛弃Segment的概念,对链表的头结点进行加锁,效率比1.7高。使用Node数组+链表+红黑树实现,并发控制使用synchronized的CAS操作。

阻塞队列底层实现原理

实现BlockingQueue接口的队列是阻塞队列,它们是线程安全的。

阻塞队列最重要的特点就是阻塞,它能使生产者和消费者两端的能力得以平衡,当有一方的速度过快时,阻塞队列会把速度降下来。最重要的两个方法:

  • take:获得并移除队列的头结点,在队列中有数据的时候是可以正常移除的,无数据则阻塞直到队列里有数据,一旦有数据了则解除阻塞状态
  • put:插入元素时如果队列没有满可以正常插入,如果队列满了则阻塞,直到队列里有空闲空间,就会消除阻塞状态

阻塞队列分为有界和无界,例如无界的​​LinkedBlcokingQueue​​​的上限是​​Integer.MAX_VALUE​​。

常用方法:

  • 抛出异常:add、remove、element(返回头结点但不删除)
  • 返回结果但是不抛出异常:offer、poll、peek
  • 阻塞:take、put

常见的阻塞队列:

  • ArrayBlockingQueue:底层是数组(通过双指针循环使用),长度初始化后固定不能修改,由ReentrantLock 保证同步。内部有两个Condition条件对象,notEmpty表示如果线程获取元素时队列为空就会在此等待,插入元素后被唤醒;notFull表示插入元素时如果队列为满就会在此等待,知道其他线程取出元素才会被唤醒。
  • LinkedBlockingQueue:底层是链表,头结点head不存元素,next才是第一个元素。使用两把锁takeLock和putLock,保证插入和删除不互斥,可以同时进行。可以指定长度也可以不指定。
  • SynchronousQueue:必须等到队列中的元素被消费后,才能继续向其中添加新的元素,因此它也被称为无缓冲的等待队列。分为公平模式和非公平模式。生产者线程在执行​​put​​​方法后就被阻塞,直到消费者线程执行​​take​​方法对队列中的元素进行了消费,生产者线程才被唤醒,继续向下执行。
  • PriorityBlockingQueue:支持优先级的无界阻塞队列,可以按照自定义的顺序排序(在构造器中传入Comparator对象),底层使用二叉堆实现。
  • DelayQueue:无界阻塞队列,只有实现了​​Delayed​​​接口的对象才能放进队列。实现了​​Delayed​​​接口的类必须要实现两个方法:​​getDelay​​​方法用于计算对象的剩余延迟时间,判断对象是否到期,计算方法一般使用过期时间减当前时间。​​compareTo​​方法用于延迟队列的内部排序比较,这里使用当前对象的延迟时间减去被比较对象的延迟时间。DelayQueue的底层是PriorityBlockingQueue,基于二叉堆。

线程池底层原理

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

如何做JVM调优

  • FullGC一天超过一次肯定就不正常了,发现FullGC频繁的时候优先调查内存泄漏问题
  • 内存泄漏解决后,jvm可以调优的空间就比较少了,作为学习还可以,否则不要投入太多的时间
  • 如果发现CPU持续偏高,排除代码问题后可以找运维咨询下,有可能是由于服务器问题导致的,进行服务器迁移后就正常了
  • 数据查询的时候也是算作服务器的入口流量的,如果访问业务没有这么大量,而且没有攻击的问题的话可以往数据库方面调查
  • 有必要时常关注服务器的GC日志,可以及早发现问题
  • 一般在生产环境出现问题时调优,也可以在测试环境使用 JMeter 类型工具先进行压测,然后针对问题调优。

常用JVM调优参数

指定堆内存,如果我们要为 JVM 分配最小 2 GB 和最大 5 GB 的堆内存大小,参数应该这样来写:

-Xms2g -Xmx5g

指定新生代内存,为 新生代分配 最小 256m 的内存,最大 1024m 的内存我们的参数应该这样来写:

-XX:NewSize=256m
-XX:MaxNewSize=1024m

如果要为 新生代分配 256m 的内存(NewSize 与 MaxNewSize 设为一致),我们的参数应该这样来写:

-Xmn256m

设置老年代和新生代内存比值,下面的参数就是设置老年代与新生代内存的比值为 1。也就是说老年代和新生代所占比值为 1:1,新生代占整个堆栈的 1/2:

-XX:NewRatio=1

1.7以前指定永久代大小:

-XX:PermSize=N #方法区 (永久代) 初始大小 
-XX:MaxPermSize=N #方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

1.8指定元空间大小:

-XX:MetaspaceSize=N # 可以理解为高水位线,如果超过这个值就会触发fullGC,并可以动态调整 
-XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小

Metaspace 的初始容量并不是 -XX:MetaspaceSize 设置,无论 -XX:MetaspaceSize 配置什么值,对于 64 位 JVM 来说,Metaspace 的初始容量都是 21807104(约 20.8m)

指定垃圾收集器:

-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseParNewGC
-XX:+UseG1GC

GC日志记录,生产环境上,或者其他要测试 GC 问题的环境上,一定会配置上打印 GC 日志的参数,便于分析 GC 相关的问题。

# 必选# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M

处理OOM,内存不足错误是非常常见的,会导致应用程序崩溃。这是一个非常关键的场景,很难通过复制来解决这个问题。这就是为什么 JVM 提供了一些参数,这些参数将堆内存转储到一个物理文件中,以后可以用来查找泄漏。

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;<cmd args >";
-XX:+UseGCOverheadLimit

这里有几点需要注意:

  • HeapDumpOnOutOfMemoryError 指示 JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中。
  • HeapDumpPath 表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个 标记,则当前进程的进程 id 将附加到文件名中,并使用.hprof 格式
  • OnOutOfMemoryError 用于发出紧急命令,以便在内存不足的情况下执行; 应该在 cmd args 空间中使用适当的命令。例如,如果我们想在内存不足时重启服务器,我们可以设置参数: -XX:OnOutOfMemoryError="shutdown -r";
  • UseGCOverheadLimit 是一种策略,它限制在抛出 OutOfMemory 错误之前在 GC 中花费的 VM 时间的比例

对象晋升到老年代的年龄阈值(默认不全是15,CMS就是6),可以通过参数来设置:

-XX:MaxTenuringThreshold

Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 ​​MaxTenuringThreshold​​ 中更小的一个值,作为新的晋升年龄阈值。

设定survivor区的目标使用率,默认50,即survivor区对象目标使用率为50%

-XX:TargetSurvivorRatio

如果需要将 1MB 以上的对象直接在年老代分配,设置:

-XX:PetenureSizeThreshold=1000000

对象特殊分配过程:

其他指令:

  • -server : 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM
  • -XX:+UseStringDeduplication : Java 8u20 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 char [] 数组来优化堆内存。
  • -XX:+UseLWPSynchronization: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。
  • -XX:LargePageSizeInBytes: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。
  • -XX:MaxHeapFreeRatio : 设置 GC 后, 堆空闲的最大百分比,以避免收缩。
  • -XX:SurvivorRatio : eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6 设置每个 survivor 和 eden 之间的比例为 1:6。
  • -XX:+UseLargePages : 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。
  • -XX:+UseStringCache : 启用 String 池中可用的常用分配字符串的缓存。
  • -XX:+UseCompressedStrings : 对 String 对象使用 byte [] 类型,该类型可以用纯 ASCII 格式表示。
  • -XX:+OptimizeStringConcat : 它尽可能优化字符串串联操作。

Java8默认收集器是什么?

与服务器CPU核心数有关:

  • 单核CPU:使用 Serial(新生代) + Serial old(老年代)
  • 多核(一般是4核以上):使用 Parallel Scavenge(新生代,标记复制) + Parallel old(老年代,标记整理)

可以通过命令查看:

java -XX:+PrintCommandLineFlags -version

-XX:InitialHeapSize=132397696 
-XX:MaxHeapSize=2118363136 
-XX:+PrintCommandLineFlags 
-XX:+UseCompressedClassPointers 
-XX:+UseCompressedOops 
-XX:-UseLargePagesIndividualAllocation 
-XX:+UseParallelGC  # 使用的GC版本
java version "1.8.0_261"
Java(TM) SE Runtime Environment (build 1.8.0_261-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)

如何确定一个对象是垃圾

  • 引用计数法:无法解决循环引用问题,每次引用及清除需要更新计数器比较麻烦,会带来性能损耗
  • 可达性分析法:由GC Root开始向下标记,标记存活对象。搜索的路径称为引用连,对象没有被引用链连接说明不在存活

垃圾收集算法

标记-清除算法

效率低,标记和清除两个过程效率都不高

容易产生空间碎片

复制算法

可用内存变小,缩小为原来的一半

不适合老年代,如果存活的对象很多,复制性能较差

标记-整理算法

所有存活对象向一端移动,清理边界以外的内存。同样多了整理这一步,效率比较低。

分代收集

根据各个年代的特点选择合适的垃圾收集算法。

新生代,每次收集都会有大量对象死去,使用复制。

老年代,对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,使用标记清除或标记整理。

垃圾收集器

垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现,要根据具体应用场景选择适合自己的垃圾收集器。

JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)

JDK 9 ~ JDK20: G1

Serial 收集器

Serial(串行)收集器,单线程。 单线程的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( Stop The World ),直到它收集结束。

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

新生代采用标记-复制算法,老年代采用标记-整理算法。特点是简单高效,没有线程交互的开销。

ParNew 收集器

Serial的多线程版本,除了使用多线程外,其余行为和Serial完全一样。

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。

  • Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)
  • CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)
吞吐量 =  CPU 中用于运行用户代码的时间 /  CPU 总消耗时间的比值

Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

是jdk1.8默认的收集器, Parallel Scavenge + Parallel Old。

Serial Old 收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

Parallel Old收集器运行示意图

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 标记-清除算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  • 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感
  • 无法处理浮动垃圾
  • 它使用的“标记-清除”算法会导致收集结束时会有大量空间碎片产生

G1 收集器

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

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

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

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

整理了80道高频面试八股文(下篇)-鸿蒙开发者社区

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。

ZGC 收集器

与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。在 ZGC 中出现 STWorld 情况更少!

Java11 的时候 ,ZGC 还在试验阶段。经过多个版本的迭代,不断的完善和修复问题,ZGC 在 Java 15 已经可以正式使用了。不过,默认的垃圾回收器依然是 G1。可以通过下面的参数启动 ZGC:

$ java -XX:+UseZGC className

对象创建过程

  • 类加载检查:jvm在读取一条new指令的时候,首先检测能否在常量池中定位到这个类的符号引用,并检测这个符号引用代表的类是否被加载、解析、初始化。如果没有会先执行类的加载
  • 内存分配:对象所需的内存大小在类加载完成后就可以确定了。主要采取两种方式,指针碰撞和空闲列表,选择哪一种方式取决于堆内存是否规整(是否规整取决于收集器使用的是标记-清除还是标记-整理)
  • 初始化默认值:将内存空间进行初始化(给定默认值,不包括对象头),这是为了保证对象在java代码中可以在不赋初始值的情况下使用,程序可以使用这些类型的默认值
  • 设置对象头:markword,Klass point,填充对齐,hashcode,对象所处的年龄段等
  • 执行初始化init方法:执行开发人员编写的对象初始化方法

双亲委派原理

首先说一下类加载器:

java虚拟机设计团队有意将类加载阶段中“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到java虚拟机外部去实现,以便于让应用程序自己决定如何去获取所需的类。实现这个动作的代码称作为“类加载器(Class Loader)”

三种类加载器:

  • 启动类加载器(Bootstrap):加载存放在​​JAVA_HOME\lib​​​目录,或者被​​-Xbootclasspath​​参数指定路径存放的,并且是java虚拟机能识别的类库加载到虚拟机内存中(主要是加载java的核心类库,即加载lib目录下的所有class)
  • 扩展类加载器(Extension):加载​​JAVA_HOME\lib\ext​​目录中,或者被java.ext.dirs系统变量所指定的路径中所有类库
  • 应用类加载器(Application):这个类的加载器是由​​sun.misc.Launcher$AppClassLoader​​​来实现,因为该加载器是ClassLoader类中的​​getSystemClassLoader()​​方法的返回值,所以一般也称为该加载器为系统类加载器。该加载器主要是加载用户类路径上所有的类库,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序的默认加载器

如果一个类加载器收到了类加载请求,它首先不会自动去尝试加载这个类,而是把这个类委托给父类加载器去完成,每一层依次这样,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成该加载请求(找不到所需的类)时,这个时候子加载器才会尝试自己去加载,这个过程就是双亲委派机制。

双亲委派机制的优点:

  • 避免了类的重复加载
  • 保护了程序的安全性,防止核心的API被修改

常用的数据结构有哪些

数组、链表、栈、队列、树、散列表(哈希表)、堆、图

常用的排序算法

冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序

不用加减号实现加减

可以使用位运算符计算。测试20+15=35

20: 0001 0100
15: 0000 1111
35: 0010 0011

不考虑进位的情况,0+0=0,1+0=1,1+1=0;位值相同为0,不同为1,所以可以等价于异或运算

进位则发生在位值都为1的时候,位值同为0、一个为0一个为1都不会触发进位,这和与运算不谋而合,所以可以等价于与运算后左移一位

所以可以使用二进制实现a+b操作:

1. a = a ^ b
2. b = (a & b) << 1
3. 重复1、2步骤直到进位值为0

减法的话可以使用a加上b的补码(~b+1)。

交换两个变量不用临时变量

将a+b的值赋给a,然后用a-b给b赋值,a-b =+ (a+b)-b 最终b的值就为a,然后将a-b的值给给a,

a-b == (a+b) - a 结果就是b。从而实现交换:

void change()
{
 int a = 1;
 int b = 2;
 a = a + b;
 b = a - b;
 a = a - b;
}

或者使用异或:a与b异或结果0011给给a,然后将更新后的a与b进行异或 0011 与 b0010进行异或得到0001

将a0011与更新后的b0001进行异或得到0010,实现变量交换

void change()
{
 int a = 1;// 0001
 int b = 2;// 0010
 a = a ^ b;// 0011
 b = a ^ b;// 0001
 a = a ^ b;// 0010
}

面向对象设计原则

  • 封装:封装是将过程和数据包围起来,数据只能通过定义的接口访问。面向对象计算从一个基本概念开始,即现实世界可以表示为一系列完全自治的、封装的对象,这些对象通过受保护的接口访问其他对象。
  • 继承:继承是一种层次模型,它连接类,允许并鼓励类的重用,提供了一种明确表达共性的方法。对象的新类可以从现有类派生,这个过程称为类继承。新类继承原类的属性。新类被称为原类的派生类(子类),原类被称为新类的基类(父类)。
  • 多态:指一个类实例的相同方法在不同情形下有不同的表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。最常见的多态就是将子类传入父类参数中,当运行时调用父类方法时,通过传入的子类决定具体的内部结构或行为。

Linux通过文件搜索关键字

grep、find、sed、awk等

Linux版本,如何查看防火墙状态,重启服务

systemctl status firewalld
start/restart

如何用docker启动软件,查看docker中启动的服务

docker run -d ...
docker ps -a

那么,这次的分享就到这里,我是Hydra,我们下期再见。


文章转载自公众号:码农参上

分类
已于2023-11-15 15:09:06修改
收藏
回复
举报
回复
    相关推荐