CMS GC

1. 概述

CMS,Most concurrently,是一款 采用 并发-标记-清除(Concurrent-Mark-Sweep)算法、针对老年代的 GC,其目的是最小化暂停时间。

CMS GC 在 JDK 9 被标记为 deprecated。

2. 执行过程

CMS 的标记过程采用指针可达性分析方法,对于存活的对象,在对象头上进行标记。

2.1 初始标记

从 GC Roots、新生代(Eden、From、To)出发标记直接关联的对象。

需要 STW 。

2.2 并发标记

该阶段进行 GC Root Tracing,从初始标记阶段 标记过的对象出发,标记所有可达的对象。

与应用线程并发执行,可以采用多个标记线程进行并发标记。

2.3 预清理

此阶段从新生代晋升的对象、新分配到老年代的对象以及在并发阶段修改了的对象 出发进行标记。
在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的 card 标记为 dirty,通过扫描这些 card,重新标记那些在并发阶段引用被更新的对象。

通过 -XX:-CMSPrecleaningEnabled 参数选择关闭该阶段,默认开启。

目的是减少重新标记阶段的暂停时间。

CMS 提供了参数 CMSScavengeBeforeRemark 在 重新标记之前强制进行一次 minor GC,这样做的好处是缩短了重新标记阶段的暂停时间,坏处是 Minor GC 后紧跟着一个 重新标记的 暂停,这样使停顿时间也比较久。

继续阅读

《垃圾回收算法手册》–第2章 标记-清扫回收

1、垃圾回收基础

垃圾回收器的安全性是指:在任何时候都不能回收存活对象。

浮动垃圾(floating garbage):如果某个对象在回收过程启动之后才变成垃圾,那么该对象只能在下一个回收周期内得到回收。

回收器在标记对象时可以使用对象中的域,也可以使用额外的数据结构如位图 bitmap;对于并发回收器或其他需要将堆分为数个独立区域的回收器,其需要额外的记忆集(remembered set)来保存赋值器所修改的指针值或者跨区域指针的位置。

赋值器:应用线程。

在类型安全的语言中,一旦某个对象在堆中不可达,赋值器在没有外部系统协助的情况下是无法访问到不可达对象的。

赋值器在工作过程中会执行三种与回收器相关的操作:创建(New)、读(Read)、写(Write)。某些特殊的内存管理器会为基本操作增加一些额外功能,即屏障(barrier),屏障操作会同步或异步地与回收器产生交互。

在新分配的对象可被赋值器操作之前,回收器都需要先对其元数据进行初始化。

任何自动内存管理系统都面临三个任务:
1、为新对象分配空间。
2、确定存活对象。
3、回收死亡对象所占用的空间。

回收空间的方法影响这分配新空间的方法。

基于指针可达性分析得到的存活对象集合中可能包含一些永远不会再被赋值器访问的对象,但是死亡对象集合中的对象却必定是死亡的。

继续阅读

CAS/volatile 原理

1. 指令的 lock 前缀

intel手册对 lock 前缀的说明如下:

  1. 确保被修饰指令执行的原子性。
  2. 禁止该指令与前面和后面的读写指令重排序。
  3. 指令执行完后把写缓冲区的所有数据刷新到内存中。(这样这个指令之前的其他修改对所有处理器可见。)

在 Pentium 及之前的处理器中,带有 lock 前缀的指令在执行期间会声言 LOCK# 信号以锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel 使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低 lock 前缀指令的执行开销。如果访问的内存区域已经缓存在处理器内部,则不会声言 LOCK# 信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据

继续阅读

MySQL 事务隔离及实现

1. 事务的 ACID 特性

  • Actomic,原子性:一个事务中的所有操作,要么全部完成,要么全部失败。

  • Consistency:一致性:事务执行前和执行后数据库都处于一致的状态。

  • Isolation:隔离性:同时进行中的事务不会看到其他事务的中间状态。

  • Durability,持久性:事务提交成功后,对数据库所做的变更都是持久的。

2. SQL 标准的事务隔离级别

  • 读未提交, read uncommitted:一个事务还没提交,它做的变更就能被别的事务看到。问题:会产生”脏读”,读到的数据可能是不一致的。

  • 读已提交, read committed:一个事务提交后,它做的变更才会被别的事务看到。问题:不可重复读,在一个事务里,前后两次执行同一个 SQL 看到的数据是不一致的,产生的原因有:1. 数据被修改或删除(称为”不可重复读”),2. 有新插入的数据(称为”幻读”)。

  • 可重复读, repeatable read:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。解决了”不可重复读”的问题,但没有解决”幻读”问题。

  • 串行化, serializable:对于同一行记录,“写”会加写锁,“读”会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。解决了”幻读”的问题,但效率低。

