java什么时候内存泄漏


1.静态集合类持有对象引用(最常见)

静态集合(如static List/Map)的生命周期与 JVM 进程一致,如果向静态集合中添加对象后不及时移除,这些对象会一直被持有,永远无法被 GC 回收。

2.未关闭的资源(IO / 数据库连接 / 网络连接)

Java 中的 IO 流、数据库连接(Connection)、Socket 连接等资源,若未显式关闭,不仅会占用文件句柄 / 连接资源,其对应的对象也可能无法被 GC 回收,导致内存泄漏。

3.ThreadLocal 使用后未清理

ThreadLocal 用于线程内共享数据,但如果线程复用(如线程池),且 ThreadLocal 未调用remove(),线程的 ThreadLocalMap 会一直持有对象引用,导致内存泄漏。

怎么评估FAQ召回的效果


基于ANN的FAISS进行毫秒级别召回,然后基于BERT模型精排。

1.评估指标平均精确率(mAP)

只处理 0/1 二元的情况,只考虑topK,不关心topK内部顺序。

upload successful

upload successful

分母:所有相关结果的sum

分子:sum_N N内查询相关个数/N

2.归一化折损累积增益(normalized discounted cumulative gain,nDCG)

能处理 0/1/2/3 多元的情况,关心topK内部顺序。

高关联度的结果排在排名靠前的位置是最合理的,因此:

高关联度的结果对得分的贡献大。

排名越靠前的结果,权重越大。

模型给出的排序结果,是否把 “更相关的东西” 排在了更前面。

reli = 0/1

3.AUC 类似AP

衡量预测分数的准确性。ROC曲线是通过将真阳性率(TPR)与假阳性率(FPR)绘制在坐标系中形成的曲线。

4. MRR(Mean Reciprocal Rank)

衡量 “第一个正确答案出现的位置”。

公式:1 /rank_of_first_relevant

对 “首命中” 非常敏感。

适用:问答系统、搜索中 “第一个结果准不准” 很重要的场景。

怎么减少模型幻觉?

1.加上“不允许猜测”的 Prompt 之后,错误率会下降 20%–40%。
2.正确的 Schema :{
“origin”: {
“type”: “string”,
“description”: “用户明确说出的城市,不能猜。如果缺失请继续询问。”
}
}
3.调用接口的时候checkParams返回追问,避免没有传参数乱猜

kafka怎么扩容能避免乱序

1. 增加服务器,不调整主题分区数

仅增加集群的承载能力,不改变现有主题的分区分布,是风险最低的扩容方式。

迁移前暂停该主题的生产 / 消费(如果业务允许),或确保生产者使用acks=all、消费者开启enable.auto.commit=false

迁移过程中,Kafka 会先同步副本数据,再切换 leader,单分区内的消息顺序不受影响

2. 扩容主题分区数(增加分区数量,核心风险点)

3. 扩线程数?

如果线程数 < partition数可以,否则无效。

select、poll、epoll、io_uring

select

通过位图(fd_set)来管理要监控的 fd,最大长度1024。

每次调用 select 都需要把 fd 集合从用户态拷贝到内核态,内核还需要遍历所有 fd 检查状态,fd 越多效率越低。

select 只返回就绪的 fd 数量,用户态需要逐个遍历所有监控的 fd 来确认哪些就绪,浪费资源。

poll

poll 解决了 select 的 fd 数量限制问题,它使用结构体数组(struct pollfd) 来管理 fd,每个结构体包含 fd 和要监控的事件(POLLIN/POLLOUT 等),内核遍历数组检查 fd 状态,就绪后会修改结构体的 revents 字段标记就绪事件。

依然存在用户态 - 内核态拷贝和内核遍历所有 fd 的问题,fd 数量大时效率依然低下;用户态仍需遍历所有 pollfd 结构体判断哪些就绪。

epoll

事件驱动:内核维护一个 epoll 实例(通过 epoll_create 创建),用户态通过 epoll_ctl 向实例中添加 / 删除 / 修改要监控的 fd 和事件,内核为每个 fd 注册回调函数,fd 就绪时主动触发事件,无需遍历所有 fd。

内存映射:通过 mmap 实现用户态和内核态的内存共享,避免 fd 集合的频繁拷贝。

  • 两种触发模式:

LT(水平触发,默认):只要 fd 有数据可读 / 可写,就会持续触发事件,直到数据被处理完。

ET(边缘触发):仅在 fd 状态从不可就绪变为就绪时触发一次,需要一次性读取 / 写入所有数据,效率更高,但编程更复杂(需处理 EAGAIN 错误)。

  • 关键特点 & 优点

无 fd 数量限制:仅受限于系统最大文件描述符数(远大于 1024)。

