上一篇文章主要讲了netty的read过程,本文主要分析一下write和writeAndFlush。

主要内容

本文分以下几个部分阐述一个java对象最后是如何转变成字节流,写到socket缓冲区中去的

  1. pipeline中的标准链表结构
  2. java对象编码过程
  3. write:写队列
  4. flush:刷新写队列
  5. writeAndFlush: 写队列并刷新

pipeline中的标准链表结构

一个标准的pipeline链式结构如下

数据从head节点流入,先拆包,然后解码成业务对象,最后经过业务Handler处理,调用write,将结果对象写出去。而写的过程先通过tail节点,然后通过encoder节点将对象编码成ByteBuf,最后将该ByteBuf对象传递到head节点,调用底层的Unsafe写到jdk底层管道

java对象编码过程

为什么我们在pipeline中添加了encoder节点,java对象就转换成netty可以处理的ByteBuf,写到管道里?

我们先看下调用write的code

BusinessHandler

  1. protected void channelRead0(ChannelHandlerContext ctx, Request request) throws Exception {
  2. Response response = doBusiness(request);
  3.  
  4. if (response != null) {
  5. ctx.channel().write(response);
  6. }
  7. }

业务处理器接受到请求之后,做一些业务处理,返回一个Response,然后,response在pipeline中传递,落到 Encoder节点,我们来跟踪一下 ctx.channel().write(response);

  1. public ChannelFuture write(Object msg) {
  2. return this.pipeline.write(msg);
  3. }

调用了Channel中的pipeline中的write方法,我们接着看

  1. public final ChannelFuture write(Object msg) {
  2. return this.tail.write(msg);
  3. }

pipeline中有属性tail,调用tail中的write,由此我们知道write消息的时候,从tail开始,接着往下看

  1. private void write(Object msg, boolean flush, ChannelPromise promise) {
  2. AbstractChannelHandlerContext next = this.findContextOutbound();
  3. Object m = this.pipeline.touch(msg, next);
  4. EventExecutor executor = next.executor();
  5. if (executor.inEventLoop()) {
  6. if (flush) {
  7. next.invokeWriteAndFlush(m, promise);
  8. } else {
  9. next.invokeWrite(m, promise);
  10. }
  11. } else {
  12. Object task;
  13. if (flush) {
  14. task = AbstractChannelHandlerContext.WriteAndFlushTask.newInstance(next, m, promise);
  15. } else {
  16. task = AbstractChannelHandlerContext.WriteTask.newInstance(next, m, promise);
  17. }
  18.  
  19. safeExecute(executor, (Runnable)task, promise, m);
  20. }
  21.  
  22. }

中间我省略了几个重载的方法,我们来看看第一行代码,next = this.findContextOutbound();

  1. private AbstractChannelHandlerContext findContextOutbound() {
  2. AbstractChannelHandlerContext ctx = this;
  3.  
  4. do {
  5. ctx = ctx.prev;
  6. } while(!ctx.outbound);
  7.  
  8. return ctx;
  9. }

通过 ctx = ctx.prev; 我们知道从tail开始找到pipeline中的第一个outbound的handler,然后调用 invokeWrite(m, promise),此时找到的第一个outbound的handler就是我们自定义的编码器Encoder

我们接着看 next.invokeWrite(m, promise);

  1. private void invokeWrite(Object msg, ChannelPromise promise) {
  2. if (this.invokeHandler()) {
  3. this.invokeWrite0(msg, promise);
  4. } else {
  5. this.write(msg, promise);
  6. }
  7.  
  8. }
  9. private void invokeWrite0(Object msg, ChannelPromise promise) {
  10. try {
  11. ((ChannelOutboundHandler)this.handler()).write(this, msg, promise);
  12. } catch (Throwable var4) {
  13. notifyOutboundHandlerException(var4, promise);
  14. }
  15.  
  16. }

一路代码跟下来,我们可以知道是调用了第一个outBound类型的handler中的write方法,也就是第一个调用的是我们自定义编码器Encoder的write方法

