YDDMAX

代码奔腾


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 搜索

Channel

发表于 2017-06-14 | 分类于 io

本文内容主要摘录自:《JAVA NIO》。

Channel的类图如下:
Channel-class

通道基础

  1. 从 Channel 接口引申出的其他接口都是面向字节的子接口,包括 WritableByteChannel和ReadableByteChannel.这也正好支持了我们之前所学的:通道只能在字节缓冲区上操作。
  2. SelectableChannel和InterruptibleChannel

打开通道

  • SocketChannel sc = SocketChannel.open( );
  • ServerSocketChannel ssc = ServerSocketChannel.open( );
  • DatagramChannel dc = DatagramChannel.open( );
  • RandomAccessFile、 FileInputStream 或 FileOutputStream对象上调用 getChannel( )方法来获取
    1
    2
    RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
    FileChannel fc = raf.getChannel( );

使用通道

  1. 通道可以是单向的,也可以是双向的,由底层的打开方式决定。
  2. ByteChannel 接口本身并不定义新的 API 方法,它是一种用来聚集它自己以一个新名称继承的多个接口的便捷接口。
  3. 通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求操作要么立即完成,要么返回一个结果表明未进行任何操作。
    只有面向流的(stream-oriented)的通道,如 sockets 和 pipes 才能使用非阻塞模式。

关闭通道

  1. 调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞,哪怕该通道处于非阻塞模式。
    通道关闭时的阻塞行为(如果有的话)是高度取决于操作系统或者文件系统的。在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。
  2. 通道引入了一些与关闭和中断有关的新行为。
    如果一个通道实现 InterruptibleChannel接口,它的行为以下述语义为准:如果一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的 interrupt( )方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个 ClosedByInterruptException 异常。
  3. 请不要将在 Channels 上休眠的中断线程同在 Selectors上休眠的中断线程混淆。前者会关闭通道,而后者则不会。
    不过,如果您的线程在 Selector 上休眠时被中断,那它的 interrupt status 会被设置。假设那个线程接着又访问一个Channel,则该通道会被关闭。
  4. 仅仅因为休眠在其上的线程被中断就关闭通道,这看起来似乎过于苛刻了。
    不过这却是 NIO架构师们所做出的明确的设计决定。经验表明,想要在所有的操作系统上一致而可靠地处理被中断的 I/O操作是不可能的。 java.nio 包中强制使用此行为来避免因操作系统独特性而导致的困境,因为该困境对 I/O 区域而言是极其危险的。这也是为增强健壮性(robustness)而采用的一种经典的权衡。
  5. 可中断的通道也是可以异步关闭的。
    实现 InterruptibleChannel 接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个 I/O 操作完成。当一个通道被关闭时,休眠在该通道上的所有线程都将被唤醒并接收到一个 AsynchronousCloseException异常。接着通道就被关闭并将不再可用。

Scanner/Gather

通道提供了一种被称为 Scatter/Gather的重要新功能(有时也被称为矢量 I/O)。

Scatter/Gather是一个简单却强大的概念,它是指在多个缓冲区上实现一个简单的 I/O 操作。
对于一个 write 操作而言,数据是从几个缓冲区按顺序抽取(称为 gather)并沿着通道发送的。缓冲区本身并不需要具备这种 gather 的能力(通常它们也没有此能力)。该 gather过程的效果就好比全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。
对于 read 操作而言,从通道读取的数据会按顺序被散布(称为 scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数63据或者缓冲区的最大空间被消耗完。大多数现代操作系统都支持本地矢量 I/O(native vectored I/O)。当您在一个通道上请求一个Scatter/Gather操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大的进步,因为减少或避免了缓冲区拷贝和系统调用。

Scatter:

1
2
3
4
5
6
7
8
public interface ScatteringByteChannel
extends ReadableByteChannel
{
public long read (ByteBuffer [] dsts)
throws IOException;
public long read (ByteBuffer [] dsts, int offset, int length)
throws IOException;
}

Gather:

1
2
3
4
5
6
7
8
public interface GatheringByteChannel
extends WritableByteChannel
{
public long write(ByteBuffer[] srcs)
throws IOException;
public long write(ByteBuffer[] srcs, int offset, int length)
throws IOException;
}

使用得当的话, Scatter/Gather 会是一个极其强大的工具。它允许您委托操作系统来完成辛苦活:将读取到的数据分开存放到多个存储桶(bucket)或者将不同的数据区块合并成一个整体。这是一个巨大的成就,因为操作系统已经被高度优化来完成此类工作了。它节省了您来回移动数据的工作,也就避免了缓冲区拷贝(为啥?)和减少了您需要编写、调试的代码数量。

文件通道

基本操作

Filechannel-class

FileChannel原型,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class FileChannel
extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// This is a partial API listing
public abstract long position( )
public abstract void position (long newPosition)
public abstract int read (ByteBuffer dst)
public abstract int read (ByteBuffer dst, long position)
public abstract int write (ByteBuffer src)
public abstract int write (ByteBuffer src, long position)
public abstract long size( )
public abstract void truncate (long size)
public abstract void force (boolean metaData)
}

  1. 文件通道总是阻塞式的,因此不能被置于非阻塞模式。
    现代操作系统都有复杂的缓存和预取机制,使得本地磁盘I/O操作延迟很少。网络文件系统一般而言延迟会多些,不过却也因该优化而受益。 面向流的 I/O的非阻塞范例对于面向文件的操作并无多大意义,这是由文件I/O本质上的不同性质造成的。
  2. FileChannel 对象是线程安全(thread-safe)的。
  3. File IO的比较
    file-io-opeartion-compare
  4. 文件空洞
    当磁盘上一个文件的分配空间小于它的文件大小时会出现“文件空洞”。对于内容稀疏的文件,大多数现代文件系统只为实际写入的数据分配磁盘空间(更准确地说,只为那些写入数据的文件系统页分配空间)。假如数据被写入到文件中非连续的位置上,这将导致文件出现在逻辑上不包含数据的区域(即“空洞”)。如果该文件被顺序读取的话,所有空洞都会被“0”填充但不占用磁盘空间。
  5. truncate()
    当需要减少一个文件的 size 时, truncate( )方法会砍掉您所指定的新 size 值之外的所有数据。
    如果当前 size 大于新 size,超出新 size 的所有字节都会被悄悄地丢弃。
    如果提供的新 size 值大于或等于当前的文件 size 值,该文件不会被修改。
    这两种情况下, truncate( )都会产生副作用:文件的position 会被设置为所提供的新 size 值。

文件锁定

有关 FileChannel 实现的文件锁定模型的一个重要注意项是:锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台 Java 虚拟机上的多个线程发起的访问。
如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会被批准。
但如果这两个线程运行在不同的 Java 虚拟机上,那么第二个线程会阻塞,因为锁最终是由操作系统或文件系统来判优的并且几乎总是在进程级而非线程级上判优。锁都是与一个文件关联的,而不是与单个的文件句柄或通道关联。锁与文件关联,而不是与通道关联。我们使用锁来判优外部进程,而不是判优同一个 Java 虚拟机上的线程。

内存映射文件

  1. 因为不需要做明确的系统调用,那会很消耗时间。
    更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗 Java 虚拟机内存堆(memory heap)。
  2. 与文件锁的范围机制不一样,映射文件的范围不应超过文件的实际大小。
    如果您请求一个超出文件大小的映射,文件会被增大以匹配映射的大小(文件空洞)。即使您请求的是一个只读映射,map( )方法也会尝试这样做并且大多数情况下都会抛出一个 IOException 异常,因为底层的文件不能被修改。该行为同之前讨论的文件“空洞”的行为是一致的。
  3. MapMode.READ_ONLY
  4. MapMode.READ_WRITE
  5. MapMode.PRIVATE(写时拷贝)
    • 您通过 put( )方法所做的任何修改都会导致产生一个私有的数据拷贝,并且该拷贝中的数据只有MappedByteBuffer 实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected),那些修改都会丢失。
    • 尽管写时拷贝的映射可以防止底层文件被修改,您也必须以 read/write 权限来打开文件以建立 MapMode.PRIVATE 映射。只有这样,返回的MappedByteBuffer 对象才能允许使用 put( )方法。
    • 选择使用 MapMode.PRIVATE 模式并不会导致您的缓冲区看不到通过其他方式对文件所做的修改。
      对文件某个区域的修改在使用 MapMode.PRIVATE模式的缓冲区中都能反映出来,除非该缓冲区已经修改了文件上的同一个区域。如果缓冲区还没对某个页做出修改,那么这个页就会反映被映射文件的相应位置上的内容。一旦某个页因为写操作而被拷贝,之后就将使用该拷贝页,并且不能被其他缓冲区或文件更新所修改。
    • 关闭相关联的 FileChannel 不会破坏映射,只有丢弃缓冲区对象本身才会破坏该映射。
      NIO设计师们之所以做这样的决定是因为当关闭通道时破坏映射会引起安全问题,而解决该安全问题又会导致性能问题。 如果您确实需要知道一个映射是什么时候被破坏的,他们建议使用虚引用(phantomreferences,参见java.lang.ref.PhantomReference)和一个 cleanup 线程。不过有此需要的概率是微乎其微的。
  6. load()
    在一个映射缓冲区上调用 load()方法会是一个代价高的操作,因为它会导致大量的页调入(page-in),具体数量取决于文件中被映射区域的实际大小。
    然而, load( )方法返回并不能保证文件就会完全常驻内存,这是由于请求页面调入(demandpaging)是动态的。具体结果会因某些因素而有所差异,这些因素包括:操作系统、文件系统,可用 Java 虚拟机内存,最大 Java 虚拟机内存,垃圾收集器实现过程等等。请小心使用 load( )方法,它可能会导致您不希望出现的结果。该方法的主要作用是为提前加载文件埋单,以便后续的访问速度可以尽可能的快。
  7. isLoaded()
    isLoaded( )方法来判断一个被映射的文件是否完全常驻内存了。不过,这也是不能保证的。同样地,返回 false 值并不一定意味着访问缓冲区将很慢或者该文件并未完全常驻内存。 isLoaded()方法的返回值只是一个暗示,由于垃圾收集的异步性质、底层操作系统以及运行系统的动态性等因素,想要在任意时刻准确判断全部映射页的状态是不可能的。
  8. force()
    该方法会强制将映射缓冲区上的更改应用到永久磁盘存储器上。当用 MappedByteBuffer 对象来更新一个文件,您应该总是使用 MappedByteBuffer.force( ),而非 FileChannel.force( ),因为通道对象可能不清楚通过映射缓冲区做出的文件的全部更改。
    MappedByteBuffer 没有不更新文件元数据的选项——元数据总是会同时被更新的。
    如果映射是以 MapMode.READ_ONLY 或 MAP_MODE.PRIVATE 模式建立的,那么调用 force()方法将不起任何作用,因为永远不会有更改需要应用到磁盘上(但是这样做也是没有害处的)。