高效性:fd 就绪时内核主动通知,无需遍历所有 fd,百万级 fd 下仍能保持高性能。

低拷贝开销:内存映射减少用户态 - 内核态数据拷贝。

io_uring

epoll 还是有一个问题。它只能告诉你网络是否可以读写,你还是需要自己写代码来读写网络。由于每次读写网络都会调用内核的函数,这样会造成大量的用户态和内核态切换,浪费很多计算资源。那有没有办法解决这个问题呢?

在2018年Linux内核新增了一个功能叫作 io_uring,它就解决了用户态切换过多的问题。

它解决问题的思路很简单。你在写程序的时候准备一个队列,里面记录了所有你想要做的读写操作,同时也包含了你预先分配的读写内存。

接着你将这个队列一股脑交给内核。内核会先做 epoll 的事情,检查哪些网络链接可以开始读写。然后内核会多做一步,帮你处理网络数据。

如果你的操作是写网络的话,会把你内存的数据写出去。如果你的操作是读操作的话,会把数据读到你预先分配的内存。内核操作完之后会把这些操作的状态记录在另一个列表里,返回给你的用户态进程。

双队列设计:

提交队列(SQ):用户态向内核提交 IO 请求(读 / 写 /accept 等)的队列。

完成队列(CQ):内核完成 IO 后,将结果写入该队列,用户态读取结果即可。

upload successful

RabbitMQ vs RocketMq


Kafka 单机吞吐量可达百万级 TPS,适用场景包括日志采集、实时计算和大数据管道。例如,社交平台的实时动态流或物联网设备数据采集。Kafka 不适合需要复杂路由或事务一致性的场景。

RocketMQ 单机吞吐量可达十万级 TPS

RabbitMQ 的单机吞吐通常在每秒数千条

upload successful

RocketMQ(金融级)

RocketMQ 的消费者默认采用了一种模拟推(Push)的、基于长轮询的拉(Pull)模型。

upload successful

  • 事务消息

分布式事务消息:这是 RocketMQ 的王牌功能。它通过 “半消息 (Half Message)” 和 “消息回查” 机制,实现了类似 XA 的分布式事务最终一致性,是解决跨系统事务的一种方案。

整个流程分为两个阶段:

第一阶段 - 发送半消息【生产者 -> send msg -> broker】:生产者发送一个对消费者不可见的 “半消息” 到 Broker。Broker 持久化该消息,但不会将其投递到目标 Topic。

第二阶段 - 提交或回滚【生产者 -> send commit -> broker】:生产者执行本地事务,并根据执行结果(成功/失败)向 Broker 发送一个 Commit 或 Rollback 指令。Broker 根据指令,将半消息变为正常消息投递给消费者,或直接将其丢弃。

【broker -> 回查 -> 生产者】为了处理生产者宕机或网络异常导致第二阶段指令无法发送的 “悬而未决” 状态,RocketMQ 引入了事务状态回查机制,由 Broker 主动向生产者查询本地事务的最终状态。

  • RocketMQ 事务消息将消息表 “外置” 到了消息中间件中,简化了业务方的开发复杂度,避免了重复造轮子,性能和可靠性更高。

与 2PC 对比:它是弱化的 2PC,不保证强一致性(因为存在回查延迟),但通过异步和重试机制,实现了高可用和最终一致性,更适合互联网场景。

与 TCC 对比:事务消息专注于解决异步消息场景的一致性;TCC 则适用于需要明确预留、确认/取消多个服务资源的同步或异步场景,粒度更细,但实现也更复杂。

  • 消息顺序

RocketMQ 通过 “分区有序” 模型来保证消息的顺序性。其核心是 “同一组顺序消息发送到同一个队列,且由一个消费者线程串行处理”。

  • 怎么不丢消息?

有死信队列,有主备

RabbitMQ

消息的强一致性保证机制

发送方

批量发送 N 条消息后一次性确认,发布确认(Confirm)模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息。

upload successful

Broker 侧

确保消息落地不丢失

队列持久化:声明队列时指定 durable=true,队列元数据会持久化到磁盘,Broker 重启后队列不丢失;
消息持久化:发送消息时指定 deliveryMode=2(持久化),消息会被写入磁盘(先写内存缓存,再异步刷盘),Broker 重启后消息不丢失;
镜像队列(Mirror Queue):开启队列镜像,将队列副本同步到集群多个节点,主节点故障时从节点接管,避免单点丢失;

消费者

确保消息被正确消费

手动确认(Ack):关闭自动确认(autoAck=false),消费者处理完业务逻辑后,主动调用 channel.basicAck() 确认消息;若处理失败,调用 basicNack()/basicReject() 让消息重新入队或进入死信队列;

幂等消费:通过消息唯一 ID(如 correlationId)+ 数据库唯一索引 / 分布式锁,避免消息重复消费导致的业务异常;