我们来看看自定义Encoder

  1. public class Encoder extends MessageToByteEncoder<Response> {
  2. @Override
  3. protected void encode(ChannelHandlerContext ctx, Response response, ByteBuf out) throws Exception {
  4. out.writeByte(response.getVersion());
  5. out.writeInt(4 + response.getData().length);
  6. out.writeBytes(response.getData());
  7. }
  8. }

自定义Encoder继承 MessageToByteEncoder ,并且重写了 encode方法,这就是编码器的核心,我们先来看 MessageToByteEncoder

  1. public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {

我们看到 MessageToByteEncoder 继承了 ChannelOutboundHandlerAdapter,说明了 Encoder 是一个 Outbound的handler

我们来看看 Encoder 的父类 MessageToByteEncoder中的write方法

MessageToByteEncoder

  1. @Override
  2. public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
  3. ByteBuf buf = null;
  4. try {
  5. // 判断当前Handelr是否能处理写入的消息
  6. if (acceptOutboundMessage(msg)) {
  7. @SuppressWarnings("unchecked")
  8. // 强制换换
  9. I cast = (I) msg;
  10. // 分配一段ButeBuf
  11. buf = allocateBuffer(ctx, cast, preferDirect);
  12. try {
  13. // 调用encode,这里就调回到 `Encoder` 这个Handelr中
  14. encode(ctx, cast, buf);
  15. } finally {
  16. // 既然自定义java对象转换成ByteBuf了,那么这个对象就已经无用了,释放掉
  17. // (当传入的msg类型是ByteBuf的时候,就不需要自己手动释放了)
  18. ReferenceCountUtil.release(cast);
  19. }
  20. // 如果buf中写入了数据,就把buf传到下一个节点
  21. if (buf.isReadable()) {
  22. ctx.write(buf, promise);
  23. } else {
  24. // 否则,释放buf,将空数据传到下一个节点
  25. buf.release();
  26. ctx.write(Unpooled.EMPTY_BUFFER, promise);
  27. }
  28. buf = null;
  29. } else {
  30. // 如果当前节点不能处理传入的对象,直接扔给下一个节点处理
  31. ctx.write(msg, promise);
  32. }
  33. } catch (EncoderException e) {
  34. throw e;
  35. } catch (Throwable e) {
  36. throw new EncoderException(e);
  37. } finally {
  38. // 当buf在pipeline中处理完之后,释放
  39. if (buf != null) {
  40. buf.release();
  41. }
  42. }
  43. }

这里,我们详细阐述一下Encoder是如何处理传入的java对象的

1.判断当前Handler是否能处理写入的消息,如果能处理,进入下面的流程,否则,直接扔给下一个节点处理
2.将对象强制转换成Encoder可以处理的 Response对象
3.分配一个ByteBuf
4.调用encoder,即进入到 Encoderencode方法,该方法是用户代码,用户将数据写入ByteBuf
5.既然自定义java对象转换成ByteBuf了,那么这个对象就已经无用了,释放掉,(当传入的msg类型是ByteBuf的时候,就不需要自己手动释放了)
6.如果buf中写入了数据,就把buf传到下一个节点,否则,释放buf,将空数据传到下一个节点
7.最后,当buf在pipeline中处理完之后,释放节点

总结一点就是,Encoder节点分配一个ByteBuf,调用encode方法,将java对象根据自定义协议写入到ByteBuf,然后再把ByteBuf传入到下一个节点,在我们的例子中,最终会传入到head节点,因为head节点是一个OutBount类型的handler

HeadContext

  1. public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
  2. unsafe.write(msg, promise);
  3. }

这里的msg就是前面在Encoder节点中,载有java对象数据的自定义ByteBuf对象,进入下一节

write:写队列

我们来看看channel中unsafe的write方法,先来看看其中的一个属性

