JAVA NIO总结

Posted on

JAVA NIO总结

(一)—基本概念

1、输入/输出:概念性描述

1.1) I/O简介

I/O 或者输入/输出指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。

在 Java 编程中,直到最近一直使用的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。NIO 与原来的 I/O 有同样的作用和目的,但是它使用不同的方式? 块 I/O。正如您将在本教程中学到的,块 I/O 的效率可以比流 I/O 高许多。

1.2)为什么要使用NIO?

NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。

1.3)流与块的比较

原来的 I/O 库(在

java.io./* 中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流* *的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。

一个 面向块* *的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

1.4)集成的IO

在 JDK 1.4 中原来的 I/O 包和 NIO 已经很好地集成了。

java.io./* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,

java.io./* 包中的一些类包含以块的形式读写数据的方法,这使得即使在更面向流的系统中,处理速度也会更快。

也可以用 NIO 库实现标准 I/O 功能。例如,可以容易地使用块 I/O 一次一个字节地移动数据。但是正如您会看到的,NIO 还提供了原 I/O 包中所没有的许多好处。

2、通道和缓冲区

2.1)概述

通道 和

缓冲区 是 NIO 中的核心对象,几乎在每一个 I/O 操作中都要使用它们。

通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。

在本节中,您会了解到 NIO 中通道和缓冲区是如何工作的。

2.2)什么是缓冲区

Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入

Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到

Stream 对象中。

在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。

缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

2.3)缓冲区类型

最常用的缓冲区类型是

ByteBuffer 。一个

ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。

ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

每一个

Buffer 类都是

Buffer 接口的一个实例。 除了

ByteBuffer ,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用

ByteBuffer ,所以它具有所有共享的缓冲区操作以及一些特有的操作。

现在您可以花一点时间运行 UseFloatBuffer.java,它包含了类型化的缓冲区的一个应用例子。

2.4)什么是通道

Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。

正如前面提到的,所有数据都通过

Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

2.5)通道类型

通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是

InputStream 或者

OutputStream 的子类), 而

通道 可以用于读、写或者同时用于读写。因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。

3、从理论到实践:NIO的读和写

3.1)概述

读和写是 I/O 的基本过程。从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。

在本节中,我们将学习有关在 Java 程序中读取和写入数据的一些知识。我们将回顾 NIO 的主要组件(缓冲区、通道和一些相关的方法),看看它们是如何交互以进行读写的。在接下来的几节中,我们将更详细地分析这其中的每个组件以及其交互。所有例子代码下载请点击例子代码

3.2)从文件中读取

在我们第一个练习中,我们将从一个文件中读取一些数据。如果使用原来的 I/O,那么我们只需创建一个

FileInputStream 并从它那里读取。而在 NIO 中,情况稍有不同:我们首先从

FileInputStream 获取一个

FileInputStream 对象,然后使用这个通道来读取数据。

在 NIO 系统中,任何时候执行一个读操作,您都是从通道中读取,但是您不是直接* *从通道读取。因为所有数据最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。

因此读取文件涉及三个步骤:(1) 从

FileInputStream 获取

Channel ,(2) 创建

Buffer ,(3) 将数据从

Channel 读到

Buffer 中。现在,让我们看一下这个过程。

3.3)三个容易的步骤

第一步是获取通道。我们从

FileInputStream 获取通道: FileInputStream fin = new FileInputStream( "readandshow.txt" );

FileChannel fc = fin.getChannel();

下一步是创建缓冲区: ByteBuffer buffer = ByteBuffer.allocate( 1024 );

最后,需要将数据从通道读到缓冲区中,如下所示: fc.read( buffer );

您会注意到,我们不需要告诉通道要读 多少数据 到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。我们将在 缓冲区内部细节 中介绍更多关于缓冲区统计机制的内容。

3.4)写入文件

在 NIO 中写入文件类似于从文件中读取。首先从

FileOutputStream 获取一个通道: FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );

FileChannel fc = fout.getChannel();

下一步是创建一个缓冲区并在其中放入一些数据 - 在这里,数据将从一个名为

message 的数组中取出,这个数组包含字符串 "Some bytes" 的 ASCII 字节(本教程后面将会解释

buffer.flip() 和

buffer.put() 调用)。 ByteBuffer buffer = ByteBuffer.allocate( 1024 );

for (int i=0; i<message.length; ++i) {

 buffer.put( message[i] );

}

buffer.flip();

最后一步是写入通道中: fc.write( buffer );

注意在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。

3.5)读写结合

下面我们将看一下在结合读和写时会有什么情况。我们以一个名为 CopyFile.java 的简单程序作为这个练习的基础,它将一个文件的所有内容拷贝到另一个文件中。CopyFile.java 执行三个基本操作:首先创建一个

Buffer ,然后从源文件中将数据读到这个缓冲区中,然后将缓冲区写入目标文件。这个程序不断重复 ― 读、写、读、写 ― 直到源文件结束。

CopyFile 程序让您看到我们如何检查操作的状态,以及如何使用

clear() 和

flip() 方法重设缓冲区,并准备缓冲区以便将新读取的数据写到另一个通道中。

运行CopyFile例子

因为缓冲区会跟踪它自己的数据,所以 CopyFile 程序的内部循环 (inner loop) 非常简单,如下所示: fcin.read( buffer );

fcout.write( buffer );

第一行将数据从输入通道

fcin 中读入缓冲区,第二行将这些数据写到输出通道

fcout 。

检查状态

下一步是检查拷贝何时完成。当没有更多的数据时,拷贝就算完成,并且可以在

read() 方法返回 -1 是判断这一点,如下所示: int r = fcin.read( buffer );

if (r==-1) {

 break;

}

重设缓冲区

最后,在从输入通道读入缓冲区之前,我们调用

clear() 方法。同样,在将缓冲区写入输出通道之前,我们调用

flip() 方法,如下所示:

buffer.clear();

int r = fcin.read( buffer );

if (r==-1) { break;

}

buffer.flip(); fcout.write( buffer );

clear() 方法重设缓冲区,使它可以接受读入的数据。

flip() 方法让缓冲区可以将新读入的数据写入另一个通道。

来源: [http://blog.csdn.net/ssjhust123/article/details/7904757](http://blog.csdn.net/ssjhust123/article/details/7904757)

(二)—缓冲区原理

4、缓冲区的内部细节

概述

本节将介绍 NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor)。

状态变量是前一节中提到的"内部统计机制"的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。

在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用 访问方法

get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法

put() 。

在本节中,您将学习关于 NIO 中的状态变量和访问方法的内容。我们将描述每一个组件,并让您有机会看到它的实际应用。虽然 NIO 的内部统计机制初看起来可能很复杂,但是您很快就会看到大部分的实际工作都已经替您完成了。您可能习惯于通过手工编码进行簿记 ― 即使用字节数组和索引变量,现在它已在 NIO 中内部地处理了。

状态变量

可以用三个值指定缓冲区在任意时刻的状态:

  • position
  • limit
  • capacity

这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。我们将在下面的小节中详细分析每一个变量,还要介绍它们如何适应典型的读/写(输入/输出)进程。在这个例子中,我们假定要将数据从一个输入通道拷贝到一个输出通道。

position

您可以回想一下,缓冲区实际上就是美化了的数组。在从通道读取时,您将所读取的数据放到底层的数组中。

position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的

position 将会设置为3,指向数组中第四个元素。

同样,在写入通道时,您是从缓冲区中获取数据。

position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的

position 将被设置为5,指向数组的第六个元素。

limit

limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。

position 总是小于或者等于

limit 。

capacity

缓冲区的

capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。

limit 决不能大于

capacity 。

观察变量

我们首先观察一个新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的

总容量 为8个字节。

Buffer 的状态如下所示: Buffer state

回想一下 ,

limit 决不能大于

capacity ,此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。 Array

position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。

position 设置如下所示: Position setting

由于

capacity 不会改变,所以我们在下面的讨论中可以忽略它。

第一次读取

现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从

position 开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 3,如下所示: Position increased to 3

limit 没有改变。

第二次读取

在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由

position 所指定的位置上,

position 因而增加 2: Position increased by 2

limit 没有改变。

flip

现在我们要将数据写到输出通道中。在这之前,我们必须调用

flip() 方法。这个方法做两件非常重要的事:

  1. 它将

limit 设置为当前

position 。

  1. 它将

position 设置为 0。

前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区: Buffer after the flip

我们现在可以将数据从缓冲区写入通道了。

position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。

limit 已被设置为原来的

position ,这意味着它包括以前读到的所有字节,并且一个字节也不多。

第一次写入

在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得

position 增加到 4,而

limit 不变,如下所示: Position advanced to 4, limit unchanged

第二次写入

我们只剩下一个字节可写了。

limit 在我们调用

flip() 时被设置为 5,并且

position 不能超过

limit 。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得

position 增加到 5,并保持

limit 不变,如下所示: Position advanced to 5, limit unchanged

clear

最后一步是调用缓冲区的

clear() 方法。这个方法重设缓冲区以便接收更多的字节。

Clear 做两种非常重要的事情:

  1. 它将

limit 设置为与

capacity 相同。

  1. 它设置

position 为 0。

下图显示了在调用

clear() 后缓冲区的状态: State of the buffer after clear() has been called

缓冲区现在可以接收新的数据了。

访问方法

到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。

或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。

在本节的最后,我们将详细分析如何使用

ByteBuffer 类的

get() 和

put() 方法直接访问缓冲区中的数据

get()方法

ByteBuffer 类中有四个

get() 方法:

  1. byte get();
  2. ByteBuffer get( byte dst[] );
  3. ByteBuffer get( byte dst[], int offset, int length );
  4. byte get( int index );

第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回

ByteBuffer 的方法只是返回调用它们的缓冲区的

this 值。

此外,我们认为前三个

get() 方法是相对的,而最后一个方法是绝对的。 相对 意味着

get() 操作服从

limit 和

position 值 ― 更明确地说,字节是从当前

position 读取的,而

position 在

get 之后会增加。另一方面,一个 绝对 方法会忽略

limit 和

position 值,也不会影响它们。事实上,它完全绕过了缓冲区的统计方法。

上面列出的方法对应于

ByteBuffer 类。其他类有等价的

get() 方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的类型。

put()方法

ByteBuffer 类中有五个

put() 方法:

  1. ByteBuffer put( byte b );
  2. ByteBuffer put( byte src[] );
  3. ByteBuffer put( byte src[], int offset, int length );
  4. ByteBuffer put( ByteBuffer src );
  5. ByteBuffer put( int index, byte b );

第一个方法

写入(put) 单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源

ByteBuffer 写入这个

ByteBuffer 。第五个方法将字节写入缓冲区中特定的

位置 。那些返回

ByteBuffer 的方法只是返回调用它们的缓冲区的

this 值。

get() 方法一样,我们将把

put() 方法划分为 相对 或者 绝对 的。前四个方法是相对的,而第五个方法是绝对的。

上面显示的方法对应于

ByteBuffer 类。其他类有等价的

put() 方法,这些方法除了不是处理字节之外,其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。

类型化的 get() 和 put() 方法

除了前些小节中描述的

get() 和

put() 方法,

ByteBuffer 还有用于读写不同类型的值的其他方法,如下所示:

  • getByte()
  • getChar()
  • getShort()
  • getInt()
  • getLong()
  • getFloat()
  • getDouble()
  • putByte()
  • putChar()
  • putShort()
  • putInt()
  • putLong()
  • putFloat()
  • putDouble()

事实上,这其中的每个方法都有两种类型 ― 一种是相对的,另一种是绝对的。它们对于读取格式化的二进制数据(如图像文件的头部)很有用。

您可以在例子程序 TypesInByteBuffer.java 中看到这些方法的实际应用。

缓冲区的使用:一个内部循环

下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。 while (true) {

 buffer.clear();
 int r = fcin.read( buffer );


 if (r==-1) {

   break;
 }


 buffer.flip();

 fcout.write( buffer );

}

read() 和

write() 调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。

clear() 和

flip() 方法用于让缓冲区在读和写之间切换。

5、关于缓冲区的更多内容

概述

到目前为止,您已经学习了使用缓冲区进行日常工作所需要掌握的大部分内容。我们的例子没怎么超出标准的读/写过程种类,在原来的 I/O 中可以像在 NIO 中一样容易地实现这样的标准读写过程。

本节将讨论使用缓冲区的一些更复杂的方面,比如缓冲区分配、包装和分片。我们还会讨论 NIO 带给 Java 平台的一些新功能。您将学到如何创建不同类型的缓冲区以达到不同的目的,如可保护数据不被修改的 只读 缓冲区,和直接映射到底层操作系统缓冲区的 直接 缓冲区。我们将在本节的最后介绍如何在 NIO 中创建内存映射文件。

缓冲区分配和包装

在能够读和写之前,必须有一个缓冲区。要创建缓冲区,您必须 分配 它。我们使用静态方法

allocate() 来分配缓冲区: ByteBuffer buffer = ByteBuffer.allocate( 1024 );

allocate() 方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中 ― 在本例中是一个

ByteBuffer 。

您还可以将一个现有的数组转换为缓冲区,如下所示: byte array[] = new byte[1024];

ByteBuffer buffer = ByteBuffer.wrap( array );

本例使用了

wrap() 方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。

缓冲区分片

slice() 方法根据现有的缓冲区创建一种 子缓冲区 。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。

使用例子可以最好地说明这点。让我们首先创建一个长度为 10 的

ByteBuffer : ByteBuffer buffer = ByteBuffer.allocate( 10 );

然后使用数据来填充这个缓冲区,在第 n 个槽中放入数字 n: for (int i=0; i<buffer.capacity(); ++i) {

 buffer.put( (byte)i );

}

现在我们对这个缓冲区 分片 ,以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 窗口

窗口的起始和结束位置通过设置

position 和

limit 值来指定,然后调用

Buffer 的

slice() 方法: buffer.position( 3 );

buffer.limit( 7 ); ByteBuffer slice = buffer.slice();

片段 是缓冲区的

子缓冲区 。不过,

片段 和

缓冲区 共享同一个底层数据数组,我们在下一节将会看到这一点。

缓冲区分片和数据共享

我们已经创建了原缓冲区的子缓冲区,并且我们知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。

我们遍历子缓冲区,将每一个元素乘以 11 来改变它。例如,5 会变成 55。 for (int i=0; i<slice.capacity(); ++i) {

 byte b = slice.get( i );
 b /*= 11;

 slice.put( i, b );

}

最后,再看一下原缓冲区中的内容: buffer.position( 0 );

buffer.limit( buffer.capacity() );

while (buffer.remaining()>0) { System.out.println( buffer.get() );

}

结果表明只有在子缓冲区窗口中的元素被改变了: $ java SliceBuffer

0 1

2 33

44 55

66 7

8 9

缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,您只需取主缓冲区的一个片,并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。

只读缓冲区

只读缓冲区非常简单 ― 您可以读取它们,但是不能向它们写入。可以通过调用缓冲区的

asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。

只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。

不能将只读的缓冲区转换为可写的缓冲区。

直接和间接缓冲区

另一种有用的

ByteBuffer 是直接缓冲区。 直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。

实际上,直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的:

给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

您可以在例子程序 FastCopyFile.java 中看到直接缓冲区的实际应用,这个程序是 CopyFile.java 的另一个版本,它使用了直接缓冲区以提高速度。

还可以用内存映射文件创建直接缓冲区。

[java] view plaincopy

  1. ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 ); //直接缓冲区

内存映射文件I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。

内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

将文件映射到内存

了解内存映射的最好方法是使用例子。在下面的例子中,我们要将一个

FileChannel (它的全部或者部分)映射到内存中。为此我们将使用

FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中: MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,

 0, 1024 );

map() 方法返回一个

MappedByteBuffer ,它是

ByteBuffer 的子类。因此,您可以像使用其他任何

ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。 来源: [http://blog.csdn.net/ssjhust123/article/details/7905278](http://blog.csdn.net/ssjhust123/article/details/7905278)

(三)—分散聚集、文件锁定、字符集

6、分散和聚集

概述

分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。

一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。分散/聚集 I/O 对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。

分散/聚集IO

通道可以有选择地实现两个新的接口:

ScatteringByteChannel 和

GatheringByteChannel 。一个

ScatteringByteChannel 是一个具有两个附加读方法的通道:

  • long read( ByteBuffer[] dsts );
  • long read( ByteBuffer[] dsts, int offset, int length );

这些

long read() 方法很像标准的

read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。

分散读取 中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。

聚集写入 类似于分散读取,只不过是用来写入。它也有接受缓冲区数组的方法:

  • long write( ByteBuffer[] srcs );
  • long write( ByteBuffer[] srcs, int offset, int length );

聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致,您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。

从例子程序 UseScatterGather.java 中可以看到分散读取和聚集写入的实际应用。

分散/聚集的应用

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。

我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

7、文件锁定

概述

文件锁定初看起来可能让人迷惑。它 似乎 指的是防止程序或者用户访问特定文件。事实上,文件锁就像常规的 Java 对象锁 ― 它们是 劝告式的(advisory) 锁。它们不阻止任何形式的数据访问,相反,它们通过锁的共享和获取赖允许系统的不同部分相互协调。

您可以锁定整个文件或者文件的一部分。如果您获取一个排它锁,那么其他人就不能获得同一个文件或者文件的一部分上的锁。如果您获得一个共享锁,那么其他人可以获得同一个文件或者文件一部分上的共享锁,但是不能获得排它锁。文件锁定并不总是出于保护数据的目的。例如,您可能临时锁定一个文件以保证特定的写操作成为原子的,而不会有其他程序的干扰。

大多数操作系统提供了文件系统锁,但是它们并不都是采用同样的方式。有些实现提供了共享锁,而另一些仅提供了排它锁。事实上,有些实现使得文件的锁定部分不可访问,尽管大多数实现不是这样的。

在本节中,您将学习如何在 NIO 中执行简单的文件锁过程,我们还将探讨一些保证被锁定的文件尽可能可移植的方法。

锁定文件

要获取文件的一部分上的锁,您要调用一个打开的

FileChannel 上的

lock() 方法。注意,如果要获取一个排它锁,您必须以写方式打开文件。 RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );

FileChannel fc = raf.getChannel(); FileLock lock = fc.lock( start, end, false );

在拥有锁之后,您可以执行需要的任何敏感操作,然后再释放锁: lock.release();

在释放锁后,尝试获得锁的其他任何程序都有机会获得它。

本小节的例子程序 UseFileLocks.java 必须与它自己并行运行。这个程序获取一个文件上的锁,持有三秒钟,然后释放它。如果同时运行这个程序的多个实例,您会看到每个实例依次获得锁。如果两个程序分别获取同一个文件不同位置的锁,则不会阻塞。比如第一个程序获取文件范围为0-20的锁,而第二个程序获取21-40的锁,则会正常执行。

文件锁定和可移植性

文件锁定可能是一个复杂的操作,特别是考虑到不同的操作系统是以不同的方式实现锁这一事实。下面的指导原则将帮助您尽可能保持代码的可移植性:

  • 只使用排它锁。
  • 将所有的锁视为劝告式的(advisory)。

8、字符集

概述

根据 Sun 的文档,一个

Charset 是“十六位 Unicode 字符序列与字节序列之间的一个命名的映射”。实际上,一个

Charset 允许您以尽可能最具可移植性的方式读写字符序列。

Java 语言被定义为基于 Unicode。然而在实际上,许多人编写代码时都假设一个字符在磁盘上或者在网络流中用一个字节表示。这种假设在许多情况下成立,但是并不是在所有情况下都成立,而且随着计算机变得对 Unicode 越来越友好,这个假设就日益变得不能成立了。

在本节中,我们将看一下如何使用

Charsets 以适合现代文本格式的方式处理文本数据。这里将使用的示例程序相当简单,不过,它触及了使用

Charset 的所有关键方面:为给定的字符编码创建

Charset ,以及使用该

Charset 解码和编码文本数据。

回页首

编码/解码

要读和写文本,我们要分别使用

CharsetDecoder 和

CharsetEncoder 。将它们称为 编码器 解码器 是有道理的。一个 字符 不再表示一个特定的位模式,而是表示字符系统中的一个实体。因此,由某个实际的位模式表示的字符必须以某种特定的 编码 来表示。

CharsetDecoder 用于将逐位表示的一串字符转换为具体的

char 值。同样,一个

CharsetEncoder 用于将字符转换回位。

在下一个小节中,我们将考察一个使用这些对象来读写数据的程序。

回页首

处理文本的正确方式

现在我们将分析这个例子程序 UseCharsets.java。这个程序非常简单 ― 它从一个文件中读取一些文本,并将该文本写入另一个文件。但是它把该数据当作文本数据,并使用

CharBuffer 来将该数句读入一个

CharsetDecoder 中。同样,它使用

CharsetEncoder 来写回该数据。

我们将假设字符以 ISO-8859-1(Latin1) 字符集(这是 ASCII 的标准扩展)的形式储存在磁盘上。尽管我们必须为使用 Unicode 做好准备,但是也必须认识到不同的文件是以不同的格式储存的,而 ASCII 无疑是非常普遍的一种格式。事实上,每种 Java 实现都要求对以下字符编码提供完全的支持:

  • US-ASCII
  • ISO-8859-1
  • UTF-8
  • UTF-16BE
  • UTF-16LE
  • UTF-16

回页首

示例程序

在打开相应的文件、将输入数据读入名为

inputData 的

ByteBuffer 之后,我们的程序必须创建 ISO-8859-1 (Latin1) 字符集的一个实例: Charset latin1 = Charset.forName( "ISO-8859-1" );

然后,创建一个解码器(用于读取)和一个编码器 (用于写入): CharsetDecoder decoder = latin1.newDecoder();

CharsetEncoder encoder = latin1.newEncoder();

为了将字节数据解码为一组字符,我们把

ByteBuffer 传递给

CharsetDecoder ,结果得到一个

CharBuffer : CharBuffer cb = decoder.decode( inputData );

如果想要处理字符,我们可以在程序的此处进行。但是我们只想无改变地将它写回,所以没有什么要做的。

要写回数据,我们必须使用

CharsetEncoder 将它转换回字节: ByteBuffer outputData = encoder.encode( cb );

在转换完成之后,我们就可以将数据写到文件中了。 来源: [http://blog.csdn.net/ssjhust123/article/details/7905367](http://blog.csdn.net/ssjhust123/article/details/7905367) (四)—网络和异步IO

9、连网和异步 I/O

概述

连网是学习异步 I/O 的很好基础,而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来说,无疑都是必须具备的知识。NIO 中的连网与 NIO 中的其他任何操作没有什么不同 ― 它依赖通道和缓冲区,而您通常使用

InputStream 和

OutputStream 来获得通道。

本节首先介绍异步 I/O 的基础 ― 它是什么以及它不是什么,然后转向更实用的、程序性的例子。

异步I/O

异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行

read() 调用时,代码会阻塞直至有可供读取的数据。同样,

write() 调用将会阻塞直至数据能够写入。

另一方面,异步 I/O 调用不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。

异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。

我们将通过研究一个名为 MultiPortEcho.java 的例子程序来查看异步 I/O 的实际应用。这个程序就像传统的 echo server,它接受网络连接并向它们回响它们可能发送的数据。不过它有一个附加的特性,就是它能同时监听多个端口,并处理来自所有这些端口的连接。并且它只在单个线程中完成所有这些工作。

Selectors

本节的阐述对应于

MultiPortEcho 的源代码中的

go() 方法的实现,因此应该看一下源代码,以便对所发生的事情有个更全面的了解。

异步 I/O 中的核心对象名为

Selector 。

Selector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

所以,我们需要做的第一件事就是创建一个

Selector : Selector selector = Selector.open();

然后,我们将对不同的通道对象调用

register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。

register() 的第一个参数总是这个

Selector 。

打开一个Server Socket Channel

为了接收连接,我们需要一个

ServerSocketChannel 。事实上,我们要监听的每一个端口都需要有一个

ServerSocketChannel 。对于每一个端口,我们打开一个

ServerSocketChannel ,如下所示: ServerSocketChannel ssc = ServerSocketChannel.open();

ssc.configureBlocking( false );

ServerSocket ss = ssc.socket(); InetSocketAddress address = new InetSocketAddress( ports[i] );

ss.bind( address );

第一行创建一个新的

ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将

ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。

选择键

下一步是将新打开的

ServerSocketChannels 注册到

Selector 上。为此我们使用 ServerSocketChannel.register() 方法,如下所示: SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

register() 的第一个参数总是这个

Selector 。第二个参数是

OP_ACCEPT ,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于

ServerSocketChannel 的唯一事件类型。

请注意对

register() 的调用的返回值。

SelectionKey 代表这个通道在此

Selector 上的这个注册。当某个

Selector 通知您某个传入事件时,它是通过提供对应于该事件的

SelectionKey 来进行的。

SelectionKey 还可以用于取消通道的注册。

内部循环

现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。使用

Selectors 的几乎每个程序都像下面这样使用内部循环: int num = selector.select();

Set selectedKeys = selector.selectedKeys();

Iterator it = selectedKeys.iterator();

while (it.hasNext()) { SelectionKey key = (SelectionKey)it.next();

 // ... deal with I/O event ...

}

首先,我们调用

Selector 的

select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,

select() 方法将返回所发生的事件的数量。

接下来,我们调用

Selector 的

selectedKeys() 方法,它返回发生了事件的

SelectionKey 对象的一个

集合 。

我们通过迭代

SelectionKeys 并依次处理每个

SelectionKey 来处理事件。对于每一个

SelectionKey ,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。

监听新连接

程序执行到这里,我们仅注册了

ServerSocketChannel ,并且仅注册它们“接收”事件。为确认这一点,我们对

SelectionKey 调用

readyOps() 方法,并检查发生了什么类型的事件: if ((key.readyOps() & SelectionKey.OP_ACCEPT)

 == SelectionKey.OP_ACCEPT) {


 // Accept the new connection
 // ...

}

可以肯定地说,

readOps() 方法告诉我们该事件是新的连接。

接收新的连接

因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心

accept() 操作会阻塞: ServerSocketChannel ssc = (ServerSocketChannel)key.channel();

SocketChannel sc = ssc.accept();

下一步是将新连接的

SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将

SocketChannel 注册到

Selector 上,如下所示: sc.configureBlocking( false );

SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );

注意我们使用

register() 的

OP_READ 参数,将

SocketChannel 注册用于 读取 而不是 接受 新连接。

删除处理过的SelectionKey

在处理

SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的

SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的

remove() 方法来删除处理过的

SelectionKey : it.remove();

现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。

传入的I/O

当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用

Selector.select() ,并返回一个或者多个 I/O 事件。这一次,

SelectionKey 将被标记为

OP_READ 事件,如下所示: } else if ((key.readyOps() & SelectionKey.OP_READ)

 == SelectionKey.OP_READ) {
 // Read the data

 SocketChannel sc = (SocketChannel)key.channel();
 // ...

}

与以前一样,我们取得发生 I/O 事件的通道并处理它。在本例中,由于这是一个 echo server,我们只希望从套接字中读取数据并马上将它发送回去。关于这个过程的细节,请参见代码中的源代码 (MultiPortEcho.java)。

回到主循环

每次返回主循环,我们都要调用

select 的

Selector() 方法,并取得一组

SelectionKey 。每个键代表一个 I/O 事件。我们处理事件,从选定的键集中删除

SelectionKey ,然后返回主循环的顶部。

这个程序有点过于简单,因为它的目的只是展示异步 I/O 所涉及的技术。在现实的应用程序中,您需要通过将通道从

Selector 中删除来处理关闭的通道。而且您可能要使用多个线程。这个程序可以仅使用一个线程,因为它只是一个演示,但是在现实场景中,创建一个线程池来负责 I/O 事件处理中的耗时部分会更有意义。 来源: [http://blog.csdn.net/ssjhust123/article/details/7905401](http://blog.csdn.net/ssjhust123/article/details/7905401)

Java NIO API详解

Posted on

Java NIO API详解

【永恒的瞬间】 ☜Give me hapy ☞

BlogJava 首页 新文章 联系 聚合 管理 诱惑的世界,难以自我! 名利的生活,面具包裹! 我们的人生,充当角色! 寂静的深夜,真实自我!

常用链接

留言簿(3)

随笔档案(6)

文章分类(188)

文章档案(206)

URL

搜索

*

最新评论


这个跟返回的XML的namespace有关。

  • --kennethpoon

阅读排行榜

评论排行榜

Java NIO API详解

NIO API 主要集中在 java.nio 和它的 subpackages 中:

java.nio

定义了 Buffer 及其数据类型相关的子类。其中被 java.nio.channels 中的类用来进行 IO 操作的 ByteBuffer 的作用非常重要。

java.nio.channels

定义了一系列处理 IO 的 Channel 接口以及这些接口在文件系统和网络通讯上的实现。通过 Selector 这个类,还提供了进行非阻塞 IO 操作的办法。这个包可以说是 NIO API 的核心。

java.nio.channels.spi

定义了可用来实现 channel 和 selector API 的抽象类。

java.nio.charset

     定义了处理字符编码和解码的类。

java.nio.charset.spi

     定义了可用来实现 charset API 的抽象类。

java.nio.channels.spi 和 java.nio.charset.spi 这两个包主要被用来对现有 NIO API 进行扩展,在实际的使用中,我们一般只和另外的 3 个包打交道。下面将对这 3 个包一一介绍。

Package java.nio

这个包主要定义了 Buffer 及其子类。 Buffer 定义了一个线性存放 primitive type 数据的容器接口。对于除 boolean 以外的其他 primitive type ,都有一个相应的 Buffer 子类, ByteBuffer 是其中最重要的一个子类。

下面这张 UML 类图描述了 java.nio 中的类的关系:

Buffer

定义了一个可以线性存放 primitive type 数据的容器接口。 Buffer 主要包含了与类型( byte, char… )无关的功能。值得注意的是 Buffer 及其子类都不是线程安全的。

每个 Buffer 都有以下的属性:

capacity

这个 Buffer 最多能放多少数据。 capacity 一般在 buffer 被创建的时候指定。

limit

在 Buffer 上进行的读写操作都不能越过这个下标。当写数据到 buffer 中时, limit 一般和 capacity 相等,当读数据时, limit 代表 buffer 中有效数据的长度。

position

读 / 写操作的当前下标。当使用 buffer 的相对位置进行读 / 写操作时,读 / 写会从这个下标进行,并在操作完成后, buffer 会更新下标的值。

mark

一个临时存放的位置下标。调用 mark() 会将 mark 设为当前的 position 的值,以后调用 reset() 会将 position 属性设置为 mark 的值。 mark 的值总是小于等于 position 的值,如果将 position 的值设的比 mark 小,当前的 mark 值会被抛弃掉。

这些属性总是满足以下条件:

0 <= mark <= position <= limit <= capacity

limit 和 position 的值除了通过 limit() 和 position() 函数来设置,也可以通过下面这些函数来改变:

Buffer clear()

把 position 设为 0 ,把 limit 设为 capacity ,一般在把数据写入 Buffer 前调用。

Buffer flip()

把 limit 设为当前 position ,把 position 设为 0 ,一般在从 Buffer 读出数据前调用。

Buffer rewind()

把 position 设为 0 , limit 不变,一般在把数据重写入 Buffer 前调用。

Buffer 对象有可能是只读的,这时,任何对该对象的写操作都会触发一个 ReadOnlyBufferException 。 isReadOnly() 方法可以用来判断一个 Buffer 是否只读。

ByteBuffer

在 Buffer 的子类中, ByteBuffer 是一个地位较为特殊的类,因为在 java.io.channels 中定义的各种 channel 的 IO 操作基本上都是围绕 ByteBuffer 展开的。

ByteBuffer 定义了 4 个 static 方法来做创建工作:

ByteBuffer allocate(int capacity)

创建一个指定 capacity 的 ByteBuffer 。

ByteBuffer allocateDirect(int capacity)

创建一个 direct 的 ByteBuffer ,这样的 ByteBuffer 在参与 IO 操作时性能会更好(很有可能是在底层的实现使用了 DMA 技术),相应的,创建和回收 direct 的 ByteBuffer 的代价也会高一些。 isDirect() 方法可以检查一个 buffer 是否是 direct 的。

ByteBuffer wrap(byte [] array)

ByteBuffer wrap(byte [] array, int offset, int length)

把一个 byte 数组或 byte 数组的一部分包装成 ByteBuffer 。

ByteBuffer 定义了一系列 get 和 put 操作来从中读写 byte 数据,如下面几个:

byte get()

ByteBuffer get(byte [] dst)

byte get(int index)

ByteBuffer put(byte b)

ByteBuffer put(byte [] src)

ByteBuffer put(int index, byte b)

这些操作可分为绝对定位和相对定为两种,相对定位的读写操作依靠 position 来定位 Buffer 中的位置,并在操作完成后会更新 position 的值。

在其它类型的 buffer 中,也定义了相同的函数来读写数据,唯一不同的就是一些参数和返回值的类型。

除了读写 byte 类型数据的函数, ByteBuffer 的一个特别之处是它还定义了读写其它 primitive 数据的方法,如:

int getInt()

     从 ByteBuffer 中读出一个 int 值。

ByteBuffer putInt(int value)

     写入一个 int 值到 ByteBuffer 中。

读写其它类型的数据牵涉到字节序问题, ByteBuffer 会按其字节序(大字节序或小字节序)写入或读出一个其它类型的数据( int,long… )。字节序可以用 order 方法来取得和设置:

ByteOrder order()

     返回 ByteBuffer 的字节序。

ByteBuffer order(ByteOrder bo)

     设置 ByteBuffer 的字节序。

ByteBuffer 另一个特别的地方是可以在它的基础上得到其它类型的 buffer 。如:

CharBuffer asCharBuffer()

为当前的 ByteBuffer 创建一个 CharBuffer 的视图。在该视图 buffer 中的读写操作会按照 ByteBuffer 的字节序作用到 ByteBuffer 中的数据上。

用这类方法创建出来的 buffer 会从 ByteBuffer 的 position 位置开始到 limit 位置结束,可以看作是这段数据的视图。视图 buffer 的 readOnly 属性和 direct 属性与 ByteBuffer 的一致,而且也只有通过这种方法,才可以得到其他数据类型的 direct buffer 。

ByteOrder

用来表示 ByteBuffer 字节序的类,可将其看成 java 中的 enum 类型。主要定义了下面几个 static 方法和属性:

ByteOrder BIG_ENDIAN

     代表大字节序的 ByteOrder 。

ByteOrder LITTLE_ENDIAN

     代表小字节序的 ByteOrder 。

ByteOrder nativeOrder()

     返回当前硬件平台的字节序。

MappedByteBuffer

ByteBuffer 的子类,是文件内容在内存中的映射。这个类的实例需要通过 FileChannel 的 map() 方法来创建。

接下来看看一个使用 ByteBuffer 的例子,这个例子从标准输入不停地读入字符,当读满一行后,将收集的字符写到标准输出:

 **public** **static** **void** main(String [] args)

    **throws** IOException

 {

    //   创建一个 capacity 为 256 的 ByteBuffer

    ByteBuffer buf = ByteBuffer.allocate(256);

    **while** ( **true** ) {

        //   从标准输入流读入一个字符

        **int** c = System.in.read();

        //   当读到输入流结束时,退出循环

        **if** (c == -1)

           **break** ;



        //   把读入的字符写入 ByteBuffer 中

        buf.put(( **byte** ) c);

        //   当读完一行时,输出收集的字符

        **if** (c == '\n' ) {

           //   调用 flip() 使 limit 变为当前的 position 的值 ,position 变为 0,

           //   为接下来从 ByteBuffer 读取做准备

           buf.flip();

           //   构建一个 byte 数组

           **byte** [] content = **new** **byte** [buf.limit()];

           //   从 ByteBuffer 中读取数据到 byte 数组中

           buf.get(content);

             //   把 byte 数组的内容写到标准输出

           System.out.print( **new** String(content));

           //   调用 clear() 使 position 变为 0,limit 变为 capacity 的值,

           //   为接下来写入数据到 ByteBuffer 中做准备

           buf.clear();

        }

    }

 }

Package java.nio.channels

这个包定义了 Channel 的概念, Channel 表现了一个可以进行 IO 操作的通道(比如,通过 FileChannel ,我们可以对文件进行读写操作)。 java.nio.channels 包含了文件系统和网络通讯相关的 channel 类。这个包通过 Selector 和 SelectableChannel 这两个类,还定义了一个进行非阻塞( non-blocking ) IO 操作的 API ,这对需要高性能 IO 的应用非常重要。

下面这张 UML 类图描述了 java.nio.channels 中 interface 的关系:

Channel

Channel 表现了一个可以进行 IO 操作的通道,该 interface 定义了以下方法:

boolean isOpen()

     该 Channel 是否是打开的。

void close()

     关闭这个 Channel ,相关的资源会被释放。

ReadableByteChannel

定义了一个可从中读取 byte 数据的 channel interface 。

int read(ByteBuffer dst)

从 channel 中读取 byte 数据并写到 ByteBuffer 中。返回读取的 byte 数。

WritableByteChannel

定义了一个可向其写 byte 数据的 channel interface 。

int write(ByteBuffer src)

     从 ByteBuffer 中读取 byte 数据并写到 channel 中。返回写出的 byte 数。

ByteChannel

ByteChannel 并没有定义新的方法,它的作用只是把 ReadableByteChannel 和 WritableByteChannel 合并在一起。

ScatteringByteChannel

继承了 ReadableByteChannel 并提供了同时往几个 ByteBuffer 中写数据的能力。

GatheringByteChannel

继承了 WritableByteChannel 并提供了同时从几个 ByteBuffer 中读数据的能力。

InterruptibleChannel

用来表现一个可以被异步关闭的 Channel 。这表现在两方面:

1. 当一个 InterruptibleChannel 的 close() 方法被调用时,其它 block 在这个 InterruptibleChannel 的 IO 操作上的线程会接收到一个 AsynchronousCloseException 。

2. 当一个线程 block 在 InterruptibleChannel 的 IO 操作上时,另一个线程调用该线程的 interrupt() 方法会导致 channel 被关闭,该线程收到一个 ClosedByInterruptException ,同时线程的 interrupt 状态会被设置。

接下来的这张 UML 类图描述了 java.nio.channels 中类的关系:

非阻塞 IO

非阻塞 IO 的支持可以算是 NIO API 中最重要的功能,非阻塞 IO 允许应用程序同时监控多个 channel 以提高性能,这一功能是通过 Selector , SelectableChannel 和 SelectionKey 这 3 个类来实现的。

SelectableChannel 代表了可以支持非阻塞 IO 操作的 channel ,可以将其注册在 Selector 上,这种注册的关系由 SelectionKey 这个类来表现(见 UML 图)。 Selector 这个类通过 select() 函数,给应用程序提供了一个可以同时监控多个 IO channel 的方法:

应用程序通过调用 select() 函数,让 Selector 监控注册在其上的多个 SelectableChannel ,当有 channel 的 IO 操作可以进行时, select() 方法就会返回以让应用程序检查 channel 的状态,并作相应的处理。

下面是 JDK 1.4 中非阻塞 IO 的一个例子,这段 code 使用了非阻塞 IO 实现了一个 time server :

 **private** **static** **void** acceptConnections( **int** port) **throws** Exception {

    //   打开一个 Selector

    Selector acceptSelector =

        SelectorProvider.provider().openSelector();



    //   创建一个 ServerSocketChannel ,这是一个 SelectableChannel 的子类

    ServerSocketChannel ssc = ServerSocketChannel.open();

    //   将其设为 non-blocking 状态,这样才能进行非阻塞 IO 操作

    ssc.configureBlocking( **false** );



    //   给 ServerSocketChannel 对应的 socket 绑定 IP 和端口

    InetAddress lh = InetAddress.getLocalHost();

    InetSocketAddress isa = **new** InetSocketAddress(lh, port);

    ssc.socket().bind(isa);



    //   将 ServerSocketChannel 注册到 Selector 上,返回对应的 SelectionKey

    SelectionKey acceptKey =

        ssc.register(acceptSelector, SelectionKey.OP_ACCEPT);



    **int** keysAdded = 0;



    //   用 select() 函数来监控注册在 Selector 上的 SelectableChannel

    //   返回值代表了有多少 channel 可以进行 IO 操作  (ready for IO)

    **while** ((keysAdded = acceptSelector.select()) > 0) {

        // selectedKeys()  返回一个 SelectionKey 的集合,

        //   其中每个 SelectionKey 代表了一个可以进行 IO 操作的 channel 。

        //   一个 ServerSocketChannel 可以进行 IO 操作意味着有新的 TCP 连接连入了

        Set readyKeys = acceptSelector.selectedKeys();

        Iterator i = readyKeys.iterator();



        **while** (i.hasNext()) {

           SelectionKey sk = (SelectionKey) i.next();

           //   需要将处理过的 key 从 selectedKeys 这个集合中删除

           i.remove();

           //   从 SelectionKey 得到对应的 channel

           ServerSocketChannel nextReady =

               (ServerSocketChannel) sk.channel();

           //   接受新的 TCP 连接

           Socket s = nextReady.accept().socket();

           //   把当前的时间写到这个新的 TCP 连接中

           PrintWriter out =

               **new** PrintWriter(s.getOutputStream(), **true** );

           Date now = **new** Date();

           out.println(now);

           //   关闭连接

           out.close();

        }

    }

 }

这是个纯粹用于演示的例子,因为只有一个 ServerSocketChannel 需要监控,所以其实并不真的需要使用到非阻塞 IO 。不过正因为它的简单,可以很容易地看清楚非阻塞 IO 是如何工作的。

SelectableChannel

这个抽象类是所有支持非阻塞 IO 操作的 channel (如 DatagramChannel 、 SocketChannel )的父类。 SelectableChannel 可以注册到一个或多个 Selector 上以进行非阻塞 IO 操作。

SelectableChannel 可以是 blocking 和 non-blocking 模式(所有 channel 创建的时候都是 blocking 模式),只有 non-blocking 的 SelectableChannel 才可以参与非阻塞 IO 操作。

SelectableChannel configureBlocking(boolean block)

     设置 blocking 模式。

boolean isBlocking()

     返回 blocking 模式。

通过 register() 方法, SelectableChannel 可以注册到 Selector 上。

int validOps()

返回一个 bit mask ,表示这个 channel 上支持的 IO 操作。当前在 SelectionKey 中,用静态常量定义了 4 种 IO 操作的 bit 值: OP_ACCEPT , OP_CONNECT , OP_READ 和 OP_WRITE 。

SelectionKey register(Selector sel, int ops)

将当前 channel 注册到一个 Selector 上并返回对应的 SelectionKey 。在这以后,通过调用 Selector 的 select() 函数就可以监控这个 channel 。 ops 这个参数是一个 bit mask ,代表了需要监控的 IO 操作。

SelectionKey register(Selector sel, int ops, Object att)

这个函数和上一个的意义一样,多出来的 att 参数会作为 attachment 被存放在返回的 SelectionKey 中,这在需要存放一些 session state 的时候非常有用。

boolean isRegistered()

     该 channel 是否已注册在一个或多个 Selector 上。

SelectableChannel 还提供了得到对应 SelectionKey 的方法:

SelectionKey keyFor(Selector sel)

返回该 channe 在 Selector 上的注册关系所对应的 SelectionKey 。若无注册关系,返回 null 。

Selector

Selector 可以同时监控多个 SelectableChannel 的 IO 状况,是非阻塞 IO 的核心。

Selector open()

   Selector 的一个静态方法,用于创建实例。

在一个 Selector 中,有 3 个 SelectionKey 的集合:

1. key set 代表了所有注册在这个 Selector 上的 channel ,这个集合可以通过 keys() 方法拿到。

2. Selected-key set 代表了所有通过 select() 方法监测到可以进行 IO 操作的 channel ,这个集合可以通过 selectedKeys() 拿到。

3. Cancelled-key set 代表了已经 cancel 了注册关系的 channel ,在下一个 select() 操作中,这些 channel 对应的 SelectionKey 会从 key set 和 cancelled-key set 中移走。这个集合无法直接访问。

以下是 select() 相关方法的说明:

int select()

监控所有注册的 channel ,当其中有注册的 IO 操作可以进行时,该函数返回,并将对应的 SelectionKey 加入 selected-key set 。

int select(long timeout)

     可以设置超时的 select() 操作。

int selectNow()

     进行一个立即返回的 select() 操作。

Selector wakeup()

     使一个还未返回的 select() 操作立刻返回。

SelectionKey

代表了 Selector 和 SelectableChannel 的注册关系。

Selector 定义了 4 个静态常量来表示 4 种 IO 操作,这些常量可以进行位操作组合成一个 bit mask 。

int OP_ACCEPT

有新的网络连接可以 accept , ServerSocketChannel 支持这一非阻塞 IO 。

int OP_CONNECT

     代表连接已经建立(或出错), SocketChannel 支持这一非阻塞 IO 。

int OP_READ

int OP_WRITE

     代表了读、写操作。

以下是其主要方法:

Object attachment()

返回 SelectionKey 的 attachment , attachment 可以在注册 channel 的时候指定。

Object attach(Object ob)

     设置 SelectionKey 的 attachment 。

SelectableChannel channel()

     返回该 SelectionKey 对应的 channel 。

Selector selector()

     返回该 SelectionKey 对应的 Selector 。

void cancel()

   cancel 这个 SelectionKey 所对应的注册关系。

int interestOps()

     返回代表需要 Selector 监控的 IO 操作的 bit mask 。

SelectionKey interestOps(int ops)

     设置 interestOps 。

int readyOps()

     返回一个 bit mask ,代表在相应 channel 上可以进行的 IO 操作。

ServerSocketChannel

支持非阻塞操作,对应于 java.net.ServerSocket 这个类,提供了 TCP 协议 IO 接口,支持 OP_ACCEPT 操作。

ServerSocket socket()

     返回对应的 ServerSocket 对象。

SocketChannel accept()

     接受一个连接,返回代表这个连接的 SocketChannel 对象。

SocketChannel

支持非阻塞操作,对应于 java.net.Socket 这个类,提供了 TCP 协议 IO 接口,支持 OP_CONNECT , OP_READ 和 OP_WRITE 操作。这个类还实现了 ByteChannel , ScatteringByteChannel 和 GatheringByteChannel 接口。

DatagramChannel 和这个类比较相似,其对应于 java.net.DatagramSocket ,提供了 UDP 协议 IO 接口。

Socket socket()

     返回对应的 Socket 对象。

boolean connect(SocketAddress remote)

boolean finishConnect()

connect() 进行一个连接操作。如果当前 SocketChannel 是 blocking 模式,这个函数会等到连接操作完成或错误发生才返回。如果当前 SocketChannel 是 non-blocking 模式,函数在连接能立刻被建立时返回 true ,否则函数返回 false ,应用程序需要在以后用 finishConnect() 方法来完成连接操作。

Pipe

包含了一个读和一个写的 channel(Pipe.SourceChannel 和 Pipe.SinkChannel) ,这对 channel 可以用于进程中的通讯。

FileChannel

用于对文件的读、写、映射、锁定等操作。和映射操作相关的类有 FileChannel.MapMode ,和锁定操作相关的类有 FileLock 。值得注意的是 FileChannel 并不支持非阻塞操作。

Channels

这个类提供了一系列 static 方法来支持 stream 类和 channel 类之间的互操作。这些方法可以将 channel 类包装为 stream 类,比如,将 ReadableByteChannel 包装为 InputStream 或 Reader ;也可以将 stream 类包装为 channel 类,比如,将 OutputStream 包装为 WritableByteChannel 。

Package java.nio.charset

这个包定义了 Charset 及相应的 encoder 和 decoder 。下面这张 UML 类图描述了这个包中类的关系,可以将其中 Charset , CharsetDecoder 和 CharsetEncoder 理解成一个 Abstract Factory 模式的实现:

Charset

代表了一个字符集,同时提供了 factory method 来构建相应的 CharsetDecoder 和 CharsetEncoder 。

Charset 提供了以下 static 的方法:

SortedMap availableCharsets()

     返回当前系统支持的所有 Charset 对象,用 charset 的名字作为 set 的 key 。

boolean isSupported(String charsetName)

     判断该名字对应的字符集是否被当前系统支持。

Charset forName(String charsetName)

     返回该名字对应的 Charset 对象。

Charset 中比较重要的方法有:

String name()

     返回该字符集的规范名。

Set aliases()

     返回该字符集的所有别名。

CharsetDecoder newDecoder()

     创建一个对应于这个 Charset 的 decoder 。

CharsetEncoder newEncoder()

     创建一个对应于这个 Charset 的 encoder 。

CharsetDecoder

将按某种字符集编码的字节流解码为 unicode 字符数据的引擎。

CharsetDecoder 的输入是 ByteBuffer ,输出是 CharBuffer 。进行 decode 操作时一般按如下步骤进行:

1. 调用 CharsetDecoder 的 reset() 方法。(第一次使用时可不调用)

2. 调用 decode() 方法 0 到 n 次,将 endOfInput 参数设为 false ,告诉 decoder 有可能还有新的数据送入。

3. 调用 decode() 方法最后一次,将 endOfInput 参数设为 true ,告诉 decoder 所有数据都已经送入。

4. 调用 decoder 的 flush() 方法。让 decoder 有机会把一些内部状态写到输出的 CharBuffer 中。

CharsetDecoder reset()

     重置 decoder ,并清除 decoder 中的一些内部状态。

CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)

从 ByteBuffer 类型的输入中 decode 尽可能多的字节,并将结果写到 CharBuffer 类型的输出中。根据 decode 的结果,可能返回 3 种 CoderResult : CoderResult.UNDERFLOW 表示已经没有输入可以 decode ; CoderResult.OVERFLOW 表示输出已满;其它的 CoderResult 表示 decode 过程中有错误发生。根据返回的结果,应用程序可以采取相应的措施,比如,增加输入,清除输出等等,然后再次调用 decode() 方法。

CoderResult flush(CharBuffer out)

有些 decoder 会在 decode 的过程中保留一些内部状态,调用这个方法让这些 decoder 有机会将这些内部状态写到输出的 CharBuffer 中。调用成功返回 CoderResult.UNDERFLOW 。如果输出的空间不够,该函数返回 CoderResult.OVERFLOW ,这时应用程序应该扩大输出 CharBuffer 的空间,然后再次调用该方法。

CharBuffer decode(ByteBuffer in)

一个便捷的方法把 ByteBuffer 中的内容 decode 到一个新创建的 CharBuffer 中。在这个方法中包括了前面提到的 4 个步骤,所以不能和前 3 个函数一起使用。

decode 过程中的错误有两种: malformed-input CoderResult 表示输入中数据有误; unmappable-character CoderResult 表示输入中有数据无法被解码成 unicode 的字符。如何处理 decode 过程中的错误取决于 decoder 的设置。对于这两种错误, decoder 可以通过 CodingErrorAction 设置成:

1. 忽略错误

2. 报告错误。(这会导致错误发生时, decode() 方法返回一个表示该错误的 CoderResult 。)

3. 替换错误,用 decoder 中的替换字串替换掉有错误的部分。

CodingErrorAction malformedInputAction()

     返回 malformed-input 的出错处理。

CharsetDecoder onMalformedInput(CodingErrorAction newAction)

     设置 malformed-input 的出错处理。

CodingErrorAction unmappableCharacterAction()

     返回 unmappable-character 的出错处理。

CharsetDecoder onUnmappableCharacter(CodingErrorAction newAction)

     设置 unmappable-character 的出错处理。

String replacement()

     返回 decoder 的替换字串。

CharsetDecoder replaceWith(String newReplacement)

     设置 decoder 的替换字串。

CharsetEncoder

将 unicode 字符数据编码为特定字符集的字节流的引擎。其接口和 CharsetDecoder 相类似。

CoderResult

描述 encode/decode 操作结果的类。

CodeResult 包含两个 static 成员:

CoderResult OVERFLOW

     表示输出已满

CoderResult UNDERFLOW

     表示输入已无数据可用。

其主要的成员函数有:

boolean isError()

boolean isMalformed()

boolean isUnmappable()

boolean isOverflow()

boolean isUnderflow()

     用于判断该 CoderResult 描述的错误。

int length()

     返回错误的长度,比如,无法被转换成 unicode 的字节长度。

void throwException()

     抛出一个和这个 CoderResult 相对应的 exception 。

CodingErrorAction

表示 encoder/decoder 中错误处理方法的类。可将其看成一个 enum 类型。有以下 static 属性:

CodingErrorAction IGNORE

     忽略错误。

CodingErrorAction REPLACE

     用替换字串替换有错误的部分。

CodingErrorAction REPORT

报告错误,对于不同的函数,有可能是返回一个和错误有关的 CoderResult ,也有可能是抛出一个 CharacterCodingException 。 posted on 2007-01-12 20:07 ☜♥☞MengChuChen 阅读(6999) 评论(0) 编辑 收藏

新用户注册 刷新评论列表

推荐购买云服务器(15%返利+最高千元奖金) 博客园 博问 IT新闻 Java程序员招聘 标题 请输入标题 姓名 请输入你的姓名 主页 请输入验证码 验证码 /* 内容(请不要发表任何与政治相关的内容) 请输入评论内容 Remember Me? 登录 [使用Ctrl+Enter键可以直接提交] 网站导航:

博客园 IT新闻 知识库 C++博客 程序员招聘 管理
Copyright ©2013 ☜♥☞MengChuChen Powered By博客园 模板提供:沪江博客

Java 内存泄露监控工具

Posted on

Java 内存泄露监控工具-- JVM监控工具介绍jstack, jconsole, jinfo, jmap, jdb, jstat


jstack -- 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到 当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。目前只有在Solaris和Linux的JDK版本里面才有。

jconsole – jconsole是基于Java Management Extensions (JMX)的实时图形化监测工具,这个工具利用了内建到JVM里面的JMX指令来提供实时的性能和资源的监控,包括了Java 程序的内存使用,Heap size, 线程的状态,类的分配状态和空间使用等等。

jinfo – jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息,目前只有在Solaris和Linux的JDK版本里面才有。

jmap – jmap 可以从core文件或进程中获得内存的具体匹配情况,包括Heap size, Perm size等等,目前只有在Solaris和Linux的JDK版本里面才有。

jdb – jdb 用来对core文件和正在运行的Java进程进行实时地调试,里面包含了丰富的命令帮助您进行调试,它的功能和Sun studio里面所带的dbx非常相似,但 jdb是专门用来针对Java应用程序的。

jstat – jstat利用了JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控等等。

jps – jps是用来查看JVM里面所有进程的具体状态, 包括进程ID,进程启动的路径等等。

jstatd 启动jvm监控服务。它是一个基于rmi的应用,向远程机器提供本机jvm应用程序的信息。默认端口1099。 实例:jstatd -J-Djava.security.policy=my.policy my.policy文件需要自己建立,内如如下: grant codebase "file:$JAVA_HOME/lib/tools.jar" { permission java.security.AllPermission; }; 这是安全策略文件,因为jdk对jvm做了jaas的安全检测,所以我们必须设置一些策略,使得jstatd被允许作网络操作

上面的操作没有通过,出现:

Could not create remote object access denied (java.util.PropertyPermission java.rmi.server.ignoreSubClasses write) java.security.AccessControlException: access denied (java.util.PropertyPermission java.rmi.server.ignoreSubClasses write) at java.security.AccessControlContext.checkPermission(AccessControlContext.java:323) at java.security.AccessController.checkPermission(AccessController.java:546) at java.lang.SecurityManager.checkPermission(SecurityManager.java:532) at java.lang.System.setProperty(System.java:727) at sun.tools.jstatd.Jstatd.main(Jstatd.java:122)

create in your usr/java/bin the jstatd.all.policy file, with the content must be

  1. grant codebase "file:${java.home}/../lib/tools.jar" {
  2. permission java.security.AllPermission;
  3. };

jps 列出所有的jvm实例 实例: jps 列出本机所有的jvm实例 jps 192.168.0.77 列出远程服务器192.168.0.77机器所有的jvm实例,采用rmi协议,默认连接端口为1099 (前提是远程服务器提供jstatd服务) 输出内容如下: jones@jones:~/data/ebook/java/j2se/jdk_gc$ jps 6286 Jps 6174 Jstat jconsole 一个图形化界面,可以观察到java进程的gc,class,内存等信息。虽然比较直观,但是个人还是比较倾向于使用jstat命令(在最后一部分会对jstat作详细的介绍)。 jinfo (linux下特有) 观察运行中的java程序的运行环境参数:参数包括Java System属性和JVM命令行参数 实例:jinfo 2083 其中2083就是java进程id号,可以用jps得到这个id号。 输出内容太多了,不在这里一一列举,大家可以自己尝试这个命令。 jstack (linux下特有) 可以观察到jvm中当前所有线程的运行情况和线程当前状态 jstack 2083 输出内容如下: jmap (linux下特有,也是很常用的一个命令) 观察运行中的jvm物理内存的占用情况。 参数如下:-heap :打印jvm heap的情况 -histo: 打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。 -histo:live : 同上,但是只答应存活对象的情况 -permstat: 打印permanent generation heap情况 命令使用: jmap -heap 2083 可以观察到New Generation(Eden Space,From Space,To Space),tenured generation,Perm Generation的内存使用情况 输出内容: jmap -histo 2083 | jmap -histo:live 2083 可以观察heap中所有对象的情况(heap中所有生存的对象的情况)。包括对象数量和所占空间大小。 输出内容: 写个脚本,可以很快把占用heap最大的对象找出来,对付内存泄漏特别有效。 jstat 最后要重点介绍下这个命令。 这是jdk命令中比较重要,也是相当实用的一个命令,可以观察到classloader,compiler,gc相关信息 具体参数如下: -class:统计class loader行为信息 -compile:统计编译行为信息 -gc:统计jdk gc时heap信息 -gccapacity:统计不同的generations(不知道怎么翻译好,包括新生区,老年区,permanent区)相应的heap容量情况 -gccause:统计gc的情况,(同-gcutil)和引起gc的事件 -gcnew:统计gc时,新生代的情况 -gcnewcapacity:统计gc时,新生代heap容量 -gcold:统计gc时,老年区的情况 -gcoldcapacity:统计gc时,老年区heap容量 -gcpermcapacity:统计gc时,permanent区heap容量 -gcutil:统计gc时,heap情况 -printcompilation:不知道干什么的,一直没用过。 一般比较常用的几个参数是: jstat -class 2083 1000 10 (每隔1秒监控一次,一共做10次) 输出内容含义如下:

Loaded Number of classes loaded. Bytes Number of Kbytes loaded. Unloaded Number of classes unloaded. Bytes Number of Kbytes unloaded. Time Time spent performing class load and unload operations. jstat -gc 2083 2000 20(每隔2秒监控一次,共做10) 输出内容含义如下: S0C Current survivor space 0 capacity (KB). EC Current eden space capacity (KB). EU Eden space utilization (KB). OC Current old space capacity (KB). OU Old space utilization (KB). PC Current permanent space capacity (KB). PU Permanent space utilization (KB). YGC Number of young generation GC Events. YGCT Young generation garbage collection time. FGC Number of full GC events. FGCT Full garbage collection time. GCT Total garbage collection time. 输出内容: 如果能熟练运用这些命令,尤其是在linux下,那么完全可以代替jprofile等监控工具了,谁让它收费呢。呵呵。 用命令的好处就是速度快,并且辅助于其他命令,比如grep gawk sed等,可以组装多种符合自己需求的工具。

u jps 的用法

用来查看 JVM 里面所有进程的具体状态 , 包括进程 ID ,进程启动的路径等等。 与 unix 上的 ps 类似,用来显示本地的java 进程,可以查看本地运行着几个 java 程序,并显示他们的进程号。

[root@localhost ~]/# jps

25517 Jps

25444 Bootstrap

u jstack 的用法

如果 java 程序崩溃生成 core 文件, jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。另外, jstack 工具还可以附属到正在运行的 java 程序中,看到当时运行的 java 程序的 java stack 和 native stack 的信息 , 如果现在运行的 java 程序呈现 hung 的状态, jstack 是非常有用的。目前只有在 Solaris 和 Linux 的 JDK 版本里面才有。

[root@localhost bin]/# jstack **25444**

Attaching to process ID 25917, please wait...

Debugger attached successfully.

Client compiler detected.

JVM version is 1.5.0_08-b03

Thread 25964: (state = BLOCKED)

Error occurred during stack walking:

sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: get_thread_regs failed for a lwp

    at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:134)

    at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.getThreadIntegerRegisterSet(LinuxDebuggerLocal.java:437)

    at sun.jvm.hotspot.debugger.linux.LinuxThread.getContext(LinuxThread.java:48)

    at

u jstat 的用法

用以判断JVM 是否存在内存问题呢?如何判断JVM 垃圾回收是否正常?一般的top 指令基本上满足不了这样的需求,因为它主要监控的是总体的系统资源,很难定位到java 应用程序。

Jstat 是JDK 自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool” ,它位于java 的bin 目录下,主要利用JVM 内建的指令对Java 应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size 和垃圾回收状况的监控。可见,Jstat 是轻量级的、专门针对JVM 的工具,非常适用。由于JVM 内存设置较大,图中百分比变化不太明显

一个极强的监视 VM 内存工具。可以用来监视 VM 内存内的各种堆和非堆的大小及其内存使用量。

jstat 工具特别强大,有众多的可选项,详细查看堆内各个部分的使用量,以及加载类的数量。使用时,需加上查看进程的进程 id ,和所选参数。

语法结构:

Usage: jstat -help|-options

   jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

参数解释:

Options — 选项,我们一般使用 -gcutil 查看gc 情况

vmid — VM 的进程号,即当前运行的java 进程号

interval– 间隔时间,单位为秒或者毫秒

count — 打印次数,如果缺省则打印无数次

S0 — Heap 上的 Survivor space 0 区已使用空间的百分比 S1 — Heap 上的 Survivor space 1 区已使用空间的百分比 E — Heap 上的 Eden space 区已使用空间的百分比 O — Heap 上的 Old space 区已使用空间的百分比 P — Perm space 区已使用空间的百分比 YGC — 从应用程序启动到采样时发生 Young GC 的次数 YGCT– 从应用程序启动到采样时 Young GC 所用的时间( 单位秒 ) FGC — 从应用程序启动到采样时发生 Full GC 的次数 FGCT– 从应用程序启动到采样时 Full GC 所用的时间( 单位秒 ) GCT — 从应用程序启动到采样时用于垃圾回收的总时间( 单位秒)

实例使用1 :

[root@localhost bin]/# jstat -gcutil 25444

S0 S1 E O P YGC YGCT FGC FGCT GCT

11.63 0.00 56.46 66.92 98.49 162 0.248 6 0.331 0.579

实例使用 2 :

[root@localhost bin]/# jstat -gcutil 25444 1000 5

S0 S1 E O P YGC YGCT FGC FGCT GCT

73.54 0.00 99.04 67.52 98.49 166 0.252 6 0.331 0.583

73.54 0.00 99.04 67.52 98.49 166 0.252 6 0.331 0.583

73.54 0.00 99.04 67.52 98.49 166 0.252 6 0.331 0.583

73.54 0.00 99.04 67.52 98.49 166 0.252 6 0.331 0.583

73.54 0.00 99.04 67.52 98.49 166 0.252 6 0.331 0.583

我们可以看到,5 次young gc 之后,垃圾内存被从Eden space 区(E) 放入了Old space 区(O) ,并引起了百分比的变化,导致Survivor space 使用的百分比从73.54%(S0) 降到0%(S1) 。有效释放了内存空间。绿框中,我们可以看到,一次full gc 之后,Old space 区(O) 的内存被回收,从99.05% 降到67.52% 。

图中同时打印了young gc 和full gc 的总次数、总耗时。而,每次young gc 消耗的时间,可以用相间隔的两行YGCT 相减得到。每次full gc 消耗的时间,可以用相隔的两行FGCT 相减得到。例如红框中表示的第一行、第二行之间发生了1次young gc ,消耗的时间为0.252-0.252 =0.0 秒。

常驻内存区(P) 的使用率,始终停留在98.49% 左右,说明常驻内存没有突变,比较正常。

如果young gc 和full gc 能够正常发生,而且都能有效回收内存,常驻内存区变化不明显,则说明java 内存释放情况正常,垃圾回收及时,java 内存泄露的几率就会大大降低。但也不能说明一定没有内存泄露。

GCT 是YGCT 和FGCT 的时间总和。

以上,介绍了Jstat 按百分比查看gc 情况的功能。其实,它还有功能,例如加载类信息统计功能、内存池信息统计功能等,那些是以绝对值的形式打印出来的,比较少用,在此就不做介绍。

[root@localhost bin]/# ps -ef | grep java

root 25917 1 2 23:23 pts /2 00:00:05 /usr/local/jdk1.5/bin/java -Djava.endorsed.dirs=/usr/local/jakarta-tomcat-5.0.30/common/endorsed -classpath /usr/local/jdk1.5/lib/tools.jar:/usr/local/jakarta-tomcat-5.0.30/bin/bootstrap.jar:/usr/local/jakarta-tomcat-5.0.30/bin/commons-logging-api.jar -Dcatalina.base=/usr/local/jakarta-tomcat-5.0.30 -Dcatalina.home=/usr/local/jakarta-tomcat-5.0.30 -Djava.io.tmpdir=/usr/local/jakarta-tomcat-5.0.30/temp org.apache.catalina.startup.Bootstrap start

jstat -class pid: 显示加载 class 的数量,及所占空间等信息。

实例使用3 :

[root@localhost bin]/# jstat -class 25917

Loaded Bytes Unloaded Bytes Time

2629 2916.8 29 24.6 0.90

jstat -compiler pid: 显示 VM 实时编译的数量等信息。

实例使用 4 :

[root@localhost bin]/# jstat -compiler 25917

Compiled Failed Invalid Time FailedType FailedMethod

 768      0       0   0.70             0

jstat –gccapacity : 可以显示, VM 内存中三代( young,old,perm )对象的使用和占用大小,如: PGCMN 显示的是最小 perm 的内存使用量, PGCMX 显示的是 perm 的内存最大使用量, PGC 是当前新生成的 perm 内存占用量, PC 是但前 perm 内存占用量。其他的可以根据这个类推, OC 是 old 内纯的占用量。

[root@localhost bin]/# jstat -gccapacity 25917

NGCMN 640.0

NGCMX 4992.0

NGC 832.0

S0C 64.0

S1C 64.0

EC 704.0

OGCMN 1408.0

OGCMX 60544.0

OGC 9504.0

OC 9504.0 OC 是 old 内纯的占用量

PGCMN 8192.0 PGCMN 显示的是最小 perm 的内存使用量

PGCMX 65536.0 PGCMX 显示的是 perm 的内存最大使用量

PGC 12800.0 PGC 是当前新生成的 perm 内存占用量

PC 12800.0 PC 是但前 perm 内存占用量

YGC 164

FGC 6

jstat -gcnew pid: new 对象的信息

[root@localhost bin]/# jstat -gcnew 25917

S0C S1C S0U S1U TT MTT DSS EC EU YGC YGCT

64.0 64.0 47.4 0.0 2 15 32.0 704.0 145.7 168 0.254

jstat -gcnewcapacity pid: new 对象的信息及其占用量

[root@localhost bin]/# jstat -gcnewcapacity 25917

NGCMN NGCMX NGC S0CMX S0C S1CMX S1C ECMX EC YGC FGC

640.0 4992.0 832.0 64.0 448.0 448.0 64.0 4096.0 704.0 168 6

jstat -gcold pid: old 对象的信息。

[root@localhost bin]/# jstat -gcold 25917

PC PU OC OU YGC FGC FGCT GCT

12800.0 12617.6 9504.0 6561.3 169 6 0.335 0.591

jstat -gcoldcapacity pid:old 对象的信息及其占用量。

[root@localhost bin]/# jstat -gcoldcapacity 25917

OGCMN OGCMX OGC OC YGC FGC FGCT GCT

1408.0 60544.0 9504.0 9504.0 169 6 0.335 0.591

jstat -gcpermcapacity pid: perm 对象的信息及其占用量。

[root@localhost bin]/# jstat -gcpermcapacity 25917

PGCMN PGCMX PGC PC YGC FGC FGCT GCT

8192.0 65536.0 12800.0 12800.0 169 6 0.335 0.591

jstat -printcompilation pid: 当前 VM 执行的信息。

[root@localhost bin]/# jstat -printcompilation -h3 25917 1000 5

每 1000 毫秒打印一次,一共打印 5 次,还可以加上 -h3 每三行显示一下标题。

Compiled Size Type Method

 788     73    1 java/io/File <init>

 788     73    1 java/io/File <init>

 788     73    1 java/io/File <init>

Compiled Size Type Method

 788     73    1 java/io/File <init>

 788     73    1 java/io/File <init>

u jmap 的用法

打印出某个 java 进程(使用 pid )内存内的,所有 ‘ 对象 ’ 的情况(如:产生那些对象,及其数量)。

可以输出所有内存中对象的工具,甚至可以将 VM 中的 heap ,以二进制输出成文本。使用方法 jmap -histo pid 。如果连用 SHELL jmap -histo pid>a.log 可以将其保存到文本中去,在一段时间后,使用文本对比工具,可以对比出 GC 回收了哪些对象。 jmap -dump:format=b,file=String 3024 可以将 3024 进程的内存 heap 输出出来到 String 文件里。

[root@localhost bin]/# jmap -histo 25917

Attaching to process ID 26221, please wait...

Debugger attached successfully.

Client compiler detected.

JVM version is 1.5.0_08-b03

Iterating over heap. This may take a while...

Unknown oop at 0xaa6e42d0

Oop's klass is null

Object Histogram:

Size Count Class description


3722768 30467 /* ConstMethodKlass

1976480 25334 char[]

1907880 46994 /* SymbolKlass

1762088 2947 byte[]

1709536 30467 /* MethodKlass

1487816 2600 /* ConstantPoolKlass

1009576 2600 /* InstanceKlassKlass

904880 2199 /* ConstantPoolCacheKlass

741432 30893 java.lang.String

653576 4785 int[]

351760 4397 java.lang.reflect.Method

277824 2894 java.lang.Class

248704 3401 short[]

200888 4411 java.lang.Object[]

193656 4045 java.lang.Object[]

179744 5617 java.util.TreeMap$Entry

175688 1800 java.util.HashMap$Entry[]

165288 6887 java.util.HashMap$Entry

104736 3273 java.lang.ref.SoftReference

104136 4339 java.lang.ref.WeakReference

96096 3521 java.lang.String[]

86160 3590 java.util.Hashtable$Entry

85584 3566 java.util.ArrayList

83472 1206 java.util.Hashtable$Entry[]

82944 1728 java.beans.MethodDescriptor

80560 265 /* ObjArrayKlassKlass

69120 1728 java.util.HashMap

52464 3055 java.lang.Class[]

43040 1076 java.util.Hashtable

42496 664 org.apache.commons.modeler.AttributeInfo

37880 947 java.util.TreeMap

33896 557 javax.management.modelmbean.ModelMBeanAttributeInfo[]

33152 518 java.beans.PropertyDescriptor

616 11 org.springframework.aop.framework.ProxyFactory

608 19 java.util.PropertyPermission

608 38 org.springframework.beans.MutablePropertyValues

608 38 org.springframework.beans.factory.support.MethodOverrides

608 2 /* ArrayKlassKlass

608 38 org.springframework.beans.factory.config.ConstructorArgumentValues

608 4 org.apache.xerces.impl.XMLDTDScannerImpl

576 24 java.util.Stack

576 36 java.util.regex.Pattern$Category

576 24 org.apache.naming.NamingEntry

560 7 java.net.URL[]

552 23 sun.management.MappedMXBeanType$BasicMXBeanType

552 1 java.util.Locale[]

552 22 java.io.ObjectStreamField[]

544 17 java.util.Collections$SynchronizedMap

176 11 java.util.regex.Pattern$Ctype

8 1 sun.reflect.GeneratedMethodAccessor49

8 1 sun.reflect.GeneratedMethodAccessor6

8 1 sun.reflect.GeneratedConstructorAccessor10

Heap traversal took 12.003 seconds.

u jinfo 的用法

可以输出并修改运行时的 java 进程的 opts 。用处比较简单,就是能输出并修改运行时的 java 进程的运行参数。用法是jinfo -opt pid 如:查看 2788 的 MaxPerm 大小可以用 jinfo -flag MaxPermSize 2788 。

u jconsole 的用法

jconsole: 一个 java GUI 监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器 VM 。

用 java 写的 GUI 程序,用来监控 VM ,并可监控远程的 VM ,非常易用,而且功能非常强。命令行里打 jconsole ,选则进程就可以了

不过我没有运行起来,老是报下面的错。会的朋友,帮忙看看。

[root@localhost bin]/# jconsole

Exception in thread "AWT-EventQueue-0" java.awt.HeadlessException:

No X11 DISPLAY variable was set, but this program performed an operation which requires it. at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:159)

    at java.awt.Window.<init>(Window.java:317)

    at java.awt.Frame.<init>(Frame.java:419)

    at javax.swing.JFrame.<init>(JFrame.java:194)

    at sun.tools.jconsole.JConsole.<init>(JConsole.java:65)

    at sun.tools.jconsole.JConsole$4.run(JConsole.java:666)

    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)

    at java.awt.EventQueue.dispatchEvent(EventQueue.java:461)

    at java.awt.EventDispatchThread.pumpOneEventForHierarchy(EventDispatchThread.java:242)

    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:163)

    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:157)

    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:149)

    at java.awt.EventDispatchThread.run(EventDispatchThread.java:110)

u jdb 的用法

用来对 core 文件和正在运行的 Java 进程进行实时地调试,里面包含了丰富的命令帮助您进行调试,它的功能和 Sun studio 里面所带的 dbx 非常相似,但 jdb 是专门用来针对 Java 应用程序的。

u jmap 的用法

打印出某个 java 进程(使用 pid )内存内的,所有 ‘ 对象 ’ 的情况(如:产生那些对象,及其数量)。

可以输出所有内存中对象的工具,甚至可以将 VM 中的 heap ,以二进制输出成文本。使用方法 jmap -histo pid 。如果连用 SHELL jmap -histo pid>a.log 可以将其保存到文本中去,在一段时间后,使用文本对比工具,可以对比出 GC 回收了哪些对象。 jmap -dump:format=b,file=String 3024 可以将 3024 进程的内存 heap 输出出来到 String 文件里。

[root@localhost bin]/# jmap -histo 25917

Attaching to process ID 26221, please wait...

Debugger attached successfully.

Client compiler detected.

JVM version is 1.5.0_08-b03

Iterating over heap. This may take a while...

Unknown oop at 0xaa6e42d0

Oop's klass is null

Object Histogram:

Size Count Class description


3722768 30467 /* ConstMethodKlass

1976480 25334 char[]

1907880 46994 /* SymbolKlass

1762088 2947 byte[]

1709536 30467 /* MethodKlass

1487816 2600 /* ConstantPoolKlass

1009576 2600 /* InstanceKlassKlass

904880 2199 /* ConstantPoolCacheKlass

741432 30893 java.lang.String

653576 4785 int[]

351760 4397 java.lang.reflect.Method

277824 2894 java.lang.Class

248704 3401 short[]

200888 4411 java.lang.Object[]

193656 4045 java.lang.Object[]

179744 5617 java.util.TreeMap$Entry

175688 1800 java.util.HashMap$Entry[]

165288 6887 java.util.HashMap$Entry

104736 3273 java.lang.ref.SoftReference

104136 4339 java.lang.ref.WeakReference

96096 3521 java.lang.String[]

86160 3590 java.util.Hashtable$Entry

85584 3566 java.util.ArrayList

83472 1206 java.util.Hashtable$Entry[]

82944 1728 java.beans.MethodDescriptor

80560 265 /* ObjArrayKlassKlass

69120 1728 java.util.HashMap

52464 3055 java.lang.Class[]

43040 1076 java.util.Hashtable

42496 664 org.apache.commons.modeler.AttributeInfo

37880 947 java.util.TreeMap

33896 557 javax.management.modelmbean.ModelMBeanAttributeInfo[]

33152 518 java.beans.PropertyDescriptor

616 11 org.springframework.aop.framework.ProxyFactory

608 19 java.util.PropertyPermission

608 38 org.springframework.beans.MutablePropertyValues

608 38 org.springframework.beans.factory.support.MethodOverrides

608 2 /* ArrayKlassKlass

608 38 org.springframework.beans.factory.config.ConstructorArgumentValues

608 4 org.apache.xerces.impl.XMLDTDScannerImpl

576 24 java.util.Stack

576 36 java.util.regex.Pattern$Category

576 24 org.apache.naming.NamingEntry

560 7 java.net.URL[]

552 23 sun.management.MappedMXBeanType$BasicMXBeanType

552 1 java.util.Locale[]

552 22 java.io.ObjectStreamField[]

544 17 java.util.Collections$SynchronizedMap

176 11 java.util.regex.Pattern$Ctype

8 1 sun.reflect.GeneratedMethodAccessor49

8 1 sun.reflect.GeneratedMethodAccessor6

8 1 sun.reflect.GeneratedConstructorAccessor10

Heap traversal took 12.003 seconds.

u jinfo 的用法

可以输出并修改运行时的 java 进程的 opts 。用处比较简单,就是能输出并修改运行时的 java 进程的运行参数。用法是jinfo -opt pid 如:查看 2788 的 MaxPerm 大小可以用 jinfo -flag MaxPermSize 2788 。

u jconsole 的用法

jconsole: 一个 java GUI 监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器 VM 。

用 java 写的 GUI 程序,用来监控 VM ,并可监控远程的 VM ,非常易用,而且功能非常强。命令行里打 jconsole ,选则进程就可以了

不过我没有运行起来,老是报下面的错。会的朋友,帮忙看看。

[root@localhost bin]/# jconsole

Exception in thread "AWT-EventQueue-0" java.awt.HeadlessException:

No X11 DISPLAY variable was set, but this program performed an operation which requires it. at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:159)

    at java.awt.Window.<init>(Window.java:317)

    at java.awt.Frame.<init>(Frame.java:419)

    at javax.swing.JFrame.<init>(JFrame.java:194)

    at sun.tools.jconsole.JConsole.<init>(JConsole.java:65)

    at sun.tools.jconsole.JConsole$4.run(JConsole.java:666)

    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)

    at java.awt.EventQueue.dispatchEvent(EventQueue.java:461)

    at java.awt.EventDispatchThread.pumpOneEventForHierarchy(EventDispatchThread.java:242)

    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:163)

    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:157)

    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:149)

    at java.awt.EventDispatchThread.run(EventDispatchThread.java:110)

u jdb 的用法

用来对 core 文件和正在运行的 Java 进程进行实时地调试,里面包含了丰富的命令帮助您进行调试,它的功能和 Sun studio 里面所带的 dbx 非常相似,但 jdb 是专门用来针对 Java 应用程序的。

来源: [http://blog.csdn.net/jacky0922/article/details/6201878](http://blog.csdn.net/jacky0922/article/details/6201878)

Java深度历险(一)——Java字节代码的操纵

Posted on

Java深度历险(一)——Java字节代码的操纵

编者按】Java作为业界应用最为广泛的语言之一,深得众多软件厂商和开发者的推崇,更是被包括Oracle在内的众多JCP成员积极地推动发展。但是对于Java语言的深度理解和运用,毕竟是很少会有人涉及的话题。InfoQ中文站特地邀请IBM高级工程师成富为大家撰写这个《Java深度历险》专栏,旨在就Java的一些深度和高级特性分享他的经验。

在一般的Java应用开发过程中,开发人员使用Java的方式比较简单。打开惯用的IDE,编写Java源代码,再利用IDE提供的功能直接运行Java 程序就可以了。这种开发模式背后的过程是:开发人员编写的是Java源代码文件(.java),IDE会负责调用Java的编译器把Java源代码编译成平台无关的字节代码(byte code),以类文件的形式保存在磁盘上(.class)。Java虚拟机(JVM)会负责把Java字节代码加载并执行。Java通过这种方式来实现其“编写一次,到处运行(Write once, run anywhere)” 的目标。Java类文件中包含的字节代码可以被不同平台上的JVM所使用。Java字节代码不仅可以以文件形式存在于磁盘上,也可以通过网络方式来下载,还可以只存在于内存中。JVM中的类加载器会负责从包含字节代码的字节数组(byte[])中定义出Java类。在某些情况下,可能会需要动态的生成 Java字节代码,或是对已有的Java字节代码进行修改。这个时候就需要用到本文中将要介绍的相关技术。首先介绍一下如何动态编译Java源文件。

动态编译Java源文件

在一般情况下,开发人员都是在程序运行之前就编写完成了全部的Java源代码并且成功编译。对有些应用来说,Java源代码的内容在运行时刻才能确定。这个时候就需要动态编译源代码来生成Java字节代码,再由JVM来加载执行。典型的场景是很多算法竞赛的在线评测系统(如PKU JudgeOnline),允许用户上传Java代码,由系统在后台编译、运行并进行判定。在动态编译Java源文件时,使用的做法是直接在程序中调用Java编译器。

JSR 199引入了Java编译器API。如果使用JDK 6的话,可以通过此API来动态编译Java代码。比如下面的代码用来动态编译最简单的Hello World类。该Java类的代码是保存在一个字符串中的。 public class CompilerTest {

public static void main(String[] args) throws Exception {
String source = "public class Main { public static void main(String[] args) {System.out.println(\"Hello World!\");} }";

  JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
  StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

  StringSourceJavaObject sourceObject = new CompilerTest.StringSourceJavaObject("Main", source);
  Iterable< extends JavaFileObject> fileObjects = Arrays.asList(sourceObject);

  CompilationTask task = compiler.getTask(null, fileManager, null, null, null, fileObjects);
  boolean result = task.call();

  if (result) {
     System.out.println("编译成功。");

  }

}

static class StringSourceJavaObject extends SimpleJavaFileObject {

  private String content = null;

  public StringSourceJavaObject(String name, String content) ??throws URISyntaxException {
     super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);

     this.content = content;
  }


  public CharSequence getCharContent(boolean ignoreEncodingErrors) ??throws IOException {

     return content;
  }

} }

如果不能使用JDK 6提供的Java编译器API的话,可以使用JDK中的工具类com.sun.tools.javac.Main,不过该工具类只能编译存放在磁盘上的文件,类似于直接使用javac命令。

另外一个可用的工具是Eclipse JDT Core提供的编译器。这是Eclipse Java开发环境使用的增量式Java编译器,支持运行和调试有错误的代码。该编译器也可以单独使用。Play框架在内部使用了JDT的编译器来动态编译Java源代码。在开发模式下,Play框架会定期扫描项目中的Java源代码文件,一旦发现有修改,会自动编译 Java源代码。因此在修改代码之后,刷新页面就可以看到变化。使用这些动态编译的方式的时候,需要确保JDK中的tools.jar在应用的 CLASSPATH中。

下面介绍一个例子,是关于如何在Java里面做四则运算,比如求出来(3+4)/*7-10的值。一般的做法是分析输入的运算表达式,自己来模拟计算过程。考虑到括号的存在和运算符的优先级等问题,这样的计算过程会比较复杂,而且容易出错。另外一种做法是可以用JSR 223引入的脚本语言支持,直接把输入的表达式当做JavaScript或是JavaFX脚本来执行,得到结果。下面的代码使用的做法是动态生成Java源代码并编译,接着加载Java类来执行并获取结果。这种做法完全使用Java来实现。 private static double calculate(String expr) throws CalculationException {

String className = "CalculatorMain"; String methodName = "calculate";

String source = "public class " + className

  + " { public static double " + methodName + "() { return " + expr + "; } }";

  //省略动态编译Java源代码的相关代码,参见上一节

boolean result = task.call();

if (result) { ClassLoader loader = Calculator.class.getClassLoader();

  try {           
     Class<?> clazz = loader.loadClass(className);

     Method method = clazz.getMethod(methodName, new Class<?>[] {});
     Object value = method.invoke(null, new Object[] {});

     return (Double) value;
  } catch (Exception e) {

     throw new CalculationException("内部错误。");       
  }   

} else { throw new CalculationException("错误的表达式。");

} }

上面的代码给出了使用动态生成的Java字节代码的基本模式,即通过类加载器来加载字节代码,创建Java类的对象的实例,再通过Java反射API来调用对象中的方法。

Java字节代码增强

Java 字节代码增强指的是在Java字节代码生成之后,对其进行修改,增强其功能。这种做法相当于对应用程序的二进制文件进行修改。在很多Java框架中都可以见到这种实现方式。Java字节代码增强通常与Java源文件中的注解(annotation)一块使用。注解在Java源代码中声明了需要增强的行为及相关的元数据,由框架在运行时刻完成对字节代码的增强。Java字节代码增强应用的场景比较多,一般都集中在减少冗余代码和对开发人员屏蔽底层的实现细节上。用过JavaBeans的人可能对其中那些必须添加的getter/setter方法感到很繁琐,并且难以维护。而通过字节代码增强,开发人员只需要声明Bean中的属性即可,getter/setter方法可以通过修改字节代码来自动添加。用过JPA的人,在调试程序的时候,会发现实体类中被添加了一些额外的 域和方法。这些域和方法是在运行时刻由JPA的实现动态添加的。字节代码增强在面向方面编程(AOP)的一些实现中也有使用。

在讨论如何进行字节代码增强之前,首先介绍一下表示一个Java类或接口的字节代码的组织形式。 类文件 {

0xCAFEBABE,小版本号,大版本号,常量池大小,常量池数组, 访问控制标记,当前类信息,父类信息,实现的接口个数,实现的接口信息数组,域个数,

域信息数组,方法个数,方法信息数组,属性个数,属性信息数组 }

如上所示,一个类或接口的字节代码使用的是一种松散的组织结构,其中所包含的内容依次排列。对于可能包含多个条目的内容,如所实现的接口、域、方法和属性等,是以数组来表示的。而在数组之前的是该数组中条目的个数。不同的内容类型,有其不同的内部结构。对于开发人员来说,直接操纵包含字节代码的字节数组的话,开发效率比较低,而且容易出错。已经有不少的开源库可以对字节代码进行修改或是从头开始创建新的Java类的字节代码内容。这些类库包括ASMcglibserpBCEL等。使用这些类库可以在一定程度上降低增强字节代码的复杂度。比如考虑下面一个简单的需求,在一个Java类的所有方法执行之前输出相应的日志。熟悉AOP的人都知道,可以用一个前增强(before advice)来解决这个问题。如果使用ASM的话,相关的代码如下:

ClassReader cr = new ClassReader(is);

ClassNode cn = new ClassNode(); cr.accept(cn, 0);

for (Object object : cn.methods) {
MethodNode mn = (MethodNode) object;

if ("".equals(mn.name) || "".equals(mn.name)) {
continue;

}
InsnList insns = mn.instructions;

InsnList il = new InsnList();
il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));

il.add(new LdcInsnNode("Enter method -> " + mn.name));
il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));

insns.insert(il); mn.maxStack += 3; }

ClassWriter cw = new ClassWriter(0); cn.accept(cw);

byte[] b = cw.toByteArray();

ClassWriter就可以获取到包含增强之后的字节代码的字节数组,可以把字节代码写回磁盘或是由类加载器直接使用。上述示例中,增强部分的逻辑比较简单,只是遍历Java类中的所有方法并添加对System.out.println方法的调用。在字节代码中,Java方法体是由一系列的指令组成的。而要做的是生成调用System.out.println方法的指令,并把这些指令插入到指令集合的最前面。ASM对这些指令做了抽象,不过熟悉全部的指令比较困难。ASM提供了一个工具类ASMifierClassVisitor,可以打印出Java类的字节代码的结构信息。当需要增强某个类的时候,可以先在源代码上做出修改,再通过此工具类来比较修改前后的字节代码的差异,从而确定该如何编写增强的代码。

对类文件进行增强的时机是需要在Java源代码编译之后,在JVM执行之前。比较常见的做法有:

  • 由IDE在完成编译操作之后执行。如Google App Engine的Eclipse插件会在编译之后运行DataNucleus来对实体类进行增强。
  • 在构建过程中完成,比如通过Ant或Maven来执行相关的操作。
  • 实现自己的Java类加载器。当获取到Java类的字节代码之后,先进行增强处理,再从修改过的字节代码中定义出Java类。
  • 通过JDK 5引入的java.lang.instrument包来完成。

java.lang.instrument

由于存在着大量对Java字节代码进行修改的需求,JDK 5引入了java.lang.instrument包并在JDK 6中得到了进一步的增强。基本的思路是在JVM启动的时候添加一些代理(agent)。每个代理是一个jar包,其清单(manifest)文件中会指定一个代理类。这个类会包含一个premain方法。JVM在启动的时候会首先执行代理类的premain方法,再执行Java程序本身的main方法。在 premain方法中就可以对程序本身的字节代码进行修改。JDK 6中还允许在JVM启动之后动态添加代理。java.lang.instrument包支持两种修改的场景,一种是重定义一个Java类,即完全替换一个 Java类的字节代码;另外一种是转换已有的Java类,相当于前面提到的类字节代码增强。还是以前面提到的输出方法执行日志的场景为例,首先需要实现java.lang.instrument.ClassFileTransformer接口来完成对已有Java类的转换。 static class MethodEntryTransformer implements ClassFileTransformer {

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ?ProtectionDomain protectionDomain, byte[] classfileBuffer)

 throws  IllegalClassFormatException {
    try {

       ClassReader cr = new ClassReader(classfileBuffer);
       ClassNode cn = new ClassNode();           

       //省略使用ASM进行字节代码转换的代码           
       ClassWriter cw = new ClassWriter(0);

       cn.accept(cw);
       return cw.toByteArray();      

    } catch (Exception e){           
       return null;

    }

}

}

有了这个转换类之后,就可以在代理的premain方法中使用它。

public static void premain(String args, Instrumentation inst) {

inst.addTransformer(new MethodEntryTransformer()); }

把该代理类打成一个jar包,并在jar包的清单文件中通过Premain-Class声明代理类的名称。运行Java程序的时候,添加JVM启动参数-javaagent:myagent.jar。这样的话,JVM会在加载Java类的字节代码之前,完成相关的转换操作。

总结

操纵Java字节代码是一件很有趣的事情。通过它,可以很容易的对二进制分发的Java程序进行修改,非常适合于性能分析、调试跟踪和日志记录等任务。另外一个非常重要的作用是把开发人员从繁琐的Java语法中解放出来。开发人员应该只需要负责编写与业务逻辑相关的重要代码。对于那些只是因为语法要求而添加的,或是模式固定的代码,完全可以将其字节代码动态生成出来。字节代码增强和源代码生成是不同的概念。源代码生成之后,就已经成为了程序的一部分,开发人员需要去维护它:要么手工修改生成出来的源代码,要么重新生成。而字节代码的增强过程,对于开发人员是完全透明的。妥善使用Java字节代码的操纵技术,可以更好的解决某一类开发问题。

参考资料

感谢张凯峰对本文的审校。

来源: [http://www.infoq.com/cn/articles/cf-java-byte-code](http://www.infoq.com/cn/articles/cf-java-byte-code)

Java对存储过程的调用方法

Posted on

Java对存储过程的调用方法

一:Java如何实现对存储过程的调用: A:不带输出参数的 ---------------不带输出参数的---------------------------------- create procedure getsum @n int =0<--此处为参数--> as declare @sum int<--定义变量--> declare @i int set @sum=0 set @i=0 while @i<=@n begin set @sum=@sum+@i set @i=@i+1 end print 'the sum is '+ltrim(rtrim(str(@sum)))

--------------在SQL中执行:-------------------- exec getsum 100

------------在JAVA中调用:--------------------- JAVA可以调用 但是在JAVA程序却不能去显示该存储过程的结果 因为上面的存储 过程的参数类型int 传递方式是in(按值)方式 import java.sql./*; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa","");

     //创建存储过程的对象
     CallableStatement c=conn.prepareCall("{call getsum(?)}");

     //给存储过程的参数设置值
     c.setInt(1,100);   //将第一个参数的值设置成100

     //执行存储过程
     c.execute();
     conn.close();

} }

B:带输出参数的 1:返回int -------------------------带输出参数的---------------- alter procedure getsum @n int =0, @result int output as declare @sum int declare @i int set @sum=0 set @i=0 while @i<=@n begin set @sum=@sum+@i set @i=@i+1 end set @result=@sum -------------------在查询分析器中执行------------ declare @myResult int exec getsum 100,@myResult output print @myResult

------------在JAVA中调用--------------------- import java.sql./*; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa","");

     //创建存储过程的对象
     CallableStatement c=conn.prepareCall("{call getsum(?,?)}");

     //给存储过程的第一个参数设置值
     c.setInt(1,100);

     //注册存储过程的第二个参数
     c.registerOutParameter(2,java.sql.Types.INTEGER);

     //执行存储过程
     c.execute();

     //得到存储过程的输出参数值
     System.out.println (c.getInt(2));
     conn.close();

} } 2:返回varchar ----------------存储过程带游标---------------- ---在存储过程中带游标 使用游标不停的遍历orderid create procedure CursorIntoProcedure @pname varchar(8000) output as --定义游标 declare cur cursor for select orderid from orders --定义一个变量来接收游标的值 declare @v varchar(5) --打开游标 open cur set @pname=''--给@pname初值 --提取游标的值 fetch next from cur into @v while @@fetch_status=0 begin

set @pname=@pname+';'+@v fetch next from cur into @v end print @pname --关闭游标 close cur --销毁游标 deallocate cur

------------执行存储过程-------------- exec CursorIntoProcedure ''

--------------JAVA调用------------------ import java.sql./*; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa",""); CallableStatement c=conn.prepareCall("{call CursorIntoProcedure(?)}");

c.registerOutParameter(1,java.sql.Types.VARCHAR);

c.execute();

System.out.println (c.getString(1)); conn.close(); } } C:删除数据的存储过程 ------------------存储过程--------------------------

drop table 学生基本信息表 create table 学生基本信息表 ( StuID int primary key, StuName varchar(10), StuAddress varchar(20) ) insert into 学生基本信息表 values(1,'三毛','wuhan') insert into 学生基本信息表 values(2,'三毛','wuhan') create table 学生成绩表 ( StuID int, Chinese int, PyhSics int foreign key(StuID) references 学生基本信息表(StuID) on delete cascade on update cascade ) insert into 学生成绩表 values(1,99,100) insert into 学生成绩表 values(2,99,100)

--创建存储过程 create procedure delePro @StuID int as delete from 学生基本信息表 where StuID=@StuID --创建完毕 exec delePro 1 --执行存储过程 --创建存储过程 create procedure selePro as select / from 学生基本信息表 --创建完毕 exec selePro --执行存储过程 ------------------在JAVA中调用---------------- import java.sql./; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa","");

     //创建存储过程的对象
     CallableStatement c=conn.prepareCall("{call delePro(?)}");

     c.setInt(1,1);

     c.execute();

     c=conn.prepareCall("{call selePro}");
     ResultSet rs=c.executeQuery();

     while(rs.next())
     {
     String Stu=rs.getString("StuID");
     String name=rs.getString("StuName");
     String add=rs.getString("StuAddress");

     System.out.println ("学号:"+"     "+"姓名:"+"     "+"地址");
     System.out.println (Stu+"     "+name+"   "+add);
     }
     c.close();

} } D:修改数据的存储过程 ---------------------创建存储过程--------------------- create procedure ModPro @StuID int, @StuName varchar(10) as update 学生基本信息表 set StuName=@StuName where StuID=@StuID

-------------执行存储过程------------------------- exec ModPro 2,'四毛' ---------------JAVA调用存储过程-------------------- import java.sql./*; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa","");

     //创建存储过程的对象
     CallableStatement c=conn.prepareCall("{call ModPro(?,?)}");

     c.setInt(1,2);
     c.setString(2,"美女");

     c.execute();

     c=conn.prepareCall("{call selePro}");
     ResultSet rs=c.executeQuery();

     while(rs.next())
     {
     String Stu=rs.getString("StuID");
     String name=rs.getString("StuName");
     String add=rs.getString("StuAddress");

     System.out.println ("学号:"+"     "+"姓名:"+"     "+"地址");
     System.out.println (Stu+"     "+name+"   "+add);
     }
     c.close();

} } E:查询数据的存储过程(模糊查询) -----------------存储过程--------------------- create procedure FindCusts @cust varchar(10) as select customerid from orders where customerid like '%'+@cust+'%' ---------------执行--------------------------- execute FindCusts 'alfki' -------------在JAVA中调用-------------------------- import java.sql./*; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa","");

     //创建存储过程的对象
     CallableStatement c=conn.prepareCall("{call FindCusts(?)}");
     c.setString(1,"Tom");

     ResultSet rs=c.executeQuery();

     while(rs.next())
     {
     String cust=rs.getString("customerid");        
     System.out.println (cust);
     }
     c.close();

} } F:增加数据的存储过程

------------存储过程-------------------- create procedure InsertPro @StuID int, @StuName varchar(10), @StuAddress varchar(20) as insert into 学生基本信息表 values(@StuID,@StuName,@StuAddress)

-----------调用存储过程--------------- exec InsertPro 5,'555','555' -----------在JAVA中执行------------- import java.sql./*; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa","");

     //创建存储过程的对象
     CallableStatement c=conn.prepareCall("{call InsertPro(?,?,?)}");
     c.setInt(1,6);
     c.setString(2,"Liu");
     c.setString(3,"wuhan");

     c.execute();

     c=conn.prepareCall("{call selePro}");
     ResultSet rs=c.executeQuery();

     while(rs.next())
     {
     String stuid=rs.getString("StuID");        
     String name=rs.getString("StuName");        
     String address=rs.getString("StuAddress");        
     System.out.println (stuid+"   "+name+"   "+address);
     }
     c.close();

} }

G:在JAVA中创建存储过程 并且在JAVA中直接调用 import java.sql./*; public class ProcedureTest { public static void main(String args[]) throws Exception { //加载驱动 DriverManager.registerDriver(new sun.jdbc.odbc.JdbcOdbcDriver()); //获得连接 Connection conn=DriverManager.getConnection("jdbc:odbc:mydata","sa","");

Statement stmt=conn.createStatement(); //在JAVA中创建存储过程 stmt.executeUpdate("create procedure OOP as select /* from 学生成绩表");

CallableStatement c=conn.prepareCall("{call OOP}");

ResultSet rs=c.executeQuery(); while(rs.next()) { String chinese=rs.getString("Chinese");

System.out.println (chinese); } conn.close();

} } 来源: [http://technicalsearch.iteye.com/blog/1433293](http://technicalsearch.iteye.com/blog/1433293)