继续阅读

MyBatis Mapper 代理实现数据库调用原理

1. Mapper 代理层执行

Mapper 代理上执行方法调用时,调用被委派给 MapperProxy 来处理。

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
        // 接口里声明的方法,转换为 MapperMethod 来调用
        final MapperMethod mapperMethod = cachedMapperMethod(method);

        // 与 Spring 集成时此处的 sqlSession 仍然 SqlSessionTemplate
        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}

MapperMethod 根据 mapperInterface.getName() + "." + method.getName() 从 Configuration 对象里找到对应的 MappedStatement,从而得到要执行的 SQL 操作类型(insert/delete/update/select/flush),然后调用传入的 sqlSession 实例上的相应的方法。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
        // 把参数转换为 SqlSession 能处理的格式
        Object param = method.convertArgsToSqlCommandParam(args);

        // 在 sqlSession 上执行并处理结果
        result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
    ...省略

如果上述方法传入的是 SqlSessionTemplate,那么这些方法调用会被 SqlSessionInterceptor 拦截,加入与 Spring 事务管理机制协作的逻辑,具体可以看这篇文章MyBatis 事务管理,这里不再展开,最终会调用到 DefaultSqlSession 实例上的方法。

2. 会话层的执行过程

SqlSession 里声明的所有方法的第一个参数如果是 String statement,则都是 mapperInterface.getName() + "." + method.getName(),表示要调用的 SQL 语句的标识符。通过它从 configuration 找到 MappedStatement

会话层最主要的逻辑是进行参数的包装,获取对应的 MappedStatement,然后调用持有的 Executor 的方法去执行。

继续阅读

MyBatis Mapper 代理创建过程

本文主要关注与 SpringBoot 集成时的初始化过程。

1. 核心组件

  • Configuration:MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。
  • SqlSession:MyBatis 的顶层 API,表示与数据库交互的会话,完成数据库增删改查操作。
  • Executor:执行器是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护。
  • StatementHandler:封装了 JDBC Statement 操作,如设置参数等。
  • ParameterHandler:负责将用户传递的参数转换成 JDBC Statement 所对应的数据类型。
  • ResultSetHandler:负责将 JDBC 返回的 ResultSet 结果集转换成 List 类型的集合。
  • TypeHandler:负责将 Java 数据类型和 jdbc 数据类型之间的映射与转换。

  • MapperFactoryBean:与 Spring 集成时表示一个 Mapper 原型的工厂 bean,用以创建最终的代理。
  • MapperProxy:与一个 mapperInterface 对应,维护了 Map<Method, MapperMethod> 映射。
  • MapperAnnotationBuilder:基于注解驱动的 MappedStatement 解析器,解析接口类的每个方法,封装成 MappedStatement。
  • MappedStatement:维护了一条 <select|update|delete|insert> 节点的封装。
  • SqlSource:负责根据用户传递的 parameterObject 动态生成 SQL 语句,将信息封装成 BoundSql 对象。
  • BoundSql:表示动态生成的 SQL 语句以及相应的参数信息。

整体架构如下图:
mybatis-整体架构

继续阅读

MyBatis 事务管理

1. 运行环境 Enviroment

当 MyBatis 与不同的应用结合时,需要不同的事务管理机制。与 Spring 结合时,由 Spring 来管理事务;单独使用时需要自行管理事务,在容器里运行时可能由容器进行管理。

MyBatis 用 Enviroment 来表示运行环境,其封装了三个属性:

public class Configuration {
    // 一个 MyBatis 的配置只对应一个环境
    protected Environment environment;
    // 其他属性 .....
}

public final class Environment {
    private final String id;
    private final TransactionFactory transactionFactory;
    private final DataSource dataSource;
}

2. 事务抽象

MyBatis 把事务管理抽象出 Transaction 接口,由 TransactionFactory 接口的实现类负责创建。

public interface Transaction {
    Connection getConnection() throws SQLException;
    void commit() throws SQLException;
    void rollback() throws SQLException;
    void close() throws SQLException;
    Integer getTimeout() throws SQLException;
}