AbstractUnsafe

  1. protected abstract class AbstractUnsafe implements Unsafe {
  2. private volatile ChannelOutboundBuffer outboundBuffer = new ChannelOutboundBuffer(AbstractChannel.this);

我们来看看 ChannelOutboundBuffer 这个类

  1. public final class ChannelOutboundBuffer {
  2. private final Channel channel;
  3. private ChannelOutboundBuffer.Entry flushedEntry;
  4. private ChannelOutboundBuffer.Entry unflushedEntry;
  5. private ChannelOutboundBuffer.Entry tailEntry;

ChannelOutboundBuffer内部维护了一个Entry链表,并使用Entry封装msg。其中的属性我们下面会详细讲

我们回到正题,接着看 unsafe.write(msg, promise);

AbstractUnsafe

  1. @Override
  2. public final void write(Object msg, ChannelPromise promise) {
  3. assertEventLoop();
  4.  
  5. ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
  6. int size;
  7. try {
  8. msg = filterOutboundMessage(msg);
  9. size = pipeline.estimatorHandle().size(msg);
  10. if (size < 0) {
  11. size = 0;
  12. }
  13. } catch (Throwable t) {
  14. safeSetFailure(promise, t);
  15. ReferenceCountUtil.release(msg);
  16. return;
  17. }
  18.  
  19. outboundBuffer.addMessage(msg, size, promise);
  20. }

1.调用 filterOutboundMessage() 方法,将待写入的对象过滤,把非ByteBuf对象和FileRegion过滤,把所有的非直接内存转换成直接内存DirectBuffer

  1. @Override
  2. protected final Object filterOutboundMessage(Object msg) {
  3. if (msg instanceof ByteBuf) {
  4. ByteBuf buf = (ByteBuf) msg;
  5. if (buf.isDirect()) {
  6. return msg;
  7. }
  8.  
  9. return newDirectBuffer(buf);
  10. }
  11.  
  12. if (msg instanceof FileRegion) {
  13. return msg;
  14. }
  15.  
  16. throw new UnsupportedOperationException(
  17. "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
  18. }

2.接下来,估算出需要写入的ByteBuf的size
3.最后,调用 ChannelOutboundBuffer 的addMessage(msg, size, promise) 方法,所以,接下来,我们需要重点看一下这个方法干了什么事情

ChannelOutboundBuffer

  1. public void addMessage(Object msg, int size, ChannelPromise promise) {
  2. // 创建一个待写出的消息节点
  3. Entry entry = Entry.newInstance(msg, size, total(msg), promise);
  4. if (tailEntry == null) {
  5. flushedEntry = null;
  6. tailEntry = entry;
  7. } else {
  8. Entry tail = tailEntry;
  9. tail.next = entry;
  10. tailEntry = entry;
  11. }
  12. if (unflushedEntry == null) {
  13. unflushedEntry = entry;
  14. }
  15.  
  16. incrementPendingOutboundBytes(size, false);
  17. }

想要理解上面这段代码,必须得掌握写缓存中的几个消息指针,如下图

ChannelOutboundBuffer 里面的数据结构是一个单链表结构,每个节点是一个 EntryEntry 里面包含了待写出ByteBuf 以及消息回调 promise,下面分别是三个指针的作用

1.flushedEntry 指针表示第一个被写到操作系统Socket缓冲区中的节点
2.unFlushedEntry 指针表示第一个未被写入到操作系统Socket缓冲区中的节点
3.tailEntry指针表示ChannelOutboundBuffer缓冲区的最后一个节点

初次调用 addMessage 之后,各个指针的情况为

fushedEntry指向空,unFushedEntry和 tailEntry 都指向新加入的节点

第二次调用 addMessage之后,各个指针的情况为

第n次调用 addMessage之后,各个指针的情况为

可以看到,调用n次addMessage,flushedEntry指针一直指向NULL,表示现在还未有节点需要写出到Socket缓冲区,而unFushedEntry之后有n个节点,表示当前还有n个节点尚未写出到Socket缓冲区中去

flush:刷新写队列

不管调用channel.flush(),还是ctx.flush(),最终都会落地到pipeline中的head节点

HeadContext

  1. @Override
  2. public void flush(ChannelHandlerContext ctx) throws Exception {
  3. unsafe.flush();
  4. }

之后进入到AbstractUnsafe

AbstractUnsafe

  1. public final void flush() {
  2. assertEventLoop();
  3.  
  4. ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
  5. if (outboundBuffer == null) {
  6. return;
  7. }
  8.  
  9. outboundBuffer.addFlush();
  10. flush0();
  11. }

flush方法中,先调用 outboundBuffer.addFlush();

ChannelOutboundBuffer

  1. public void addFlush() {
  2. Entry entry = unflushedEntry;
  3. if (entry != null) {
  4. if (flushedEntry == null) {
  5. flushedEntry = entry;
  6. }
  7. do {
  8. flushed ++;
  9. if (!entry.promise.setUncancellable()) {
  10. int pending = entry.cancel();
  11. decrementPendingOutboundBytes(pending, false, true);
  12. }
  13. entry = entry.next;
  14. } while (entry != null);
  15. unflushedEntry = null;
  16. }
  17. }

可以结合前面的图来看,首先拿到 unflushedEntry 指针,然后将 flushedEntry 指向unflushedEntry所指向的节点,调用完毕之后,三个指针的情况如下所示

相当于所有的节点都即将开始推送出去

接下来,调用 flush0();

AbstractUnsafe

  1. protected void flush0() {
  2. doWrite(outboundBuffer);
  3. }

发现这里的核心代码就一个 doWrite,继续跟

AbstractNioByteChannel

  1. protected void doWrite(ChannelOutboundBuffer in) throws Exception {
  2. int writeSpinCount = -1;
  3.  
  4. boolean setOpWrite = false;
  5. for (;;) {
  6. // 拿到第一个需要flush的节点的数据
  7. Object msg = in.current();
  8. if (msg instanceof ByteBuf) {
  9. // 强转为ByteBuf,若发现没有数据可读,直接删除该节点
  10. ByteBuf buf = (ByteBuf) msg;
  11.  
  12. boolean done = false;
  13. long flushedAmount = 0;
  14. // 拿到自旋锁迭代次数
  15. if (writeSpinCount == -1) {
  16. writeSpinCount = config().getWriteSpinCount();
  17. }
  18. // 自旋,将当前节点写出
  19. for (int i = writeSpinCount - 1; i >= 0; i --) {
  20. int localFlushedAmount = doWriteBytes(buf);
  21. if (localFlushedAmount == 0) {
  22. setOpWrite = true;
  23. break;
  24. }
  25.  
  26. flushedAmount += localFlushedAmount;
  27. if (!buf.isReadable()) {
  28. done = true;
  29. break;
  30. }
  31. }
  32.  
  33. in.progress(flushedAmount);
  34.  
  35. // 写完之后,将当前节点删除
  36. if (done) {
  37. in.remove();
  38. } else {
  39. break;
  40. }
  41. }
  42. }
  43. }

这里略微有点复杂,我们分析一下

1.第一步,调用current()先拿到第一个需要flush的节点的数据

ChannelOutBoundBuffer

  1. public Object current() {
  2. Entry entry = flushedEntry;
  3. if (entry == null) {
  4. return null;
  5. }
  6.  
  7. return entry.msg;
  8. }

2.第二步,拿到自旋锁的迭代次数

  1. if (writeSpinCount == -1) {
  2. writeSpinCount = config().getWriteSpinCount();
  3. }

3.自旋的方式将ByteBuf写出到jdk nio的Channel

  1. for (int i = writeSpinCount - 1; i >= 0; i --) {
  2. int localFlushedAmount = doWriteBytes(buf);
  3. if (localFlushedAmount == 0) {
  4. setOpWrite = true;
  5. break;
  6. }
  7.  
  8. flushedAmount += localFlushedAmount;
  9. if (!buf.isReadable()) {
  10. done = true;
  11. break;
  12. }
  13. }

doWriteBytes 方法跟进去

  1. protected int doWriteBytes(ByteBuf buf) throws Exception {
  2. final int expectedWrittenBytes = buf.readableBytes();
  3. return buf.readBytes(javaChannel(), expectedWrittenBytes);
  4. }

我们发现,出现了 javaChannel(),表明已经进入到了jdk nio Channel的领域,我们来看看 buf.readBytes(javaChannel(), expectedWrittenBytes);

  1. public int readBytes(GatheringByteChannel out, int length) throws IOException {
  2. this.checkReadableBytes(length);
  3. int readBytes = this.getBytes(this.readerIndex, out, length);
  4. this.readerIndex += readBytes;
  5. return readBytes;
  6. }

我们来看关键代码 this.getBytes(this.readerIndex, out, length)

  1. private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException {
  2. this.checkIndex(index, length);
  3. if (length == ) {
  4. return ;
  5. } else {
  6. ByteBuffer tmpBuf;
  7. if (internal) {
  8. tmpBuf = this.internalNioBuffer();
  9. } else {
  10. tmpBuf = ((ByteBuffer)this.memory).duplicate();
  11. }
  12.  
  13. index = this.idx(index);
  14. tmpBuf.clear().position(index).limit(index + length);
  15. //将tmpBuf中的数据写到out中
  16. return out.write(tmpBuf);
  17. }
  18. }

我们来看看out.write(tmpBuf)

  1. public int write(ByteBuffer src) throws IOException {
  2. ensureOpen();
  3. if (!writable)
  4. throw new NonWritableChannelException();
  5. synchronized (positionLock) {
  6. int n = ;
  7. int ti = -;
  8. try {
  9. begin();
  10. ti = threads.add();
  11. if (!isOpen())
  12. return ;
  13. do {
  14. n = IOUtil.write(fd, src, -, nd);
  15. } while ((n == IOStatus.INTERRUPTED) && isOpen());
  16. return IOStatus.normalize(n);
  17. } finally {
  18. threads.remove(ti);
  19. end(n > );
  20. assert IOStatus.check(n);
  21. }
  22. }
  23. }

和read实现一样,SocketChannelImpl的write方法通过IOUtil的write实现:关键代码 n = IOUtil.write(fd, src, -1, nd);

  1. static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
  2. //如果是DirectBuffer,直接写,将堆外缓存中的数据拷贝到内核缓存中进行发送
  3. if (var1 instanceof DirectBuffer) {
  4. return writeFromNativeBuffer(var0, var1, var2, var4);
  5. } else {
  6. //非DirectBuffer
  7. //获取已经读取到的位置
  8. int var5 = var1.position();
  9. //获取可以读到的位置
  10. int var6 = var1.limit();
  11.  
  12. assert var5 <= var6;
  13. //申请一个原buffer可读大小的DirectByteBuffer
  14. int var7 = var5 <= var6 ? var6 - var5 : ;
  15. ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);
  16.  
  17. int var10;
  18. try {
  19.  
  20. var8.put(var1);
  21. var8.flip();
  22. var1.position(var5);
  23. //通过DirectBuffer写,将堆外缓存的数据拷贝到内核缓存中进行发送
  24. int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
  25. if (var9 > ) {
  26. var1.position(var5 + var9);
  27. }
  28.  
  29. var10 = var9;
  30. } finally {
  31. //回收分配的DirectByteBuffer
  32. Util.offerFirstTemporaryDirectBuffer(var8);
  33. }
  34.  
  35. return var10;
  36. }
  37. }

代码逻辑我们就不再讲了,代码注释已经很清楚了,这里我们关注一点,我们可以看看我们前面的一个方法 filterOutboundMessage(),将待写入的对象过滤,把非ByteBuf对象和FileRegion过滤,把所有的非直接内存转换成直接内存DirectBuffer

说明到了这一步所有的 var1 意境是直接内存DirectBuffer,就不需要走到else,就不需要write两次了

4.删除该节点

节点的数据已经写入完毕,接下来就需要删除该节点

ChannelOutBoundBuffer

  1. public boolean remove() {
  2. Entry e = flushedEntry;
  3. Object msg = e.msg;
  4.  
  5. ChannelPromise promise = e.promise;
  6. int size = e.pendingSize;
  7.  
  8. removeEntry(e);
  9.  
  10. if (!e.cancelled) {
  11. ReferenceCountUtil.safeRelease(msg);
  12. safeSuccess(promise);
  13. }
  14.  
  15. // recycle the entry
  16. e.recycle();
  17.  
  18. return true;
  19. }

首先拿到当前被flush掉的节点(flushedEntry所指),然后拿到该节点的回调对象 ChannelPromise, 调用 removeEntry()方法移除该节点

  1. private void removeEntry(Entry e) {
  2. if (-- flushed == 0) {
  3. flushedEntry = null;
  4. if (e == tailEntry) {
  5. tailEntry = null;
  6. unflushedEntry = null;
  7. }
  8. } else {
  9. flushedEntry = e.next;
  10. }
  11. }

这里的remove是逻辑移除,只是将flushedEntry指针移到下个节点,调用完毕之后,节点图示如下

writeAndFlush: 写队列并刷新

理解了write和flush这两个过程,writeAndFlush 也就不难了

  1. public final ChannelFuture writeAndFlush(Object msg) {
  2. return tail.writeAndFlush(msg);
  3. }
  4.  
  5. public ChannelFuture writeAndFlush(Object msg) {
  6. return writeAndFlush(msg, newPromise());
  7. }
  8.  
  9. public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
  10. write(msg, true, promise);
  11.  
  12. return promise;
  13. }
  14.  
  15. private void write(Object msg, boolean flush, ChannelPromise promise) {
  16. AbstractChannelHandlerContext next = findContextOutbound();
  17. EventExecutor executor = next.executor();
  18. if (executor.inEventLoop()) {
  19. if (flush) {
  20. next.invokeWriteAndFlush(m, promise);
  21. } else {
  22. next.invokeWrite(m, promise);
  23. }
  24. }
  25. }