Channel-to-Channel传输

  1. transferTo( )和 transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。
  2. 只有 FileChannel 类有这两个方法,因此 channel-to-channel 传输中通道之一必须是 FileChannel。您不能在 socket 通道之间直接传输数据,不过 socket 通道实现WritableByteChannel 和 ReadableByteChannel接口,因此文件的内容可以用 transferTo( )方法传输给一个 socket 通道,或者也可以用 transferFrom( )方法将数据从一个 socket通道直接读取到一个文件中。
  3. 直接的通道传输不会更新与某个 FileChannel 关联的 position 值。
    • 对于传输数据来源是一个文件的transferTo()方法,如果position+count的值大于文件的size值,传输会在文件尾的位置终止。
    • 假如传输的目的地是一个非阻塞模式的socket通道,那么当发送队列(sendqueue)满了之后传输就可能终止,并且如果输出队列(output queue)已满的话可能不会发送任何数据。
    • 类似地,对于 transferFrom( )方法:如果来源 src是另外一个FileChannel并且已经到达文件尾,那么传输将提早终止;如果来源 src 是一个非阻塞 socket通道,只有当前处于队列中的数据才会被传输(可能没有数据)。由于网络数据传输的非确定性,阻塞模式的socket 也可能会执行部分传输,这取决于操作系统。许多通道实现都是提供它们当前队列中已有的数据而不是等待您请求的全部数据都准备好。

Socket通道

socket-channel-class

  1. 使用Channel没有为每个 socket连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。
  2. 请注意 DatagramChannel 和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。
  3. 虽然每个 socket 通道(在 java.nio.channels 包中)都有一个关联的 java.net socket 对象,却并非所有的 socket 都有一个关联的通道。如果您用传统方式(直接实例化)创建了一个Socket 对象,它就不会有关联的 SocketChannel并且它的 getChannel( )方法将总是返回 null。

非阻塞模式

Socket 通道可以工作在阻塞和非阻塞模式下,并且可以在运行过程中动态切换。
SelectableChannel:

1
2
3
4
5
6
7
8
9
10
public abstract class SelectableChannel
extends AbstractChannel
implements Channel
{
// This is a partial API listing
public abstract void configureBlocking (boolean block)
throws IOException;
public abstract boolean isBlocking( );99
public abstract Object blockingLock( );
}

  1. 要把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。
    非阻塞 I/O 和可选择性是紧密相连的,那也正是管理阻塞模式的 API 代码要在SelectableChannel 超级类中定义的原因。
  2. blockingLock( )
    该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。

ServerSocketChannel

1
2
3
4
5
6
7
8
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
{
public static ServerSocketChannel open( ) throws IOException
public abstract ServerSocket socket( );
public abstract SocketChannel accept( ) throws IOException;
public final int validOps( )
}
  1. 用静态的 open( )工厂方法创建一个新的 ServerSocketChannel 对象,将会返回与一个未绑定的java.net.ServerSocket 关联的通道。该对等 ServerSocket可以通过在返回的ServerSocketChannel上调用socket()方法来获取。作为ServerSocketChannel 的对等体被创建的 ServerSocket对象依赖通道实现。这些socket关联的SocketImpl能识别通道。通道不能被封装在随意的 socket 对象外面
  2. accept
    • 如果您选择在 ServerSocket 上调用 accept( )方法
      那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对象。
    • 如果您选择在 ServerSocketChannel 上调用 accept( )
      方法则会返回 SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。
    • 如果在非阻塞模式下的ServerSocketChannel上调用
      当没有传入连接在等待时, ServerSocketChannel.accept( )会立即返回 null。

SocketChannel

SocketChannel原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
// This is a partial API listing
public static SocketChannel open( ) throws IOException
public static SocketChannel open (InetSocketAddress remote)
throws IOException
public abstract Socket socket( );
public abstract boolean connect (SocketAddress remote)
throws IOException;103
public abstract boolean isConnectionPending( );
public abstract boolean finishConnect( ) throws IOException;
public abstract boolean isConnected( );
public final int validOps( )
}

  1. connect()
    • 阻塞模式下:
      在Socket对象上调用和通过在阻塞模式的SocketChannel上调用时相同:线程在连接建立好或超时过期之前都将保持阻塞。
    • 非阻塞模式的SocketChannel上调用
      线程不阻塞,如果返回值是true,说明连接立即建立了(这可能是本地环回连接);如果连接不能立即建立, connect( )方法会返回 false 且并发地继续连接建立过程。
  2. 在 SocketChannel 上并没有一种connect()方法可以让您指定超时(timeout)值,编程时一般使用非阻塞模式的SocketChannel.connect()建立异步连接。
  3. isConnectPending()
  4. finishConnect()
    • connect( )方法尚未被调用。那么将产生 NoConnectionPendingException 异常。
    • 连接建立过程正在进行,尚未完成。那么什么都不会发生, finishConnect( )方法会立即返回false 值。
    • 在非阻塞模式下调用 connect()方法之后,SocketChannel又被切换回了阻塞模式。那么如果有必要的话,调用线程会阻塞直到连接建立完成, finishConnect( )方法接着就会返回 true值。
    • 在初次调用 connect( )或最后一次调用finishConnect()之后,连接建立过程已经完成。那么SocketChannel对象的内部状态将被更新到已连接状态, finishConnect( )方法会返回 true值,然后 SocketChannel对象就可以被用来传输数据了。
    • 连接已经建立。那么什么都不会发生,finishConnect()方法会返回true值。假如某个SocketChannel上当前正由一个并发连接, isConnectPending( )方法就会返回 true 值。
  5. isConnected( )
    不阻塞
  6. 当通道处于中间的连接等待(connection-pending)状态时,您只可以调用 finishConnect( )、isConnectPending( )或isConnected( )方法。
  7. 如果尝试异步连接失败,那么下次调用finishConnect()方法会产生一个适当的经检查的异常以指出问题的性质。通道然后就会被关闭并将不能被连接或再次使用.
  8. connect( )和 finishConnect()方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。如果此情形下您有疑问或不能承受一个读或写操作在某个通道上阻塞,请用isConnected()方法测试一下连接状态。

DatagramChannel

TCP/IP 这样面向流的的协议为了在包导向的互联网基础设施上维护流语义必然会产生巨大的开销,并且流隐喻不能适用所有的情形。数据报的吞吐量要比流协议高很多, 并且数据报可以做很多流无法完成的事情。

DatagramChannel的原型:

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
public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
// This is a partial API listing
public static DatagramChannel open( ) throws IOException
public abstract DatagramSocket socket( );
public abstract DatagramChannel connect (SocketAddress remote)
throws IOException;
public abstract boolean isConnected( );
public abstract DatagramChannel disconnect( ) throws IOException;
public abstract SocketAddress receive (ByteBuffer dst)
throws IOException;107
public abstract int send (ByteBuffer src, SocketAddress target)
public abstract int read (ByteBuffer dst) throws IOException;
public abstract long read (ByteBuffer [] dsts) throws IOException;
public abstract long read (ByteBuffer [] dsts, int offset,
int length)
throws IOException;
public abstract int write (ByteBuffer src) throws IOException;
public abstract long write(ByteBuffer[] srcs) throws IOException;
public abstract long write(ByteBuffer[] srcs, int offset,
int length)
throws IOException;
}

  1. DatagramChannel 对象既可以充当服务器(监听者)也可以充当客户端(发送者)。
    • 如果您希望新创建的通道负责监听,那么通道必须首先被绑定到一个端口或地址/端口组合上。
    • 不论通道是否绑定,所有发送的包都含有 DatagramChannel 的源地址(带端口号)。
    • 未绑定的 DatagramChannel可以接收发送给它的端口的包,通常是来回应该通道之前发出的一个包。通道绑定了端口或者发送了数据包之后就有了相应端口,否则没有端口,也就不能接收数据。
  2. 与面向流的的 socket 不同, DatagramChannel 可以发送单独的数据报给不同的目的地址。同样, DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)。
  3. receive()
    receive( )方法将下次将传入的数据报的数据净荷复制到预备好的ByteBuffer中并返回一个SocketAddress对象以指出数据来源。
    • 如果通道处于阻塞模式, receive( )可能无限期地休眠直到有包到达。
    • 如果是非阻塞模式,当没有可接收的包时则会返回 null。
    • 假如您提供的 ByteBuffer 没有足够的剩余空间来存放您正在接收的数据包,没有被填充的字节都会被悄悄地丢弃
  4. send()
    如果 DatagramChannel 对象处于阻塞模式,调用线程可能会休眠直到数据报被加入传输队列。
    发送数据报是一个全有或全无(all-or-nothing)的行为。
    • 如果通道是非阻塞的,返回值要么是字节缓冲区的字节数,要么是“0”。
    • 如果传输队列没有足够空间来承载整个数据报,那么什么内容都不会被发送。
  5. connect()语义
    • 将 DatagramChannel 置于已连接的状态可以使除了它所“连接”到的地址之外的任何其他源地址的数据报被忽略。这是很有帮助的,因为不想要的包都已经被网络层丢弃了,从而避免了使用代码来接收、检查然后丢弃包的麻烦。
    • 当 DatagramChannel 已连接时,使用同样的令牌,您不可以发送包到除了指定给connect()方法的目的地址以外的任何其他地址。试图一定要这样做的话会导致一个 SecurityException 异常。
    • 已连接通道会发挥作用的使用场景之一是一个客户端/服务器模式、使用 UDP 通讯协议的实时游戏。每个客户端都只和同一台服务器进行会话而希望忽视任何其他来源地数据包。将客户端的DatagramChannel 实例置于已连接状态可以减少按包计算的总开销(因为不需要对每个包进行安全检查)和剔除来自欺骗玩家的假包。服务器可能也想要这样做,不过需要每个客户端都有一个DatagramChannel 对象。
    • DatagramChannel 没有单独的 finishConnect( )方法。我们可以使用isConnected()方法来测试一个数据报通道的连
      接状态。
    • DatagramChannel 对象可以任意次数地进行连接或断开连接。每次连接都可以到一个不同的远程地址。
      调用 disconnect( )方法可以配置通道,以便它能再次接收来自安全管理器(如果已安装)所允许的任意远程地址的数据或发送数据到这些地址上。
    • 当一个 DatagramChannel 处于已连接状态时,发送数据将不用提供目的地址而且接收时的源地址也是已知的。这意味着DatagramChannel已连接时可以使用常规的 read( )和 write( )方法,包括scatter/gather形式的读写来组合或分拆包的数据。
    • read( )方法返回读取字节的数量,如果通道处于非阻塞模式的话这个返回值可能是“0”。write( )方法的返回值同 send( )方法一致:要么返回您的缓冲区中的字节数量,要么返回“0”(如果由于通道处于非阻塞模式而导致数据报不能被发送)。当通道不是已连接状态时调用 read( )或write( )方法,都将产生 NotYetConnectedException 异常。