public interface TransactionFactory {
    void setProperties(Properties props);
    Transaction newTransaction(Connection conn);
    Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}

Executor 的实现持有一个 SqlSession 实现,事务控制是委托给 SqlSession 的方法来实现的。

public abstract class BaseExecutor implements Executor {
    protected Transaction transaction;

    public void commit(boolean required) throws SQLException {
        if (closed) {
            throw new ExecutorException("Cannot commit, transaction is already closed");
        }
        clearLocalCache();
        flushStatements();
        if (required) {
            transaction.commit();
        }
    }

    public void rollback(boolean required) throws SQLException {
        if (!closed) {
            try {
                clearLocalCache();
                flushStatements(true);
            } finally {
                if (required) {
                    transaction.rollback();
                }
            }
        }
    }

    // 省略其他方法、属性
}

继续阅读

MyBatis 缓存

所有 insert/update/delete 语句都会导致缓存被清除。

1. 一级缓存

一级缓存是针对数据库会话的,用于优化在一次数据库会话里多次执行同样的 SQL。

有两种范围:

  • SESSION:会话级,默认的。
  • STATEMENT:语句级,每次执行完 mapper 中的语句后都清除一级缓存,其实就是禁用这个语句的一级缓存。

如果要在全局更改一级缓存的范围,需要在 MyBatis 的配置文件中设置:

<setting name="localCacheScope" value="STATEMENT"/>

2. 二级缓存

二级缓存是针对 Mapper 级别的,默认是启用的,但生效的话要对每个 Mapper 进行配置,Mapper 里没有配置的不使用二级缓存。

注意:如果在多个 Mapper 中存在对同一个表的操作,那么这几个 Mapper 的缓存数据可能会出现不一致现象。

<!-- 不启用的话在配置文件中指定如下 -->
<settings>
<setting name="cacheEnabled" value="false" />
</settings>


<!-- Mapper 文件里配置 cache 元素以生效 -->
<mapper namespace="...UserMapper">
    <!-- 默认对该 Mapper 文件里的所有查询使用二级缓存 -->
    <cache/>

    <!-- 该select语句不使用缓存 -->
    <select id="selectAbc" useCache="false">
        ...
    </select>
</mapper>

3. 源码实现

继续阅读

JUC 并发 Queue 设计与介绍

Queue 体系

Queue 是一种先进先出的队列。

ArrayBlockingQueue 和 LinkedBlockingQueue 是带阻塞特性,基于锁来实现。ArrayBlockingQueue 采用同一把锁来控制出、入队列操作;LinkedBlockingQueue 用两把锁来分别控制出、入队列操作,提高了并发性能。

ConcurrentLinkedQueue 非阻塞,采用无锁算法、利用 CAS 操作来实现。

0.1 BlockingQueue

当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素、但队列为空时,消费者会被阻塞。

其实现类必须是线程安全,入队列 happen-before 出队列。

0.2 TransferQueue

继承自 BlockingQueue,更进一步:生产者会一直阻塞直到添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列)。

特别适用于这种应用间传递消息的场景:生产者有时需要等待消费者接收消息,有时只需把消息放进队列、不需要等待消费者接收。

// 传递元素给消费者,如果需要则等待。确保一次传递完成。
void transfer(E e);

// 非阻塞
boolean tryTransfer(E e);

// 基于等待时间的。
boolean tryTransfer(E e, long timeout, TimeUnit unit);

// 返回是否有在等待接收元素的消费者
// (BlockingQueue.take()或带等待时间的 poll 方法调用)
boolean hasWaitingConsumer();

// 返回大概的在等待接收元素的消费者
//(BlockingQueue.take()或带等待时间的 poll 方法调用)
int getWaitingConsumerCount();

0.3 Deque

允许在两端进行插入、删除元素的线性集合。

实现类:

  • ArrayDeque:基于数组加头尾两个指针来实现、非线程安全的。
  • LinkedList:基于双向链表实现、非线程安全的。
  • ConcurrentLinkedDeque:基于双向链表、CAS 元语实现、无界的。

0.4 BlockingDeque

当 Deque 里没有元素时阻塞消费者,当没有空闲空间时阻塞生产者。

目前只有一个实现类 LinkedBlockingDeque,使用双向链表来存储元素,支持容量限制,用一把锁来保证线程安全性。因为允许在两端进行操作,双向链表更合适。

继续阅读