可以看到,最终,通过一个boolean变量,表示是调用 invokeWriteAndFlush,还是 invokeWriteinvokeWrite便是我们上文中的write过程

  1. private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
  2. invokeWrite0(msg, promise);
  3. invokeFlush0();
  4. }

可以看到,最终调用的底层方法和单独调用 write 和 flush 是一样的

  1. private void invokeWrite(Object msg, ChannelPromise promise) {
  2. invokeWrite0(msg, promise);
  3. }
  4.  
  5. private void invokeFlush(Object msg, ChannelPromise promise) {
  6. invokeFlush0(msg, promise);
  7. }

由此看来,invokeWriteAndFlush基本等价于write方法之后再来一次flush

总结

1.pipeline中的编码器原理是创建一个ByteBuf,将java对象转换为ByteBuf,然后再把ByteBuf继续向前传递
2.调用write方法并没有将数据写到Socket缓冲区中,而是写到了一个单向链表的数据结构中,flush才是真正的写出
3.writeAndFlush等价于先将数据写到netty的缓冲区,再将netty缓冲区中的数据写到Socket缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功
4.netty中的缓冲区中的ByteBuf为DirectByteBuf

Netty源码分析 (八)----- write过程 源码分析的更多相关文章

  1. [spring源码学习]八、IOC源码-messageSource

    一.代码实例 我们在第八章可以看到,spring的context在初始化的时候,会默认调用系统中的各种约定好的bean,其中第一个bean就是id为messageSource的bean,我们了解这应该 ...

  2. 框架源码系列八:Spring源码学习之Spring核心工作原理(很重要)

    目录:一.搞清楚ApplicationContext实例化Bean的过程二.搞清楚这个过程中涉及的核心类三.搞清楚IOC容器提供的扩展点有哪些,学会扩展四.学会IOC容器这里使用的设计模式五.搞清楚不 ...

  3. SOFA 源码分析 —— 服务引用过程

    前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ...

  4. Dubbo 源码分析 - 服务调用过程

    注: 本系列文章已捐赠给 Dubbo 社区,你也可以在 Dubbo 官方文档中阅读本系列文章. 1. 简介 在前面的文章中,我们分析了 Dubbo SPI.服务导出与引入.以及集群容错方面的代码.经过 ...

  5. 手机自动化测试:Appium源码分析之跟踪代码分析八

    手机自动化测试:Appium源码分析之跟踪代码分析八   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家 ...

  6. MyBatis 源码分析 - 配置文件解析过程

    * 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...

  7. 源码分析HotSpot GC过程(一)

    «上一篇:源码分析HotSpot GC过程(一)»下一篇:源码分析HotSpot GC过程(三):TenuredGeneration的GC过程 https://blogs.msdn.microsoft ...

  8. 源码分析HotSpot GC过程(三):TenuredGeneration的GC过程

    老年代TenuredGeneration所使用的垃圾回收算法是标记-压缩-清理算法.在回收阶段,将标记对象越过堆的空闲区移动到堆的另一端,所有被移动的对象的引用也会被更新指向新的位置.看起来像是把杂陈 ...

  9. (3.10)mysql基础深入——mysqld 服务器与客户端连接过程 源码分析【待写】

    (3.10)mysql基础深入——mysqld 服务器与客户端连接过程 源码分析[待写]

  10. YARN(MapReduce 2)运行MapReduce的过程-源码分析

    这是我的分析,当然查阅书籍和网络.如有什么不对的,请各位批评指正.以下的类有的并不完全,只列出重要的方法. 如要转载,请注上作者以及出处. 一.源码阅读环境 需要安装jdk1.7.0版本及其以上版本, ...