管道

Pipe 类创建一对提供环回机制的 Channel 对象。这两个通道的远端是连接起来的,以便任何写在 SinkChannel 对象上的数据都能出现在 SourceChannel 对象上。

pipe-class

Pipe原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
package java.nio.channels;
public abstract class Pipe
{
public static Pipe open( ) throws IOException
public abstract SourceChannel source( );
public abstract SinkChannel sink( );119
public static abstract class SourceChannel
extends AbstractSelectableChannel
implements ReadableByteChannel, ScatteringByteChannel
public static abstract class SinkChannel
extends AbstractSelectableChannel
implements WritableByteChannel, GatheringByteChannel
}

  1. Pipe.SourceChannel(管道负责读的一端), Pipe.SinkChannel(管道负责写的一端)。
    这两个通道实例是在 Pipe 对象创建的同时被创建的,可以通过在 Pipe 对象上分别调用 source( )和 sink( )方法来取回。
  2. SinkChannel 和 SourceChannel 都由 AbstractSelectableChannel 引申而来(所以也是从 SelectableChannel 引申而来),这意味着 pipe 通道可以同选择器一起使用.
  3. 管道可以被用来仅在同一个 Java虚拟机内部传输数据(不能在外部)。虽然有更加有效率的方式来在线程之间传输数据,但是使用管道的好处在于封装性。
  4. 生产者线程和用户线程都能被写道通用的Channel API中。根据给定的通道类型,相同的代码可以被用来写数据到一个文件、socket或管道。选择器可以被用来检查管道上的数据可用性,如同在socket通道上使用那样地简单。这样就可以允许单个用户线程使用一个Selector来从多个通道有效地收集数据,并可任意结合网络连接或本地工作线程使用。因此,这些对于可伸缩性、冗余度以及可复用性来说无疑都是意义重大的。
  5. 管道所能承载的数据量是依赖实现的(implementation-dependent)。唯一可保证的是写到SinkChannel中的字节都能按照同样的顺序在 SourceChannel 上重现。

通道工具类

channel-util-table

这些方法返回的包封 Channel 对象可能会也可能不会实现 InterruptibleChannel 接口,它们也可能不是从 SelectableChannel 引申而来。因此,可能无法将这些包封通道同 java.nio.channels包中定义的其他通道类型交换使用。

Zero Copy I: User-Mode Perspective

发表于 2017-06-14 | 分类于 io

Zero Copy I: User-Mode Perspective

标签(空格分隔): io,java


转自:Zero Copy I: User-Mode Perspective

Explaining what is zero-copy functionality for Linux, why it’s useful and where it needs work.
By now almost everyone has heard of so-called zero-copy functionality under Linux, but I often run into people who don’t have a full understanding of the subject. Because of this, I decided to write a few articles that dig into the matter a bit deeper, in the hope of unraveling this useful feature. In this article, we take a look at zero copy from a user-mode application point of view, so gory kernel-level details are omitted intentionally.

What Is Zero-Copy

普通的

To better understand the solution to a problem, we first need to understand the problem itself. Let’s look at what is involved in the simple procedure of a network server d?mon serving data stored in a file to a client over the network. Here’s some sample code:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

Looks simple enough; you would think there is not much overhead with only those two system calls. In reality, this couldn’t be further from the truth. Behind those two calls, the data has been copied at least four times, and almost as many user/kernel context switches have been performed.(Actually this process is much more complicated, but I wanted to keep it simple). To get a better idea of the process involved, take a look at Figure 1. The top side shows context switches, and the bottom side shows copy operations.

zero-copy-1
Figure 1. Copying in Two Sample System Calls

  1. Step one: the read system call causes a context switch from user mode to kernel mode. The first copy is performed by the DMA engine, which reads file contents from the disk and stores them into a kernel address space buffer.

  2. Step two: data is copied from the kernel buffer into the user buffer, and the read system call returns. The return from the call caused a context switch from kernel back to user mode. Now the data is stored in the user address space buffer, and it can begin its way down again.

  3. Step three: the write system call causes a context switch from user mode to kernel mode. A third copy is performed to put the data into a kernel address space buffer again. This time, though, the data is put into a different buffer, a buffer that is associated with sockets specifically.

  4. Step four: the write system call returns, creating our fourth context switch. Independently and asynchronously, a fourth copy happens as the DMA engine passes the data from the kernel buffer to the protocol engine. You are probably asking yourself, “What do you mean independently and asynchronously? Wasn’t the data transmitted before the call returned?” Call return, in fact, doesn’t guarantee transmission; it doesn’t even guarantee the start of the transmission.It simply means the Ethernet driver had free descriptors in its queue and has accepted our data for transmission. There could be numerous packets queued before ours. Unless the driver/hardware implements priority rings or queues, data is transmitted on a first-in-first-out basis. (The forked DMA copy in Figure 1 illustrates the fact that the last copy can be delayed).

As you can see, a lot of data duplication is not really necessary to hold things up. Some of the duplication could be eliminated to decrease overhead and increase performance. As a driver developer, I work with hardware that has some pretty advanced features. Some hardware can bypass the main memory altogether and transmit data directly to another device. This feature eliminates a copy in the system memory and is a nice thing to have, but not all hardware supports it. There is also the issue of the data from the disk having to be repackaged for the network, which introduces some complications. To eliminate overhead, we could start by eliminating some of the copying between the kernel and user buffers.

引入mmap

One way to eliminate a copy is to skip calling read and instead call mmap. For example:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

To get a better idea of the process involved, take a look at Figure 2. Context switches remain the same.

zero-copy-step2
Figure 2. Calling mmap

  1. Step one: the mmap system call causes the file contents to be copied into a kernel buffer by the DMA engine. The buffer is shared then with the user process, without any copy being performed between the kernel and user memory spaces.

  2. Step two: the write system call causes the kernel to copy the data from the original kernel buffers into the kernel buffers associated with sockets.

  3. Step three: the third copy happens as the DMA engine passes the data from the kernel socket buffers to the protocol engine.

By using mmap instead of read, we’ve cut in half the amount of data the kernel has to copy(减小了一次copy,上下文切换次数没有变). this yields reasonably good results when a lot of data is being transmitted. However, this improvement doesn’t come without a price; there are hidden pitfalls when using the mmap+write method. You will fall into one of them when you memory map a file and then call write while another process truncates the same file. Your write system call will be interrupted by the bus error signal SIGBUS, because you performed a bad memory access. The default behavior for that signal is to kill the process and dump core—not the most desirable operation for a network server. There are two ways to get around this problem.

  1. The first way is to install a signal handler for the SIGBUS signal, and then simply call return in the handler. By doing this the write system call returns with the number of bytes it wrote before it got interrupted and the errno set to success. Let me point out that this would be a bad solution, one that treats the symptoms and not the cause of the problem. Because SIGBUS signals that something has gone seriously wrong with the process, I would discourage using this as a solution.

  2. The second solution involves file leasing (which is called “opportunistic locking” in Microsoft Windows) from the kernel. This is the correct way to fix this problem. By using leasing on the file descriptor, you take a lease with the kernel on a particular file. You then can request a read/write lease from the kernel. When another process tries to truncate the file you are transmitting, the kernel sends you a real-time signal, the RT_SIGNAL_LEASE signal. It tells you the kernel is breaking your write or read lease on that file. Your write call is interrupted before your program accesses an invalid address and gets killed by the SIGBUS signal. The return value of the write call is the number of bytes written before the interruption, and the errno will be set to success. Here is some sample code that shows how to get a lease from the kernel:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
    perror("kernel lease set signal");
    return -1;
    }
    /* l_type can be F_RDLCK F_WRLCK */
    if(fcntl(fd, F_SETLEASE, l_type)){
    perror("kernel lease set type");
    return -1;
    }

You should get your lease before mmaping the file, and break your lease after you are done. This is achieved by calling fcntl F_SETLEASE with the lease type of F_UNLCK.

引入Sendfile

In kernel version 2.1, the sendfile system call was introduced to simplify the transmission of data over the network and between two local files.
Introduction of sendfile not only reduces data copying, it also reduces context switches. Use it like this:

sendfile(socket, file, len);

To get a better idea of the process involved, take a look at Figure 3.

step-zero-copy-3
Figure 3. Replacing Read and Write with Sendfile

  1. Step one: the sendfile system call causes the file contents to be copied into a kernel buffer by the DMA engine. Then the data is copied by the kernel into the kernel buffer associated with sockets.

  2. Step two: the third copy happens as the DMA engine passes the data from the kernel socket buffers to the protocol engine.

You are probably wondering what happens if another process truncates the file we are transmitting with the sendfile system call. If we don’t register any signal handlers, the sendfile call simply returns with the number of bytes it transferred before it got interrupted, and the errno will be set to success.

If we get a lease from the kernel on the file before we call sendfile, however, the behavior and the return status are exactly the same. We also get the RT_SIGNAL_LEASE signal before the sendfile call returns.

So far, we have been able to avoid having the kernel make several copies, but we are still left with one copy. Can that be avoided too? Absolutely, with a little help from the hardware.

引入支持Scatter的sendFile

To eliminate all the data duplication done by the kernel, we need a network interface that supports gather operations. This simply means that data awaiting transmission doesn’t need to be in consecutive memory; it can be scattered through various memory locations. In kernel version 2.4, the socket buffer descriptor was modified to accommodate those requirements—what is known as zero copy under Linux. This approach not only reduces multiple context switches, it also eliminates data duplication done by the processor. For user-level applications nothing has changed, so the code still looks like this:

sendfile(socket, file, len);
To get a better idea of the process involved, take a look at Figure 4.
zero-copy-4

Figure 4. Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.

  1. Step one: the sendfile system call causes the file contents to be copied into a kernel buffer by the DMA engine.

  2. Step two: no data is copied into the socket buffer. Instead, only descriptors with information about the whereabouts and length of the data are appended to the socket buffer. The DMA engine passes data directly from the kernel buffer to the protocol engine, thus eliminating the remaining final copy.

Because data still is actually copied from the disk to the memory and from the memory to the wire, some might argue this is not a true zero copy.
This is zero copy from the operating system standpoint, though, because the data is not duplicated between kernel buffers.
When using zero copy, other performance benefits can be had besides copy avoidance, such as fewer context switches, less CPU data cache pollution and no CPU checksum calculations.

practice

Now that we know what zero copy is, let’s put theory into practice and write some code. You can download the full source code from www.xalien.org/articles/source/sfl-src.tgz. To unpack the source code, type tar -zxvf sfl-src.tgz at the prompt. To compile the code and create the random data file data.bin, run make.

Looking at the code starting with header files:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* sfl.c sendfile example program
Dragan Stancevic <
header name function / variable
-------------------------------------------------*/
#include <stdio.h> /* printf, perror */
#include <fcntl.h> /* open */
#include <unistd.h> /* close */
#include <errno.h> /* errno */
#include <string.h> /* memset */
#include <sys/socket.h> /* socket */
#include <netinet/in.h> /* sockaddr_in */
#include <sys/sendfile.h> /* sendfile */
#include <arpa/inet.h> /* inet_addr */
#define BUFF_SIZE (10*1024) /* size of the tmp buffer */