限流机制:通过 channel.basicQos() 设置每次预取的消息数(如 basicQos(10)),避免消费者过载导致消息堆积 / 丢失。

其他特性

  • 惰性队列(Lazy Queue)

RabbitMQ从3.6.0版本开始引入了惰性队列(Lazy Queue)的概念。 惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。 当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了

  • 队列和消费者的基础关系:一对一 / 一对多绑定

队列是消费者的 “数据源”:消费者必须明确绑定到某个 / 某些队列(通过 basic.consume 命令),才能接收该队列的消息;未绑定队列的消费者无法获取任何消息。

一个队列可被多个消费者绑定(一对多):这是最常见的场景,用于消息的负载均衡。

一个消费者可绑定多个队列(多对一):通过多次执行绑定命令,消费者可同时监听多个队列的消息(但实际开发中更推荐用交换机路由,而非直接绑定多队列)

POJO VO DTO

upload successful

POJO

POJO 仅包含私有属性、公共的 getter 和 setter 方法,以及可能的构造方法和其他业务方法。

创建一个 POJO 类,通常包含以下步骤:

1.定义私有属性。

2.提供公共的 getter 和 setter 方法。

3.可选地提供构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class User {
private String name;
private int age;

// 构造方法
public User() {}

public User(String name, int age) {
this.name = name;
this.age = age;
}

// Getter 和 Setter 方法
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

数据传输对象(DTO)

DTO用于不同层之间的数据传输,尤其是Service层与Controller层的交互。

特点:

1.可能包含多个PO的组合或裁剪后的字段(例如从30个字段中选取10个传输)

2.支持序列化,常用于远程调用(如RPC、HTTP接口)

3.无业务逻辑


持久化对象(Entity)

与数据库表中的记录映射

BO(Business Object,业务对象)

Service层内部业务逻辑,封装复杂业务逻辑,可包含多个PO的组合。

VO(View Object,视图对象)

定义:VO是展示层(前端页面)直接使用的对象,仅包含前端需要展示的数据,通常以JSON形式返回

应用场景:Controller层将数据封装为VO后传递给前端,避免暴露敏感字段(如密码、内部状态)

go范型


指明需要编译的具体类型

比较凌乱的写法:

1
2
// 一个可以容纳所有int,uint以及浮点类型的泛型切片
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T

相对好维护的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Int interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32
}
type Float interface {
~float32 | ~float64
}

type Slice[T Int | Uint | Float] []T // 使用 '|' 将多个接口类型组合


var s Slice[int] // 正确

type MyInt int
var s2 Slice[MyInt] // MyInt底层类型是int,所以可以用于实例化,如果不加~就不行。因为泛型类型 Slice[T] 允许的是 int 作为类型实参,而不是 MyInt (虽然 MyInt 类型底层类型是 int ,但它依旧不是 int 类型)。

具体步骤

  • 1.编译期处理:类型检查与中间表示(IR)生成

类型验证:编译器首先会对泛型函数 / 类型(如 func FooT any)进行类型检查,确保类型参数(Type Parameter)符合类型约束(Constraint),比如验证 T 是否实现了约束中要求的方法。

生成通用 IR:编译器会为泛型定义生成一份 “通用” 的中间表示(Intermediate Representation),这份 IR 不绑定具体类型,仅记录类型参数的约束和逻辑。

实例化触发:当代码中首次使用具体类型实例化泛型(如 Fooint)时,编译器会触发实例化过程:
对于基本类型 / 简单类型(如 int、string、[]int):编译器会为该类型生成一份特化的代码(类似 C++ 模板特化),避免运行时开销;

对于复杂类型 / 不常用类型:编译器会生成一份 “共享代码”,通过 reflect 相关的底层机制(但非暴露给用户的 reflect 包)处理类型参数,减少编译后二进制体积。

  • 2.运行时处理:字典传递(Dictionary Passing)

这是 Go 泛型最核心的底层机制:编译器为每个泛型实例化生成一个类型字典(Type Dictionary),并将其作为隐式参数传递给泛型函数。

类型字典的内容:包含类型参数的元信息(如类型大小、对齐方式、哈希函数、比较函数等),以及约束中要求的方法指针(如果约束包含方法)。

字典传递的作用:
当泛型函数需要操作类型参数(如赋值、比较、调用方法)时,会通过类型字典获取该类型的具体行为,而非硬编码;

例如 func Min[T constraints.Ordered](a, b T) T 中,比较 a < b 的逻辑并非直接编译为 int 的比较或 string 的比较,而是通过类型字典中的比较函数指针来执行。

总结

既避免了 C++ 模板 “代码膨胀” 的问题,也解决了 Java 泛型 “类型擦除” 导致的运行时类型信息丢失问题,是 Go 团队针对 Go 语言特性的折中优化。

