zero's Blog

持续迭代

堆外内存
  堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。
  可以使用ByteBuffer等类来操纵堆外内存了,使用ByteBuffer分配本地内存则非常简单,直接:

1
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);

  可以通过指定JVM参数来确定堆外内存大小限制:-XX:MaxDirectMemorySize=512m
  对于这种direct buffer内存不够的时候会抛出错误: java.lang.OutOfMemoryError: Direct buffer memory
  堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。

 

  使用ByteBuffer堆外内存时,必须要由我们自己释放;必须先释放堆内存中的对象(引用),才能释放堆外内存,但是我们又不能强制JVM释放堆内存。例如:ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。
  再者,假设堆内 ByteBuffer对象的引用升级到了老年代,导致这个引用会长期存在无法回收,这时堆外的内存将长期无法得到回收。

Read more »

先有一个需求,有一组Task,每个task有一个flow表示要处理的流量,要求分配在n个线程中,保证每个线程处理的流量数均衡。

思路是对每个线程依据已分配的task数打分,每次分配给分数最低的线程。打分算法如下:

$$Scoce_i=weight*\frac{I_i}{I_{total}}+(1-weight)*\frac{N_i}{N_{total}}$$

  • ${I_i}$表示 i 线程上已经分配的 flow和; ${I_{total}}$表示所有已经分配的 flow和;
  • ${N_i}$表示 i 线程上已经分配的task数; ${N_{total}}$表示所有已经分配的task数;
Read more »

生成毫秒数+3位自增:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static AtomicLong sequence = new AtomicLong();

public static String generSeq() {
for (; ; ) {
long oldSeq = sequence.get();
long curMS = System.currentTimeMillis() * 1000;

long newSeq = curMS > oldSeq ? curMS : oldSeq + 1;
if (sequence.compareAndSet(oldSeq, newSeq)) {
return String.valueOf(newSeq);
}
}
}

生成yyyyMMddHHmmssSSS+2位自增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private static AtomicLong lastSeq = new AtomicLong();
public static String generSeqStr() {
for (; ; ) {
long oldSeq = lastSeq.get();
String curDate = LocalDateTime.now().format(DATE_TIME_FORMATTER);
long seq = Long.valueOf(curDate) * 100;// *1000会越界

long newSeq = seq > oldSeq ? seq : oldSeq + 1;
if (lastSeq.compareAndSet(oldSeq, newSeq)) {
return String.valueOf(newSeq);
}
}
}

现在假定有这样一个需求,实现针对集群(比如 Hadoop 集群)版本为 V1 与 V2 的对应的执行程序,那么假定有如下项目:

1
2
3
4
5
Executor-Parent: 提供基础的 Maven 引用,可利用 Maven 一键打包所有的子模块/项目
Executor-Common: 提供基础的接口,已经有公有的实现等
Executor-Proxy: 执行不同版本程序的代理程序
Executor-V1: 版本为V1的执行程序
Executor-V2: 版本为V2的执行程序

这里为了更凸显 ClassLoader 的实现,不做 Executor-Parent 的实现,同时为了简便,也没有设置包名。

  1. Executor-Common
    在 Executor-Common 中提供一个接口,声明执行的具体方法:
    1
    2
    3
    public interface Executor {
    void execute(final String name);
    }
Read more »

[toc]
Java8之前集合之间互相转化,例如List -> Map需要写许多代码,但是Java8之后可以使用Lambda表达式简化代码。
示例:

1.List<T> ---> Map<K, V>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<String> list = Arrays.asList("zero");
Map<String, Integer> map = list.stream().collect(
Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String e) {
return e;
}
}, new Function<String, Integer>() {
@Override
public Integer apply(String e) {
return e.hashCode();
}
}));
System.out.println(map);

示例中,Collectors.toMap接收两个Function参数,第一个Function是生成key,第二个Function是生成value,两个Function<T,X>的第一个泛型参数类型是对应的list的类型,这里即是String。

使用Lambda表达式简化后:

1
2
3
List<String> list = Arrays.asList("zero");
Map<String, Integer> map = list.stream().collect(
Collectors.toMap(e -> e, e -> e.hashCode()));
Read more »

  JAVA反射机制是在运行状态中,对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
  简而言之,反射就是可以通过名称来得到对象(类,属性,方法)的技术。

一.获取类对应的Class对象

  1. getClass()方式: Object类中的方法,每个类都拥有此方法,如:
    1
    2
    String str = new String();
    Class cls = str.getClass();

2.类名.class方式,如:

1
Class cls = String.class;
  1. Class.forName()方式,如:
    1
    2
    3
    4
    5
    6
    try {
    Class cls = Class.forName("java.lang.String");
    } catch (ClassNotFoundException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
Read more »

起因

  运行在docker上的一个服务,在某个版本之后,占用的内存开始增长,直到docker分配的内存上限,但是并不会OOM。使用ps查看进程使用的内存和虚拟内存 ( Linux内存管理 )。除了虚拟内存比较高达到17GB以外,实际使用的内存RSS也夸张的达到了7GB,远远超过了-Xmx的设定。

1
2
[root]$ ps -p 75 -o rss,vsz  
RSS VSZ 7152568 17485844

  

排查过程

  明显的,是有堆外内存的使用,了解到基础软件涉及到netty,netty会用到一些DirectByteBuffer,第一轮排查采用如下方式:

1
2
jmap -dump:format=b,file=75.dump 75 通过分析堆内存找到DirectByteBuffer的引用和大小
部署一个升级基础软件之前的版本,持续观察

  结果还是出现了内存超用的问题。

Read more »

Lambda表达式到底被编译成了什么,JVM会在编译时和运行时对Lambda表达式动手脚。

  • 编译时:Lambda 表达式会生成一个方法, 方法实现了表达式的代码逻辑;生成invokedynamic指令,调用bootstrap方法,由java.lang.invoke.LambdaMetafactory.metafactory方法实现。

  • 运行时:invokedynamic指令调用metafactory方法。 它会返回一个CallSite, 此CallSite返回目标类型的一个匿名实现类, 此类关联编译时产生的方法;lambda表达式调用时会调用匿名实现类关联的方法。

Read more »

[toc]

数据结构

ConcurrentSkipListMap

ConcurrentSkipListMap类实现了一个基于跳表(Skip List)算法的并发有序映射,在并发环境中非常有用,尤其是当需要保持键值对的排序顺序,并且多个线程可能同时读写映射时。

假如,有一个在线购物平台,其中有一个功能是根据商品的评分对商品进行排序,用户可以看到按评分从高到低排列的商品列表,并且这个列表需要实时更新,因为其他用户可能正在对商品进行评分。在这个场景中,可以使用ConcurrentSkipListMap来存储商品ID和对应的评分,可以将评分作为键(因为需要根据评分进行排序),将商品ID作为值,每当有新评分时,可以安全地更新映射,而不需要担心并发问题,由于ConcurrentSkipListMap支持多个线程可以同时读写映射,而不会导致数据不一致或需要额外的同步措施。

ConcurrentSkipListMap类实现了SortedMap接口,提供了基于跳表算法的有序键值对存储,其优点在于高效的并发访问性能,能够在多线程环境下保持良好的读写吞吐量,特别适合需要高并发的场景,此外,ConcurrentSkipListMap的插入、删除和查找操作都具有对数级别的平均时间复杂度,使得它在处理大量数据时依然能够保持较快的响应速度。

相较于其他数据结构,如ConcurrentHashMap,ConcurrentSkipListMap的内存占用通常更高,因为它需要维护跳表的多层结构,此外,在高并发写入的场景下,ConcurrentSkipListMap的性能可能会略逊于专门优化过的哈希表实现。

在技术方案选型上,如果应用程序需要维护一个有序的键值对集合,并且这个集合会被多个线程并发访问,那么ConcurrentSkipListMap是一个很好的选择。但如果对内存占用有严格要求,或者写入操作远多于读取操作,那么可能需要考虑其他更适合的数据结构。

ConcurrentSkipListSet

ConcurrentSkipListSet类在多线程环境下,它能够轻松应对大量的插入、删除和查找操作,同时保持数据的完整性和一致性,其内部基于跳表数据结构的实现,确保了即使在处理大规模数据时,也能具有出色的性能表现。

ConcurrentSkipListSet类实现了一个基于SkipList(跳表)算法的可排序的并发集合,SkipList是一种可以在对数预期时间内完成搜索、插入、删除等操作的数据结构,通过维护多个指向其他元素的“跳跃”链接来实现高效查找。

ConcurrentSkipListSet 类实现了 SortedSet 接口,内部基于 Skip List(跳表)数据结构,并提供了高效的并发访问,这个类能够保证元素的有序性,并且允许并发修改。

ConcurrentLinkedQueue

ConcurrentLinkedQueue类它利用无锁算法,确保在多线程环境下元素的快速入队和出队,且不会因队列满而阻塞生产者,无界设计让数据流动更自由,非常适合高并发、大数据量的处理场景,是构建响应迅速、可扩展并发系统的理想选择。

ConcurrentLinkedQueue类主要用来解决在多线程环境下对队列进行安全并发访问的问题。它采用了一种不同的策略,基于无锁(lock-free)算法实现,通过原子操作和CAS(Compare-and-Swap)等机制来保证线程安全,这种策略允许多个线程同时访问队列,而不需要进行加锁和等待,从而大大提高了并发性能。

ConcurrentLinkedDeque

ConcurrentLinkedDeque类提供了线程安全的双端队列操作,支持高效的并发访问,因此在多线程环境下,可以放心地在队列的两端添加或移除元素,而不用担心数据的一致性问题。同时,它的内部实现采用了无锁算法,从而避免了传统锁带来的性能开销。

  滑动窗口计数有很多使用场景,比如说限流防止系统雪崩。相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺。
  滑动窗口原理是在每次有访问进来时,先判断前N个单位时间内的总访问量是否超过了设置的阈值,并对当前时间片上的请求数+1。

Read more »
0%