Besides the regular and required for basic socket operation, we need a prototype definition of the sendfile system call. This can be found in the server flag:

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
/* are we sending or receiving */
if(argv[1][0] == 's') is_server++;
/* open descriptors */
sd = socket(PF_INET, SOCK_STREAM, 0);
if(is_server) fd = open("data.bin", O_RDONLY);
The same program can act as either a server/sender or a client/receiver. We have to check one of the command-prompt parameters, and then set the flag is_server to run in sender mode. We also open a stream socket of the INET protocol family. As part of running in server mode we need some type of data to transmit to a client, so we open our data file. We are using the system call sendfile to transmit data, so we don't have to read the actual contents of the file and store it in our program memory buffer. Here's the server address:
/* clear the memory */
memset(&sa, 0, sizeof(struct sockaddr_in));
/* initialize structure */
sa.sin_family = PF_INET;
sa.sin_port = htons(1033);
sa.sin_addr.s_addr = inet_addr(argv[2]);
We clear the server address structure and assign the protocol family, port and IP address of the server. The address of the server is passed as a command-line parameter. The port number is hard coded to unassigned port 1033. This port number was chosen because it is above the port range requiring root access to the system.
Here is the server execution branch:
if(is_server){
int client; /* new client socket */
printf("Server binding to [%s]\n", argv[2]);
if(bind(sd, (struct sockaddr *)&sa,
sizeof(sa)) < 0){
perror("bind");
exit(errno);
}
As a server, we need to assign an address to our socket descriptor. This is achieved by the system call bind, which assigns the socket descriptor (sd) a server address (sa):
if(listen(sd,1) < 0){
perror("listen");
exit(errno);
}

Because we are using a stream socket, we have to advertise our willingness to accept incoming connections and set the connection queue size. I’ve set the backlog queue to 1, but it is common to set the backlog a bit higher for established connections waiting to be accepted. In older versions of the kernel, the backlog queue was used to prevent syn flood attacks. Because the system call listen changed to set parameters for only established connections, the backlog queue feature has been deprecated for this call. The kernel parameter tcp_max_syn_backlog has taken over the role of protecting the system from syn flood attacks:

1
2
3
4
if((client = accept(sd, NULL, NULL)) < 0){
perror("accept");
exit(errno);
}

The system call accept creates a new connected socket from the first connection request on the pending connections queue. The return value from the call is a descriptor for a newly created connection; the socket is now ready for read, write or poll/select system calls:

1
2
3
4
5
6
7
if((cnt = sendfile(client,fd,&off,
BUFF_SIZE)) < 0){
perror("sendfile");
exit(errno);
}
printf("Server sent %d bytes.\n", cnt);
close(client);

A connection is established on the client socket descriptor, so we can start transmitting data to the remote system. We do this by calling the sendfile system call, which is prototyped under Linux in the following manner:

1
2
3
extern ssize_t
sendfile (int __out_fd, int __in_fd, off_t *offset,
size_t __count) __THROW;

The first two parameters are file descriptors. The third parameter points to an offset from which sendfile should start sending data. The fourth parameter is the number of bytes we want to transmit. In order for the sendfile transmit to use zero-copy functionality, you need memory gather operation support from your networking card. You also need checksum capabilities for protocols that implement checksums, such as TCP or UDP. If your NIC is outdated and doesn’t support those features, you still can use sendfile to transmit files. The difference is the kernel will merge the buffers before transmitting them.

others

Portability Issues

  1. One of the problems with the sendfile system call, in general, is the lack of a standard implementation, as there is for the open system call.Sendfile implementations in Linux, Solaris or HP-UX are quite different. This poses a problem for developers who wish to use zero copy in their network data transmission code.

    • One of the implementation differences is Linux provides a sendfile that defines an interface for transmitting data between two file descriptors (file-to-file) and (file-to-socket).
    • HP-UX and Solaris, on the other hand, can be used only for file-to-socket submissions.
  2. The second difference is Linux doesn’t implement vectored transfers. Solaris sendfile and HP-UX sendfile have extra parameters that eliminate overhead associated with prepending headers to the data being transmitted.

Looking Ahead

The implementation of zero copy under Linux is far from finished and is likely to change in the near future. More functionality should be added. For example, the sendfile call doesn’t support vectored transfers, and servers such as Samba and Apache have to use multiple sendfile calls with the TCP_CORK flag set. This flag tells the system more data is coming through in the next sendfile calls. TCP_CORK also is incompatible with TCP_NODELAY and is used when we want to prepend or append headers to the data. This is a perfect example of where a vectored call would eliminate the need for multiple sendfile calls and delays mandated by the current implementation.

One rather unpleasant limitation in the current sendfile is it cannot be used when transferring files greater than 2GB. Files of such size are not all that uncommon today, and it’s rather disappointing having to duplicate all that data on its way out. Because both sendfile and mmap methods are unusable in this case, a sendfile64 would be really handy in a future kernel version.

Conclusion

Despite some drawbacks, zero-copy sendfile is a useful feature, and I hope you have found this article informative enough to start using it in your programs. If you have a more in-depth interest in the subject, keep an eye out for my second article, titled “Zero Copy II: Kernel Perspective”, where I will dig a bit more into the kernel internals of zero copy.

FileChannel

发表于 2017-06-12

FileChannel主要进行文件IO的操作,下面主要分析write()的整个过程,read()和write()是相同的。

概要 

write()/read()时:

  • 如果传入的是DirectBuffer,则不需要进行数据复制,最后通过write系统调用完成IO。
  • 如果传入的是非DirectBuffer,则先从本线程的DirectBuffer池中得到一个满足(size能容纳所有数据)的Buffer,之后进行数据复制,并使用DirectBuffer参与write系统调用完成IO。

对于第二种情况,数据复制会引起效率。如果业务代码再不重复利用所传入的非DirectBuffer,则会增加GC频率。
第二种情况,线程里有一个DirectBuffer池,使得DirectBuffer可以重复利用。这样不仅可以减小DirectBuffer的新建和释放开销,而且可以减小GC频率。这给我们以借鉴,我们在编写业务代码时页应该这样处理。

下面分析具体的源码。

源码

sun.nio.ch.FileChannelImpl.write()

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
public int write(ByteBuffer src) throws IOException {
ensureOpen();
if (!writable)
throw new NonWritableChannelException();
synchronized (positionLock) {
int n = 0;
int ti = -1;
Object traceContext = IoTrace.fileWriteBegin(path);
try {
begin();
ti = threads.add();
if (!isOpen())
return 0;
do {
//这里写
n = IOUtil.write(fd, src, -1, nd);
} while ((n == IOStatus.INTERRUPTED) && isOpen());
return IOStatus.normalize(n);
} finally {
threads.remove(ti);
end(n > 0);
IoTrace.fileWriteEnd(traceContext, n > 0 ? n : 0);
assert IOStatus.check(n);
}
}
}

sun.nio.ch.IOUtil.write()

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
static int write(FileDescriptor fd, ByteBuffer src, long position,
NativeDispatcher nd)
throws IOException
{
//如果是DirectBuffer
if (src instanceof DirectBuffer)
return writeFromNativeBuffer(fd, src, position, nd);
// Substitute a native buffer
int pos = src.position();
int lim = src.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
//从本线程所缓存的临时DirectBuffer池中得到一个size满足的Buffer(如果没有符合的,new一个)
ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
try {
//数据copy
bb.put(src);
bb.flip();
// Do not update src until we see how many bytes were written
src.position(pos);
//真正的写
int n = writeFromNativeBuffer(fd, bb, position, nd);
if (n > 0) {
// now update src
src.position(pos + n);
}
return n;
} finally {
//将本次使用的DirectBuffer重新还给DirectBuffer池
Util.offerFirstTemporaryDirectBuffer(bb);
}
}

接上面,sun.nio.ch.Util.getTemporaryDirectBuffer()。
下面代码说明了每个线程维持了一个DirectBuffer池,当使用的是Heap类型的Buffer进行IO时,需要先从池中得到一个DirectBuffer,之后还有进行数据复制等,并使用DirectBuffer参与系统调用完成IO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Returns a temporary buffer of at least the given size
*/
static ByteBuffer getTemporaryDirectBuffer(int size) {
BufferCache cache = bufferCache.get();
ByteBuffer buf = cache.get(size);
if (buf != null) {
return buf;
} else {
// No suitable buffer in the cache so we need to allocate a new
// one. To avoid the cache growing then we remove the first
// buffer from the cache and free it.
if (!cache.isEmpty()) {
buf = cache.removeFirst();
free(buf);
}
return ByteBuffer.allocateDirect(size);
}
}

sun.nio.ch.IOUtil.writeFromNativeBuffer()

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
private static int writeFromNativeBuffer(FileDescriptor fd, ByteBuffer bb,
long position, NativeDispatcher nd)
throws IOException
{
int pos = bb.position();
int lim = bb.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
int written = 0;
if (rem == 0)
return 0;
if (position != -1) {
//不从Buffer头开始写
//sun.nio.ch.FileDispatcherImpl.writeFromNativeBuffer()
written = nd.pwrite(fd,
((DirectBuffer)bb).address() + pos,
rem, position);
} else {
//从Buffer头开始
//sun.nio.ch.FileDispatcherImpl.writeFromNativeBuffer()
written = nd.write(fd, ((DirectBuffer)bb).address() + pos, rem);
}
if (written > 0)
bb.position(pos + written);
return written;
}

sun.nio.ch.FileDispatcherImpl.writeFromNativeBuffer()

1
2
3
4
5
int write(FileDescriptor fd, long address, int len) throws IOException {
//native调用
//源码在openjdk/jdk/src/solaris/native/sun/nio/ch/FileDispatcherImpl.c里面
return write0(fd, address, len);
}

接上面,native调用:

1
2
3
4
5
6
7
8
9
10
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_write0(JNIEnv *env, jclass clazz,
jobject fdo, jlong address, jint len)
{
jint fd = fdval(env, fdo);
void *buf = (void *)jlong_to_ptr(address);
//write的原型是
  //extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
return convertReturnVal(env, write(fd, buf, len), JNI_FALSE);
}

所以最后,通过系统调用完成IO.操作系统调用接口的原型是:

1
2
3
4
5
/* Write N bytes of BUF to FD. Return the number written, or -1.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;

openjdk之编译经常出现的问题

发表于 2017-06-12 | 分类于 jvm

openjdk编译过程中,因为系统环境和openjdk版本等问题,会出现各种问题。本文主要列出两个经常出现的问题及解决办法。

time is more than 10 years from present

1
2
3
4
5
Error: time is more than 10 years from present: 1136059200000
Java.lang.RuntimeException: time is more than 10 years from present: 1136059200000
at build.tools.generatecurrencydata.GenerateCurrencyData.makeSpecialCaseEntry(GenerateCurrencyData.java:285)
at build.tools.generatecurrencydata.GenerateCurrencyData.buildMainAndSpecialCaseTables(GenerateCurrencyData.java:225)
at build.tools.generatecurrencydata.GenerateCurrencyData.main(GenerateCurrencyData.java:154)

解决办法:

修改CurrencyData.properties(路径:jdk/src/share/classes/java/util/CurrencyData.properties)

修改108行
AZ=AZM;2009-12-31-20-00-00;AZN
修改381行
MZ=MZM;2009-06-30-22-00-00;MZN
修改443行
RO=ROL;2009-06-30-21-00-00;RON
修改535行
TR=TRL;2009-12-31-22-00-00;TRY
修改561行
VE=VEB;2009-01-01-04-00-00;VEF

OS is not supported: Linux … 4.0.0-1-amd64 …

openjdk在编译时检查linux的内核版本,之前的检查代码没有检查4.x版本(那个时候还没有这个版本的内核),导致出错。我们只需要在对应的检查代码里加上即可。

在文件hotspot/make/linux/Makefile中,修改如下:

1
2
-SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 3%
+SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 3% 4%

openjdk8之编译和debug

发表于 2017-06-12 | 分类于 jvm

系统环境为ubuntu 16.04,uname -a:

1
Linux ddy-Aspire-V5-573G 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

在本文中,要编译的openjdk版本为:openjdk-8u40-src-b25-10_feb_2015。
尝试了编译openjdk-8-src-b132-03_mar_2014,但是失败。网上说,因为ubuntu16.04较新,但是该版本的JDK较老,所以失败。

下面说明编译和debug过程。

make版本

OpenJDK8可以使用”config && make”编译构建,不再使用Ant和ALT_ *环境变量来配置构建。
不过需要GNU make 3.81或更新的版本

安装引导JDK

我使用的引导JDK是jdk-7u76-linux-x64。

1
2
3
java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)

安装编译工具类库:

安装gcc、g++、make等
sudo apt-get install build-essential
安装XRender
sudo apt-get install libxrender-dev
sudo apt-get install xorg-dev
安装alsa
sudo apt-get install libasound2-dev
Cups
sudo apt-get install libcups2-dev
安装零碎的工具包
sudo apt-get install gawk zip libxtst-dev libxi-dev libxt-dev

建立编译脚本

–with-boot-jdk:指定引导JDK所在目录,以防其他安装的JDK影响(本机上以前安装了JDK8,并配置了JAVA_HOME指向JDK8);
–with-target-bits:指定编译64位系统的JDK;

为可以进行源码调试,再指定下面三个参数:
–with-debug-level=slowdebug:指定可以生成最多的调试信息;
–enable-debug-symbols ZIP_DEBUGINFO_FILES=0:生成调试的符号信息,并且不压缩;
在openjdk目录下新建build.sh,内容如下:

1
2
3
cd openjdk
bash ./configure --with-target-bits=64 --with-boot-jdk=/usr/java/jdk1.7.0_80/ --with-debug-level=slowdebug --enable-debug-symbols ZIP_DEBUGINFO_FILES=0
make all ZIP_DEBUGINFO_FILES=0

编译

执行./build.sh
编译完成是这样的:
openjdk8-compile-success.png

用GDB测试是否能debug

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
46
47
48
49
50
51
52
53
54
55
ddy@ddy-Aspire-V5-573G ~/openjdk-compile/openjdk-8u40-src-b25-10_feb_2015/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin $ ./java -version
openjdk version "1.8.0-internal-debug"
OpenJDK Runtime Environment (build 1.8.0-internal-debug-ddy_2017_06_11_23_26-b00)
OpenJDK 64-Bit Server VM (build 25.40-b25-debug, mixed mode)
ddy@ddy-Aspire-V5-573G ~/openjdk-compile/openjdk-8u40-src-b25-10_feb_2015/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin $ export CLASSPATH=.:/home/
ddy/java_src
ddy@ddy-Aspire-V5-573G ~/openjdk-compile/openjdk-8u40-src-b25-10_feb_2015/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin $ gdb --args java FileChann
elTest
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from java...done.
(gdb) break init.cpp:95
No source file named init.cpp.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (init.cpp:95) pending.
(gdb) run
Starting program: /home/ddy/openjdk-compile/openjdk-8u40-src-b25-10_feb_2015/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java FileChannelTest
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7fc8700 (LWP 9311)]
[Switching to Thread 0x7ffff7fc8700 (LWP 9311)]
Thread 2 "java" hit Breakpoint 1, init_globals ()
at /home/ddy/openjdk-compile/openjdk-8u40-src-b25-10_feb_2015/openjdk/hotspot/src/share/vm/runtime/init.cpp:95
95 jint init_globals() {
(gdb) l
90 chunkpool_init();
91 perfMemory_init();
92 }
93
94
95 jint init_globals() {
96 HandleMark hm;
97 management_init();
98 bytecodes_init();
99 classLoader_init();
(gdb) quit
A debugging session is active.
Inferior 1 [process 9307] will be killed.
Quit anyway? (y or n) y
ddy@ddy-Aspire-V5-573G ~/openjdk-compile/openjdk-8u40-src-b25-10_feb_2015/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin $

openjdk之编译经常出现的问题
openjdk7的编译和debug
编译主要参考:ubuntu14.04 编译openjdk7
debug主要参考:CentOS上编译OpenJDK8源码 以及 在eclipse上调试HotSpot虚拟机源码

openjdk7之编译和debug

发表于 2017-06-11 | 分类于 jvm

为了更好的学习JDK、HotSpot等源码,需要能debug JDK、HotSpot等源码。本文主要讲述,怎么编译openjdk并debug相关源码。
在本文中,要编译的openjdk:openjdk-7u40-fcs-src-b43-26_aug_2013.zip
系统环境为ubuntu 16.04,uname -a:

1
Linux ddy-Aspire-V5-573G 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

编译

  1. 下载源代码
    openjdk的源码可以通过hg方式下载。  
    也可以从此处下载:openjdk源码  
  2. 安装引导JDK
    因为JDK中有很多代码是Java自身实现的,所以还需要一个已经安装在本机上可用的JDK,叫做“Bootstrap JDK”。我所选用的Bootstarp JDK是JDK1.6.0_45。  

    1
    2
    3
    java version "1.6.0_45"
    Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
    Java HotSpot(TM) Server VM (build 20.45-b01, mixed mode)

    JDK1.6.0_45下载地址:jdk1.6.0_45.tar.gz

  3. 安装编译前的依赖环境
    安装gcc、g++、make等  
    sudo apt-get install build-essential  
    安装ant 1.7以上   
    sudo apt-get install ant  
    安装XRender   
    sudo apt-get install libxrender-dev   
    sudo apt-get install xorg-dev   
    安装alsa   
    sudo apt-get install libasound2-dev  
    Cups   
    sudo apt-get install libcups2-dev   
    安装零碎的工具包   
    sudo apt-get install gawk zip libxtst-dev libxi-dev libxt-dev  
  4. 配置编译脚本  
    将你的openjdk解压后,并进入该文件夹。比如我的是在/home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk
    下。新建一个build.sh,并添加如下内容:  
    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
    export LANG=C
    #将一下两项设置为你的BootstrapJDK安装目录
    export ALT_BOOTDIR=/home/ddy/jdk1.6.0_45
    export ALT_JDK_IMPORT_PATH=/home/ddy/jdk1.6.0_45
    #允许自动下载依赖包
    export ALLOW_DOWNLOADS=true
    #使用预编译头文件,以提升便以速度
    export USE_PRECOMPILED_HEADER=true
    #要编译的内容,我只选择了LANGTOOLS、HOTSPOT以及JDK
    export BUILD_LANGTOOLS=true
    export BUILD_JAXP=false
    export BUILD_JAXWS=false
    export BUILD_CORBA=false
    export BUILD_HOSTPOT=true
    export BUILD_JDK=true
    #要编译的版本
    export SKIP_DEBUG_BUILD=false
    export SKIP_FASTDEBUG_BUILD=true
    export DEBUG_NAME=debug
    #避免javaws和浏览器Java插件等的build
    BUILD_DEPLOY=false
    #不build安装包
    BUILD_INSTALL=false
    #包含全部的调试信息
    export ENABLE_FULL_DEBUG_SYMBOLS=1
    #调试信息是否压缩,如果配置为1,libjvm.debuginfo会被压缩成libjvm.diz,将不能被debug。
    export ZIP_DEBUGINFO_FILES=0
    #用于编译线程数
    export HOTSPOT_BUILD_JOBS=3
    #设置存放编译结果的目录
    #export ALT_OUTPUTDIR=/home/ddy/openjdk/7/build
    unset CLASSPATH
    unset JAVA_HOME
    make sanity
    DEBUG_BINARIES=true make 2>&1

5.开始编译
在openjdk目录下,运行build.sh

1
2
chmod +x build.sh
./build.sh

最后编译耗时将近2分钟。编译完成输出如下信息:
![compile-success][4]
此时openjdk就编译完成了,编译的输出在``/home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk/build/``下。

进入/home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk/build/linux-amd64-debug/j2re-image/bin n,执行
./java -version
输出的java版本信息将是带着你的机器用户名,我的输出是:

1
2
3
openjdk version "1.7.0-internal-debug"
OpenJDK Runtime Environment (build 1.7.0-internal-debug-ddy_2017_06_10_22_30-b00)
OpenJDK 64-Bit Server VM (build 24.0-b56-jvmg, mixed mode)

debug

编译完成了之后,就可以对JDK源码和HotSpot源码等进行debug了。

JDK

首先是JDK源码,在build目录下编译生成的jdk里面的jar包都是可编译的了,直接把eclipse的JDK或者JRE换成编译成功的JDK或者JRE即可。

HotSpot

注意,如果不能进入断点,出现以下类似信息:
Missing separate debuginfo for/root/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
是因为在编译时因为编译配置项不正确而没有生成调试的符号信息,或生成后被压缩为”libjvm.diz”了,所以无法找到。如果是因为没有编译时没有生成调试信息,需要修改编译配置并重新编译。对于被压缩的情况,可以去到”libjvm.so”所在目录

  • 然后解压:unzip libjvm.diz
  • 解压出来:libjvm.debuginfo

如果在编译时,把配置信息修改如下,则不会出现不能上述问题。

1
2
3
4
#包含全部的调试信息
export ENABLE_FULL_DEBUG_SYMBOLS=1
#调试信息是否压缩,如果配置为1,libjvm.debuginfo会被压缩成libjvm.diz,将不能被debug。
export ZIP_DEBUGINFO_FILES=0

使用GDB

参考:CentOS上编译OpenJDK8源码 以及 在eclipse上调试HotSpot虚拟机源码

使用eclipse

  1. 生成要运行的JAVA类
    首先在/home/ddy/src/java-src目录下建立要运行的FileChannelTest.java,这个类在写文件时调用了JDK的native方法,其代码如下:  

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    public class FileChannelTest {
    public static void main(String[] args) throws IOException {
    FileChannel channel=new RandomAccessFile("test.txt","rw").getChannel();
    ByteBuffer buffer=ByteBuffer.allocate(1000);
    channel.write(buffer);
    }
    }

    然后对其进行编译,运行:  

    1
    2
    3
    4
    ddy@ddy-Aspire-V5-573G ~ $ cd src/java-src/
    ddy@ddy-Aspire-V5-573G ~/src/java-src $ pwd
    /home/ddy/src/java-src
    ddy@ddy-Aspire-V5-573G ~/src/java-src $ /home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk/build/linux-amd64-debug/j2sdk-image/bin/javac FileChannelTest.java
  2. 下载eclipse,安装C/C++插件 
    到官网选择一个合适的eclipse下载,因为本人主要进行JAVA开发,所以下载的是j2EE版本,这个版本没有C/C++的功能。不过可以安装插件使其支持C/C++功能。”help -> Eclipse Maketplace”,搜索”c++”找到Eclipse C++ IDE..安装。安装后,就可以转到C++开发视图界面了。

  3. 导入hotspot工程
    File-> New -> Makefile Project With Existing Code
    在界面中:
    Project Name:openjdk(这个可以自己选择)
    Existing Code Location:/root/openjdk
    Toolchain:选Linux GCC,然后按Finish.
  4. 配置源码调试
    1. 右键工程 -> Debug As -> Debug Configurations -> 右键左边的C/C++ Application -> New -> 进入Main选项卡;
      在选项卡中:
      Project: openjdk(选择导入的openjdk工程)
      C/C++ Application: /home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk/build/linux-amd64-debug/j2sdk-image/bin/java(编译生成的openjdk虚拟机入口)
      Disable auto build:因为不再在eclipse里面编译hotspot源码,所以这里选上它;
    2. 然后切换到Arguments选项卡, 输入Java参数, 这里填上 “FileChannelTest”也就是我们要执行的JAVA程序。
    3. 然后切换到Environment选项卡, 添加变量:
      JAVA_HOME=/home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk/build/linux-amd64-debug/j2sdk-image(编译生成JDK所在目录)
      CLASSPATH=.:/home/ddy/src/java-src (FileChannelTest.java文件所在目录)
      点击下面的Apply保存;
    4. 断点Debug
       下面分别在源码上打两个断点:
      • init.cpp(/home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk/hotspot/src/share/vm/runtime目录下) 95行
      • FileDispatchImpl.c(/home/ddy/openjdk-compile/openjdk-7u40-fcs-b43-26/openjdk/jdk/src/solaris/native/sun/nio/ch目录下) 107行
  5. 然后开始debug。
    首先是第一个断点:
    init.cpp-95.png

    F8进行到下一个断电点:

    FileDispatcherImpl-write.c.png

    从上图可以看到,FileChannel.write()最后调用的是write()操作系统调用。
    所以,大家现在可以随便debug HotSpot的源码和JDK的native源码了。酷!

参考资料

openjdk之编译经常出现的问题
openjdk8的编译和debug
编译主要参考:ubuntu14.04 编译openjdk7
debug主要参考:CentOS上编译OpenJDK8源码 以及 在eclipse上调试HotSpot虚拟机源码

Buffer

发表于 2017-06-08 | 分类于 java

本机环境:
Linux 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

Buffer

Buffer的类图如下:

Buffer类图

除了Boolean,其他基本数据类型都有对应的Buffer,但是只有ByteBuffer才能和Channel交互。只有ByteBuffer才能产生Direct的buffer,其他数据类型的Buffer只能产生Heap类型的Buffer。ByteBuffer可以产生其他数据类型的视图Buffer,如果ByteBuffer本身是Direct的,则产生的各视图Buffer也是Direct的。

Direct和Heap类型Buffer的本质

首选说说JVM是怎么进行IO操作的。

JVM在需要通过操作系统调用完成IO操作,比如可以通过read系统调用完成文件的读取。read的原型是:ssize_t read(int fd,void *buf,size_t nbytes),和其他的IO系统调用类似,一般需要缓冲区作为其中一个参数,该缓冲区要求是连续的。

Buffer分为Direct和Heap两类,下面分别说明这两类buffer。

Heap

Heap类型的Buffer存在于JVM的堆上,这部分内存的回收与整理和普通的对象一样。Heap类型的Buffer对象都包含一个对应基本数据类型的数组属性(比如:final **[] hb),数组才是Heap类型Buffer的底层缓冲区。
但是Heap类型的Buffer不能作为缓冲区参数直接进行系统调用,主要因为下面两个原因。

  • JVM在GC时可能会移动缓冲区(复制-整理),缓冲区的地址不固定。
  • 系统调用时,缓冲区需要是连续的,但是数组可能不是连续的(JVM的实现没要求连续)。

所以使用Heap类型的Buffer进行IO时,JVM需要产生一个临时Direct类型的Buffer,然后进行数据复制,再使用临时Direct的Buffer作为参数进行操作系统调用。这造成很低的效率,主要是因为两个原因:

  • 需要把数据从Heap类型的Buffer里面复制到临时创建的Direct的Buffer里面。
  • 可能产生大量的Buffer对象,从而提高GC的频率。所以在IO操作时,可以通过重复利用Buffer进行优化。

Direct

Direct类型的buffer,不存在于堆上,而是JVM通过malloc直接分配的一段连续的内存,这部分内存成为直接内存,JVM进行IO系统调用时使用的是直接内存作为缓冲区。
-XX:MaxDirectMemorySize,通过这个配置可以设置允许分配的最大直接内存的大小(MappedByteBuffer分配的内存不受此配置影响)。
直接内存的回收和堆内存的回收不同,如果直接内存使用不当,很容易造成OutOfMemoryError。JAVA没有提供显示的方法去主动释放直接内存,sun.misc.Unsafe类可以进行直接的底层内存操作,通过该类可以主动释放和管理直接内存。同理,也应该重复利用直接内存以提高效率。

MappedByteBuffer和DirectByteBuffer之间的关系

This is a little bit backwards: By rights MappedByteBuffer should be a subclass of DirectByteBuffer, but to keep the spec clear and simple, and for optimization purposes, it’s easier to do it the other way around.This works because DirectByteBuffer is a package-private class.(本段话摘自MappedByteBuffer的源码)

实际上,MappedByteBuffer属于映射buffer(自己看看虚拟内存),但是DirectByteBuffer只是说明该部分内存是JVM在直接内存区分配的连续缓冲区,并不一是映射的。也就是说MappedByteBuffer应该是DirectByteBuffer的子类,但是为了方便和优化,把MappedByteBuffer作为了DirectByteBuffer的父类。另外,虽然MappedByteBuffer在逻辑上应该是DirectByteBuffer的子类,而且MappedByteBuffer的内存的GC和直接内存的GC类似(和堆GC不同),但是分配的MappedByteBuffer的大小不受-XX:MaxDirectMemorySize参数影响。
MappedByteBuffer封装的是内存映射文件操作,也就是只能进行文件IO操作。MappedByteBuffer是根据mmap产生的映射缓冲区,这部分缓冲区被映射到对应的文件页上,属于直接内存在用户态,通过MappedByteBuffer可以直接操作映射缓冲区,而这部分缓冲区又被映射到文件页上,操作系统通过对应内存页的调入和调出完成文件的写入和写出。

MappedByteBuffer

通过FileChannel.map(MapMode mode,long position, long size)得到MappedByteBuffer,下面结合源码说明MappedByteBuffer的产生过程。

FileChannel.map的源码:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException
{
ensureOpen();
if (position < 0L)
throw new IllegalArgumentException("Negative position");
if (size < 0L)
throw new IllegalArgumentException("Negative size");
if (position + size < 0)
throw new IllegalArgumentException("Position + size overflow");
//最大2G
if (size > Integer.MAX_VALUE)
throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE");
int imode = -1;
if (mode == MapMode.READ_ONLY)
imode = MAP_RO;
else if (mode == MapMode.READ_WRITE)
imode = MAP_RW;
else if (mode == MapMode.PRIVATE)
imode = MAP_PV;
assert (imode >= 0);
if ((mode != MapMode.READ_ONLY) && !writable)
throw new NonWritableChannelException();
if (!readable)
throw new NonReadableChannelException();
long addr = -1;
int ti = -1;
try {
begin();
ti = threads.add();
if (!isOpen())
return null;
//size()返回实际的文件大小
//如果实际文件大小不符合,则增大文件的大小,文件的大小被改变,文件增大的部分默认设置为0。
if (size() < position + size) { // Extend file size
if (!writable) {
throw new IOException("Channel not open for writing " +
"- cannot extend file to required size");
}
int rv;
do {
//增大文件的大小
rv = nd.truncate(fd, position + size);
} while ((rv == IOStatus.INTERRUPTED) && isOpen());
}
//如果要求映射的文件大小为0,则不调用操作系统的mmap调用,只是生成一个空间容量为0的DirectByteBuffer
//并返回
if (size == 0) {
addr = 0;
// a valid file descriptor is not required
FileDescriptor dummy = new FileDescriptor();
if ((!writable) || (imode == MAP_RO))
return Util.newMappedByteBufferR(0, 0, dummy, null);
else
return Util.newMappedByteBuffer(0, 0, dummy, null);
}
//allocationGranularity的大小在我的系统上是4K
//页对齐,pagePosition为第多少页
int pagePosition = (int)(position % allocationGranularity);
//从页的最开始映射
long mapPosition = position - pagePosition;
//因为从页的最开始映射,增大映射空间
long mapSize = size + pagePosition;
try {
// If no exception was thrown from map0, the address is valid
//native方法,源代码在openjdk/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c,
//参见下面的说明
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
// An OutOfMemoryError may indicate that we've exhausted memory
// so force gc and re-attempt map
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException y) {
Thread.currentThread().interrupt();
}
try {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError y) {
// After a second OOME, fail
throw new IOException("Map failed", y);
}
}
// On Windows, and potentially other platforms, we need an open
// file descriptor for some mapping operations.
FileDescriptor mfd;
try {
mfd = nd.duplicateForMapping(fd);
} catch (IOException ioe) {
unmap0(addr, mapSize);
throw ioe;
}
assert (IOStatus.checkAll(addr));
assert (addr % allocationGranularity == 0);
int isize = (int)size;
Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
if ((!writable) || (imode == MAP_RO)) {
return Util.newMappedByteBufferR(isize,
addr + pagePosition,
mfd,
um);
} else {
return Util.newMappedByteBuffer(isize,
addr + pagePosition,
mfd,
um);
}
} finally {
threads.remove(ti);
end(IOStatus.checkAll(addr));
}
}

map0的源码实现:

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
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
//linux系统调用是通过整型的文件id引用文件的,这里得到文件id
jint fd = fdval(env, fdo);
int protections = 0;
int flags = 0;
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
protections = PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_PRIVATE;
}
//这里就是操作系统调用了,mmap64是宏定义,实际最后调用的是mmap
mapAddress = mmap64(
0, /* Let OS decide location */
len, /* Number of bytes to map */
protections, /* File permissions */
flags, /* Changes are shared */
fd, /* File descriptor of mapped file */
off); /* Offset into file */
if (mapAddress == MAP_FAILED) {
if (errno == ENOMEM) {
//如果没有映射成功,直接抛出OutOfMemoryError
JNU_ThrowOutOfMemoryError(env, "Map failed");
return IOS_THROWN;
}
return handle(env, -1, "Map failed");
}
return ((jlong) (unsigned long) mapAddress);
}