SingleFlight


Group

1
2
3
4
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}

Group 结构体由一个互斥锁和一个 map 组成,可以看到注释 map 是懒加载的,所以 Group 只要声明就可以使用,不用进行额外的初始化零值就可以直接使用。call 保存了当前调用对应的信息,map 的键就是我们调用Do方法传入的 key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type call struct {
wg sync.WaitGroup

// 函数的返回值,在 wg 返回前只会写入一次
val interface{}
err error

// 使用调用了 Forgot 方法
forgotten bool

// 统计调用次数以及返回的 channel
dups int
chans []chan<- Result
}

Do

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()

// 前面提到的懒加载
if g.m == nil {
g.m = make(map[string]*call)
}

// 会先去看 key 是否已经存在
if c, ok := g.m[key]; ok {
// 如果存在就会解锁
c.dups++
g.mu.Unlock()

// 然后等待 WaitGroup 执行完毕,只要一执行完,所有的 wait 都会被唤醒
c.wg.Wait()

// 这里区分 panic 错误和 runtime 的错误,避免出现死锁,后面可以看到为什么这么做
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}

// 如果我们没有找到这个 key 就 new call
c := new(call)

// 然后调用 waitgroup 这里只有第一次调用会 add 1,其他的都会调用 wait 阻塞掉
// 所以这要这次调用返回,所有阻塞的调用都会被唤醒
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()

// 然后我们调用 doCall 去执行
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}

docall

这个方法的实现有点意思,使用了两个 defer 巧妙的将 runtime 的错误和我们传入 function 的 panic 区别开来避免了由于传入的 function panic 导致的死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
normalReturn := false
recovered := false

// 第一个 defer 检查 runtime 错误
defer func() {

}()

// 使用一个匿名函数来执行
func() {
defer func() {
if !normalReturn {
// 如果 panic 了我们就 recover 掉,然后 new 一个 panic 的错误
// 后面在上层重新 panic
if r := recover(); r != nil {
c.err = newPanicError(r)
}
}
}()

c.val, c.err = fn()

// 如果 fn 没有 panic 就会执行到这一步,如果 panic 了就不会执行到这一步
// 所以可以通过这个变量来判断是否 panic 了
normalReturn = true
}()

// 如果 normalReturn 为 false 就表示,我们的 fn panic 了
// 如果执行到了这一步,也说明我们的 fn recover 住了,不是直接 runtime exit
if !normalReturn {
recovered = true
}
}

DoChan

Do chan 和 Do 类似,其实就是一个是同步等待,一个是异步返回,主要实现上就是,如果调用 DoChan 会给 call.chans 添加一个 channel 这样等第一次调用执行完毕之后就会循环向这些 channel 写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
ch := make(chan Result, 1)
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
c.chans = append(c.chans, ch)
g.mu.Unlock()
return ch
}
c := &call{chans: []chan<- Result{ch}}
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()

go g.doCall(c, key, fn)

return ch
}

forget

用于手动释放某个 key 下次调用就不会阻塞等待了

1
2
3
4
5
6
7
8
func (g *Group) Forget(key string) {
g.mu.Lock()
if c, ok := g.m[key]; ok {
c.forgotten = true
}
delete(g.m, key)
g.mu.Unlock()
}

单调栈

LC 84. 柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

upload successful

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
public int largestRectangleArea(int[] heights) {
List<Integer>left = new ArrayList(); // index
List<Integer>right = new ArrayList(); // index
for(int i=0; i<heights.length;i++) {
left.add(-1);
right.add(heights.length);
}
Stack<Integer>minStack = new Stack(); // <index>
// left side nearest index lower then i
for (int i = 0; i < heights.length; i++) {
if(minStack.size() <= 0) {
minStack.push(i);
continue;
}
while(!minStack.isEmpty() && heights[minStack.peek()]>=heights[i]){
minStack.pop();
}
if (!minStack.isEmpty()) {
left.set(i,minStack.peek());
}
minStack.push(i);
}
minStack = new Stack(); // <index>
for (int i = heights.length-1; i>=0; i--) {
if(minStack.size() <= 0) {
minStack.push(i);
continue;
}
while(!minStack.isEmpty() && heights[minStack.peek()]>=heights[i]){
minStack.pop();
}
if (!minStack.isEmpty()) {
right.set(i,minStack.peek());
}
minStack.push(i);
}
int maxArea = 0;
for (int i = 0; i < heights.length; i++) {
int newArea = (heights[i]*(right.get(i)-left.get(i)-1));
maxArea = newArea > maxArea ? newArea:maxArea;
}
return maxArea;
}
}
| 本站总访问量次 ,本文总阅读量