随机推荐

  1. 转载 | Sublime Text3 安装以及初次配置

    本文引自:http://blog.csdn.net/u011272513/article/details/52088800 工具:官网下载:Sublime Text3 安装:直接运行安装.http:/ ...

  2. javaScript基础-0 javascript概述

    一.简介 javaScript一种面向web的编程语言,是一种动态类型.弱类型.基于原型的语言,内置支持类型.它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早 ...

  3. 100天搞定机器学习|day43 几张GIF理解K-均值聚类原理

    前文推荐 如何正确使用「K均值聚类」? KMeans算法是典型的基于距离的聚类算法,采用距离作为相似性的评价指标,即认为两个对象的距离越近,其相似度就越大.该算法认为簇是由距离靠近的对象组成的,因此把 ...

  4. 以图搜图之模型篇: 基于 InceptionV3 的模型 finetune

    在以图搜图的过程中,需要以来模型提取特征,通过特征之间的欧式距离来找到相似的图形. 本次我们主要讲诉以图搜图模型创建的方法. 图片预处理方法,看这里:https://keras.io/zh/prepr ...

  5. python(自用手册)导图

  6. 析构函数中调用 Dispose 报错 :Internal .Net Framework Data Provider error 1.[非原创]

    搜索MSDN的资源可以找到答案: 原文如下http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=473449&SiteID=1 以下是关于 ...

  7. nodeCZBK-笔记2

    目录 day04 mongoDB数据库使用 day05 node使用mongoDB数据库 day04 mongoDB数据库使用 电脑全局安装数据库 开机命令:mongod --dbpath c:\mo ...

  8. OpenXML性能真的低下吗?

    博文NET导出Excel的四种方法及评测 中对比了4个库的导出性能,但对OpenXML的评价并不高,本人觉得不合理,所以我重新测试下性能 基于OpenXML的包装类 ExcelDownWorker p ...

  9. Suring开发集成部署时问题记录

    前言 开发时一定要用管理员模式打开VS或者VSCODE进行开发,同时不要在nuget上直接下载,要去github上下载源代码调试.第一方便调试,第二Surging迭代较快,nuget版本往往不是最新的 ...

  10. Leetcode之二分法专题-275. H指数 II(H-Index II)

    Leetcode之二分法专题-275. H指数 II(H-Index II) 给定一位研究者论文被引用次数的数组(被引用次数是非负整数),数组已经按照升序排列.编写一个方法,计算出研究者的 h 指数. ...