虽然FileChannel.map()的zise参数是long,但是size的大小最大为Integer.MAX_VALUE,也就是最大只能映射最大2G大小的空间。实际上操作系统提供的MMAP可以分配更大的空间,但是JAVA限制在2G,ByteBuffer等Buffer也最大只能分配2G大小的缓冲区。
MappedByteBuffer是通过mmap产生得到的缓冲区,这部分缓冲区是由操作系统直接创建和管理的,最后JVM通过unmmap让操作系统直接释放这部分内存。

Haep**Buffer

下面以ByteBuffer为例,说明Heap类型Buffer的细节。
该类型的Buffer可以通过下面方式产生:

  • ByteBuffer.allocate(int capacity)
  • ByteBuffer.wrap(byte[] array)
    使用传入的数组作为底层缓冲区,变更数组会影响缓冲区,变更缓冲区也会影响数组。
  • ByteBuffer.wrap(byte[] array,int offset, int length)
    使用传入的数组的一部分作为底层缓冲区,变更数组的对应部分会影响缓冲区,变更缓冲区也会影响数组。

DirectByteBuffer

DirectByteBuffer只能通过ByteBuffer.allocateDirect(int capacity) 产生。
ByteBuffer.allocateDirect()源码如下:

1
2
3
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

DirectByteBuffer()源码如下:

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
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
//直接内存是否要页对齐,我本机测试的不用
boolean pa = VM.isDirectMemoryPageAligned();
//页的大小,本机测试的是4K
int ps = Bits.pageSize();
//如果页对齐,则size的大小是ps+cap,ps是一页,cap也是从新的一页开始,也就是页对齐了
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//JVM维护所有直接内存的大小,如果已分配的直接内存加上本次要分配的大小超过允许分配的直接内存的最大值会
//引起GC,否则允许分配并把已分配的直接内存总量加上本次分配的大小。如果GC之后,还是超过所允许的最大值,
//则throw new OutOfMemoryError("Direct buffer memory");
Bits.reserveMemory(size, cap);
long base = 0;
try {
//是吧,unsafe可以直接操作底层内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {、
//没有分配成功,把刚刚加上的已分配的直接内存的大小减去。
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

unsafe.allocateMemory()的源码在openjdk/src/openjdk/hotspot/src/share/vm/prims/unsafe.cpp中。具体的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < 0) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
if (sz == 0) {
return 0;
}
sz = round_to(sz, HeapWordSize);
//最后调用的是 u_char* ptr = (u_char*)::malloc(size + space_before + space_after),也就是malloc。
void* x = os::malloc(sz, mtInternal);
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
//Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
return addr_to_java(x);
UNSAFE_END

JVM通过malloc分配得到连续的缓冲区,这部分缓冲区可以直接作为缓冲区参数进行操作系统调用。

JDK源码之编译

发表于 2017-06-05 | 分类于 java

本文主要说明如何编译组成rt.jar的源码,不涉及JVM的编译。

rt.jar

rt.jar也就是运行时相关的class类,安装的jre里面的rt.jar是不能被debug的,为了对JDK的源码进行debug跟踪,需要重新编译rt.jar。
rt.jar的全部源码在openjdk的jdk目录下,具体的路径是openjdk\jdk\src\,源码根据操作系统的不同和是否共享分成几个目录。
实际上rt.jar因为操作系统的不同,所包含的class也会有所不同,这导致在一些特殊特性上JAVA不再是“一次编译,到处执行”。
比如,在java7中,linux/unix下的jre支持com.sun.java.swing.plaf.gtk.GTKLookAndFeel,但是windows就不支持。
操作系统安装的JDK里面的src.zip不包含sun.*包,sun.\包在openjdk源码里面,具体的路径是:jdk\src\share\classes。
src.zip除了缺少sun.*包,还缺少其他源代码,不过这些源代码都能在share目录和各操作系统对应目录下找到。
openjdk的目录说明和源码下载参见:openjdk目录

编译

本文编译的是openjdk-7里面jdk目录下的源码。

  1. 新建java project
  2. 将openjdk源码copy到project中。除了复制share目录下的共享源码,还需要复制具体操作系统下的源码。
  3. 编译导出jar。

参考资料:怎么对jdk核心包进行跟踪调试,并查看调试中的变量值

openjdk源码目录

发表于 2017-06-05 | 分类于 java

本文主要参考自:openjdk源码结构

源码下载

openjdk-7的源码:openjdk7源码
openjdk-8的源码:openJDK8源码

目录说明

openjdk的源码的目录结构如下图所示:
此处输入图片的描述

各个目录的说明如下:
—— corba:不流行的多语言、分布式通讯接口
—— hotspot:Java 虚拟机
—— jaxp:XML 处理
—— jaxws:一组 XML web services 的 Java API
—— jdk:java 开发工具包
—— —— 针对操作系统的部分
—— —— share:与平台无关的实现
—— langtools:Java 语言工具
—— nashorn:JVM 上的 JavaScript 运行时

为了让大家易于理解,有所简化了结构。

Corba

全称 Common Object Request Broker Architecture,通用对象请求代理架构,是基于 对象-服务 机制设计得。

与 JavaBean、COM 等是同种范畴。

目前,通用的远程过程调用协议是 SOAP(Simple Object Access Protocol,简单对象访问协议),消息格式是 XML-RPC(存在 Json-RPC)。
另外,Apache Thrift 提供了多语言 C/S 通讯支持; 不少语言也内置了跨语言调用或对分布式环境友好,比如: lua 可以与 c 代码互调用,Go 可以调用 C 代码,erlang 在本地操作与分布式环境下的操作方法一样等。
Corba和RMI类似,都能实现RPC。但是RMI只是针对JAVA环境的,Corba是支持多语言的方案

Hotspot

全称 Java HotSpot Performance Engine,是 Java 虚拟机的一个实现,包含了服务器版和桌面应用程序版。利用 JIT 及自适应优化技术(自动查找性能热点并进行动态优化)来提高性能。

使用 java -version 可以查看 Hotspot 的版本。

从 Java 1.3 起为默认虚拟机,现由 Oracle 维护并发布。

其他 java 虚拟机:

  • JRockit:专注于服务器端,曾经号称是“世界上速度最快的 Java 虚拟机”,现归于 Oracle 旗下。
  • J9:IBM 设计的 Java 虚拟机。
  • Harmony:Apache 的顶级项目之一,从 2011 年 11 月 6 日起入驻 Apache 的 Java 项目。虽然其能够兼容 jdk,但由于 JCP (Java Community Process)仅仅允许授权给 Harmony 一个带有限制条件的TCK(Technology Compatibility Kit),即仅仅能使用在 J2SE ,而不是所有Java实现上(包括 J2ME 和 J2EE),导致了 Apache 组织与 Oracle 的决裂。Harmony 是 Android 虚拟机 Dalvik 的前身。
  • Dalvik 并不是 Java 虚拟机,它执行的是 dex 文件而不是 class 文件,使用的也是寄存器架构而不是栈架构 。

jaxp

全称 Java API for XML Processing,处理 XML 的Java API,是 Java XML 程序设计的应用程序接口之一,它提供解析和验证XML文档的能力。
jaxp 提供了处理 xml 文件的三种接口:

  • DOM 接口(文档对象模型解析),位于 \openjdk\jaxp\src\org\w3c\dom
  • SAX 接口(xml 简单 api 解析),位于 \openjdk\jaxp\src\org\xml\sax
  • StAX 接口(xml 流 api),位于 \openjdk\jaxp\src\javax\xml
  • 除了解析接口,JAXP还提供了XSLT接口用来对XML文档进行数据和结构的转换。

JaxWS

全称 Java API for Web Services,JAX-WS 允许开发者选择 RPC-oriented(面向 RPC) 或者 message-oriented(消息通信,erlang 使用的就是消息通信,不过 Java 内存模型是内存共享)来实现自己的web services。

通过 Web Services 提供的环境,可以实现 Java 与其他编程语言的交互(事实上就是 thrift 所做的,任何一种语言都可以通过 Web Services 实现与其他语言的通信,客户端用一种语言,服务器端可以用其他语言)。

LangTools

Java 语言支持工具

JDK

全称 Java Development Kit。

share

classes 目录里的是 Java 的实现,native 目录里的是 C++ 的实现,两部分基本对应。这两个目录里的结构与 java 的包也是对应,各个部分的用途另外再讲。

back、instrument、javavm、npt、transport 几个部分是实现 java 的基础部分,都是 C++ 代码,在这里从最底层理解 java,往后这些内容也会详讲。

sample 和 demo 目录有以下示例,区别在于 demo 目录是 针对 applets 的。

Nashorn

Nashorn 项目的目的是基于 Java 在 JVM 上实现一个轻量级高性能的 JavaScript 运行环境。基于 JSR-223 协议,Java 程序员可在 Java 程序中嵌入 JavaScript 代码。
该项目使用了 JSR-229 里描述的新连接机制(从 Java7起开始使用的连接机制):新的字节码(invokedynamic)以及新的基于方法句柄(method handle)的连接机制。通过接口注入(interface injection)在运行时修改类也是 JSR-229 里的内容。

JAVA IO

发表于 2017-06-04 | 分类于 technology

计算机IO

本部分内容主要参考自《JAVA NIO》的第一章简介。

缓冲区操作

下图简单描述了数据从外部磁盘向运行中的进程的内存区域移动的过程。
进程使用 read( )系统调用,要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,这一步通过 DMA 完成,无需主 CPU 协助。一旦磁盘控制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行 read()调用时指定的缓冲区。
缓冲区操作
为什么不让磁盘控制器直接将磁盘上数据直接copy到用户缓冲区呢?

  • 硬件无法直接访问用户空间
  • 像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责
    数据的分解、再组合工作,因此充当着中间人的角色。

    发散和汇聚

    根据发散/汇聚的概念,进程只需一个系统调用,就能把一连串缓冲区地址传递给操作系统。然后,内核就可以顺序填充或排干多个缓冲区,读的时候就把数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来(如下图所示)。
    发散和汇聚
    优点:

  • 这样用户进程就不必多次执行系统调用(那样做可能代价不菲)

  • 内核也可以优化数据的处理过程,因为它已掌握待传输数据的全部信息。
  • 如果系统配有多个 CPU,甚至可以同时填充或排干多个缓冲区。
  • 多个缓冲区可能方便一些特定协议的编程(自己加的)。

    虚拟内存

    现代操作系统都支持虚拟内存,CPU为了更好的支持虚拟内存也加入了MMU(内存管理单元)。在寻址时,需要把虚拟地址经过MMU计算得到真实的物理地址,然后再去寻址。
    虚拟内存的好处:

  • 一个以上的虚拟地址可指向同一个物理内存地址。

  • 虚拟内存空间可大于实际可用的硬件内存。

    内存页面调度

    为了支持虚拟内存,需要把内存存储到硬盘上,现代操作系统是通过内存页面调度实现的。对于采用分页技术的现代操作系统而言,这也是数据在磁盘与物理内存之间往来的唯一方式。
    下面的步骤说明了内存页面调度的过程:

  • 当 CPU 引用某内存地址时,MMU负责确定该地址所在页(往往通过对地址值进行移位或屏蔽位操作实现),并将虚拟页号转换为物理页号(这一步由硬件完成,速度极快)。如果当前不存在与该虚拟页形成有效映射的物理内存页,MMU 会向 CPU 提交一个页错误。

  • 页错误随即产生一个陷阱(类似于系统调用),把控制权移交给内核,附带导致错误的虚拟地址信息,然后内核采取步骤验证页的有效性。内核会安排页面调入操作,把缺失的页内容读回物理内存。这往往导致别的页被移出物理内存,好给新来的页让地方。在这种情况下,如果待移出的页已经被碰过了(自创建或上次页面调入以来,内容已发生改变),还必须首先执行页面调出,把页内容拷贝到磁盘上的分页区。
  • 如果所要求的地址不是有效的虚拟内存地址(不属于正在执行的进程的任何一个内存段),则该页不能通过验证,段错误随即产生。于是,控制权转交给内核的另一部分,通常导致的结果就是进程被强令关闭。
    一旦出错的页通过了验证,MMU 随即更新,建立新的虚拟到物理的映射(如有必要,中断被移出页的映射),用户进程得以继续。造成页错误的用户进程对此不会有丝毫察觉,一切都在不知不觉中进行。

    文件IO

    虚拟内存通过页面调度可以调度内存页和硬盘页,现代操作系统对文件IO的操作也是基于页面调度实现的。
    采用分页技术的操作系统执行 I/O 的全过程可总结为以下几步:

  • 确定请求的数据分布在文件系统的哪些页(磁盘扇区组)。磁盘上的文件内容和元数
    据可能跨越多个文件系统页,而且这些页可能也不连续。

  • 在内核空间分配足够数量的内存页,以容纳得到确定的文件系统页。
  • 在内存页与磁盘上的文件系统页之间建立映射。
  • 为每一个内存页产生页错误。
  • 虚拟内存系统俘获页错误,安排页面调入,从磁盘上读取页内容,使页有效。
  • 一旦页面调入操作完成,文件系统即对原始数据进行解析,取得所需文件内容或属性信息。

    内存映射文件

    为了在内核空间的文件系统页与用户空间的内存区之间移动数据,一次以上的拷贝操作几乎总是免不了的。这是因为,在文件系统页与用户缓冲区之间往往没有一一对应关系。但是,还有一种大多数操作系统都支持的特殊类型的 I/O 操作,允许用户进程最大限度地利用面向页的系统 I/O 特性,并完全摒弃缓冲区拷贝。这就是内存映射 I/O。
    内存映射文件将内存页全部映射到文件的硬盘块上,存在一一映射关系。使用内存映射文件时,用户态是没有缓冲区的,只存在映射到硬盘页的内存页面缓冲区,所以是真正的ZeroCopy。而不使用内存映射文件的IO,因为直接内存和内存页缓冲区没有映射关系,所以使使用直接内存作为缓冲区也是需要最少一次copy的。(那文件IO时使用直接内存的好处是什么?)

    文件锁定

    JVM实现的是进程之间的锁定,一个进程之间的多个线程之间是不锁定的。
    支持共享锁、独占锁等
    支持锁定文件的部分区域,粒度支持到字节。

    流IO

    流的传输一般(也不必然如此)比块设备慢,经常用于间歇性输入。多数操作系统允许把流置于非块模式,这样,进程可以查看流上是否有输入,即便当时没有也不影响它干别的。这样一种能力使得进程可以在有输入的时候进行处理,输入流闲置的时候执行其他功能。
    处理流IO需要依赖操作系统的通知机制:

  • Selector通知已就绪,可以进行相关IO操作。

  • AIO通知IO操作已完成,可以处理相关业务逻辑了。

JAVA IO 发展和总结

BIO 抽象工作良好,适应用途广泛。但是当移动大量数据时,这些 I/O 类可伸缩性不强,也没有提供当今大多数操作系统普遍具备的常用 I/O 功能,如文件锁定、非块 I/O、就绪性选择和内存映射。这些特性对实现可伸缩性是至关重要的,对保持与非 Java 应用程序的正常交互也可以说是必不可少的,尤其是在企业应用层面,而传统的 Java I/O 机制却没有模拟这些通用 I/O 服务。操作系统与 Java 基于流的 I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的。BIO 类喜欢操作小块数据——单个字节、几行文本。

  • BIO
    JDK版本:1.1
    时间:
    特点:
  1. 同步阻塞
  2. 面向流(字节流和字符流)
  3. 不支持操作系统支持的很多IO概念和特性
  4. read()操作返回所读的字节数,write操作没有返回值
  5. read是SeekAble的(支持skip),write不是Seekable的。
  • NIO.1
    JDK版本:1.4
    时间:2002
    特点:
  1. 针对现代操作系统重新抽象和封装实现了Channel和Buffer
  2. 网络IO支持配置是否阻塞,文件IO只能是阻塞的
  3. 网络IO支持多路复用(异步),使用多路复用时,Channel必须是非阻塞模式,而且阻塞模式不能再被修改
  4. 网络IO和文件IO都是可中断的。文件IO过程中被中断时,JVM会关闭Channel。
  5. 支持直接Buffer、支持内存映射文件
  6. 支持文件锁定,但是是进程级别的锁定
  7. 支持Pipe
  8. read操作返回所读的字节数,write操作返回所写的字节数
  9. Channel是全双工的。虽然RandomAccessFile也可以是全双工的,但是Channel这种封装方式更好
  • NIO.2(AIO)
    JDK版本:1.7
    时间:2011
    特点:
  1. 文件IO和网络IO是异步的(当然是非阻塞的),异步形式是future和callback。
  2. Watch Service
  3. 文件IO支持更丰富的API

BIO

NIO.1

NIO.2

异步

Watch Service

IO

更丰富的文件操作

12
yunzhu

yunzhu

code rush

14 日志
5 分类
12 标签
RSS
WeChat GitHub Weibo QQ
© 2017 yunzhu
由 Hexo 强力驱动
主题 - NexT.Pisces