在C语言中,对字符串的处理最通常的做法是使用char数组,但这种方式的弊端是数组本身无法封装字符串操作所需的基本方法;
在Java语言中,String对象可以认为是char数组的延伸和进一步封装。基本实现主要有3部分组成:char数组,offset偏移量,count长度;
(1)char数组
:表示String的内容,它是String对象所表示字符串的超集;
(2)offset偏移量和count长度
:String的真实内容需要有这两个值在char数组中进行定位和截取;
String对象3个基本特点:
(1)不变性
:是指String对象一旦生成,则不能再对它进行改变。String的这个特性可以泛化成不变模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式
的主要作用在于当一个对象需要被多线程共享,并且频繁访问时,可以省略同步和锁等待的时间,从而大幅提高系统性能。
(2)针对常量池优化
:当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅节省内存空间。
(3)类的final定义
:作为final类的String对象在系统中不可能有任何子类,这是对系统安全的防护。同时,对于JDK 1.5之前,使用final定义,有助于帮助虚拟机寻找机会,内联所有的final方法,从而提高效率。但这种优化方法在JDK 1.5之后,效果并不明显。
subString方法源码如下
:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen);
}
// 内存泄漏是因为调用了该构造方法,这个问题JDK 1.7中已修复
String (char value[], int offset, int count) {
this.value = value;
this.offset = offset;
this.count = count;
}
以上是一个包作用域的构造方法,其目的是为了能高效且快速地共享String内的char数组对象。但在这种通过偏移量来截取字符串的方法中,String的原始内容value数组被复制到新的子字符串中。设想,如果原始字符串特别大,截取的字符串长度特别小,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的内存空间,而仅仅通过偏移量和长度来决定实际值。这种算法提高了运算速度却浪费大量空间。【String的这个构造函数使用了以空间换时间的策略,浪费了内存空间,却提高了字符串的生成速度】
JDK 1.7中已把该问题修复,源码如下:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
split()方法
:支持正则表达式,恰当地使用,可以起到事半功倍。但是,就简单的字符串分割而言,它的性能表现却不尽人意,因此,在性能敏感的系统中频繁使用这个方法是不可取的。
StringTokenizer类
:是JDK中提供的专门用来处理字符串分割子串的工具类,性能明显高于split()方法;使用方式如下:
StringTokenizer st = new StringTokenizer(orgStr, ";");
for (int i = 0; i < 10000; i ++) {
while (st.hasMoreTokens()) {
st.nextToken();
}
st = new StringTokenizer(orgStr, ";");
}
更优化的字符串分割方式
:其性能远远超过split()和StringTokenizer,适合高频率调用。
String tmp = orgStr;
for (int i = 0; i < 10000; i++) {
while (true) {
String splitStr = null;
int j = tmp.indexOf(";");
if (j < 0) break;
splitStr = tmp.substring(0, j);
tmp = tmp.substring(j+1);
}
tmp = orgStr;
}
高效率的charAt()方法
:它的功能和indexOf()正好相反,但是它的效率却和indexOf()一样高。判断字符串orgStr是否以“abc”开始和结束,单纯使用charAt()方法更高效:
int len = orgStr.length();
if (orgStr.charAt(0) == 'a' && orgStr.charAt(1) == 'b' && orgStr.charAt(2) == 'c');
if (orgStr.charAt(len-1) == 'a' && orgStr.charAt(len-2) == 'b' && orgStr.charAt(len-3) == 'c');
String常量的累加
:
对于静态字符串的连接操作,Java在编译时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
String变量的累加
:
对于变量字符串的累加,Java也做了相应的编译优化,使用了StringBuilder对象来实现字符串的累加。
在代码实现中直接对String对象做的累加操作会在编译时被优化,因此其性能比理论值好很多,但是仍建议在代码实现中,显示地使用StringBuilder或者StringBuffer对象来提升性能,而不是依靠编译器对程序进行优化。
for (int i = 0; i < 10000; i++) {
str = str + i;
}
编译器优化后,进行反编译后的代码:
for (int i = 0; i < 10000; i++) {
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}
以上反编译代码,虽然String的加法运行被编译成StringBuilder的实现,但在这种情况下,编译器并没有做出足够聪明的判断,每次循环都生成了新的StringBuilder实例从而大大降低了系统性能。
在无需考虑线程安全的情况下可以使用性能相对比较好的StringBuilder,但若系统有线程安全要求,只能选择StringBuffer。
StringBuilder和StringBuffer在不指定容量参数时,默认是16个字符。两者的扩容策略是将原来容量大小翻倍,以新的容量申请内存空间,建立新的char数组,然后将原来数据中的内容复制到这个新的数组中。因此,对于大对象的扩容会涉及大量的内存复制操作。所以,如果能够预先评估StringBuilder的大小,将能够有效地节省这些操作,从而提高系统的性能。
List是重要的数据结构之一。包括3种List实现:ArrayList,Vector和LinkedList。3种List均继承自AbstractList的实现。而AbstractList直接实现了List接口,并扩展自AbstractCollection。
ArrayList和Vector使用了数组实现,可以认为封装了对内部数组的操作,几乎使用了相同的算法,它们的唯一区别可认为是对多线程的支持。ArrayList没有对任何一个方法做线程同步,因此不是线程安全的。Vector中绝大部分方法都做了线程同步,是一种线程安全的实现。
LinkedList使用了循环双向链表数据结构。LinkedList链表由一系列表项连接而成。一个表项包含3个部分:元素内容,前驱表项和后驱表项。在JDK的实现中,无论LinkedList是否为空,链表内都有一个header表项,它既表示链表的开始,也表示链表的结尾。表项header的后驱表项是链表中的第一个元素,表项header的前驱表项便是链表中最后一个元素。
增加元素到列表尾端
:
只要ArrayList的当前容量足够大,add()操作的效率非常高的。当ArrayList进行扩容时,才会数组复制,最终会调用System.arraycopy()方法,因此,add()操作的效率还是相当高的。
public boolean add(E e) {
ensureCapacity(size + 1); // 确保内部数组有足够空间,扩容时会发生数组复制
elementData[size + 1] = e;
return true;
}
LinkedList由于使用了链表的结构,因此不需要维护容量的大小。从这点上说,它比ArrayList有一定的性能优势,然而,每次的元素增加都需要新建一个Entry对象,并进行更多的赋值操作。在频繁的系统调用中,对性能会产生一定的影响。使用LinkedList对堆内存和GC的要求更高。
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size ++;
modCount ++;
return newEntry;
}
增加元素到列表任意位置
:
由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该位置后的所有元素需要重新排列,因此,其效率相对会比较低。
public void add(int index, E element) {
if (index > size || index < 0) {
throw new IndexOutOfBoundsException ("Index: " + index + ", Size: " + size);
}
ensureCapacity(size + 1);
System.arraycopy(elementData, index, elementData, index+1, size - index);
element[index] = element;
size ++;
}
每次插入操作,都会进行一次数组复制。而这个操作在增加元素到List尾端的时候是不存在的。大量的数组重组操作会导致系统性能低下。并且,插入的元素在List中的位置越靠前,数组重组的开销也越大。尽可能地将元素插入到List的尾端附近,有助于提高该方法的性能。
对于LinkedList来说,在List尾端插入数据与在任意位置插入数据是一样的。并不会因为插入的位置靠前而导致插入方法的性能降低。
public void add(int index, E element) {
addBefore(element, (index == size ? header : entry(index)));
}
删除任意位置元素
:
对ArrayList来说,remove()方法和add()方法是雷同的。在任意位置移除元素后,都要进行数组的重组。
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index+1, elementData, index, numMoved);
}
elementData[--size] = null;
return oldValue;
}
在LinkedList的实现中,首先要通过循环找到要删除的元素。如果删除的位置处于List的前半段,则从前往后找;若其位置处于后半段,则从后往前找。因此无论要删除较为靠前或者靠后的元素都是非常高效的;但要移除List中间的元素却几乎要遍历完半个List,在List拥有大量元素的情况下,效率很低。
public E remove(int index) {
return remove(entry(index));
}
private Entry<E> entry(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
Entry<E> e = header;
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
容量参数
:
默认情况下,ArrayList数组的初始值大小为10,每次扩容将新的数组大小设置为原大小的1.5倍。Vector数组的初始值大小为10,每次扩容将新的数组大小设置为原大小的2.0倍。
遍历列表
:
String tmp;
List<String> list = new ArrayList<>();
// ForEach循环
long start = System.currentTimeMillis();
for (String s : list) {
tmp = s;
}
System.out.println("foreach spend: " + (System.currentTimeMillis() - start));
// 迭代器
start = System.currentTimeMillis();
for (Iterator<String> it = list.iterator(); it.hasNext();) {
tmp = it.next();
}
System.out.println("iterator spend: " + (System.currentTimeMillis() - start));
// for循环,使用随机访问
start = System.currentTimeMillis();
int size = list.size();
for (int i = 0; i < size; i++) {
tmp = list.get(i);
}
System.out.println("for spend: " + (System.currentTimeMillis() - start));
对于数据量100万级别的ArrayList和LinkedList,使用以上三种循环方式遍历,可得知,对于最简便的ForEach循环并没有很好的性能表现,综合性能不如普通的迭代器。而使用for循环通过随机访问遍历列表时,ArrayList表现很好,但是LinkedList的表现却无法让人接受。这是因为对LinkedList进行随机访问时,总会进行一次列表的遍历操作。
编译器会将ForEach循环体作为迭代器处理,二者是完全等价的。但在ForEach循环的迭代操作中,又存在一步多余的赋值操作,从而导致ForEach循环的性能比直接使用迭代器略差一些。
对ArrayList这些基于数组的实现来说,随机访问的速度是很快的。在遍历这些List对象时,可以优先考虑随机访问。但对于LinkedList等基于链表的实现,随机访问的性能是非常差的,应避免使用。
围绕着Map接口,最主要的实现类有Hashtable,HashMap,LinkedHashMap和TreeMap。在Hashtable的子类中,还有Properties类的实现。
HashMap的实现原理
:
简单地说,HashMap就是将key做hash算法,然后将hash值映射到内存地址,直接取得key所对应的数据。在HashMap中,底层数据结构使用的是数组,所谓的内存地址即数组的下标索引。HashMap的高性能需要保证以下几点:
(1)hash算法必须是高效的;
(2)hash值到内存地址(数组索引)的算法是快速的;
(3)根据内存地址(数组索引)可以直接取得对应的值;
在HashMap中,hash算法有关代码如下:
int hash = hash(key.hashCode());
public native int hashCode();
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
Object类的hashCode()方法默认是native的实现,可以认为不存在性能问题。而hash()函数的实现全部基于位运算。
native方法通常比一般的方法快,因为它直接调用操作系统本地链接库的API。由于hashCode()方法是可以重载的,因此,为了保证HashMap的性能,需要确保相关的hashCode()是高效的。而位运算也比算术,逻辑运算快。
当取得key的hash值后,需要通过hash值得到内存地址:
int i = indexFor(hash, table.length);
static int indexFor(int h, int length) {
return h & (length - 1);
}
Hash冲突
:
HashMap的内部维护着一个Entry数组,每一个Entry表项包括key,value,next和hash几项。这里特别注意其中的next部分,它指向了另外一个Entry。可以看到当put()操作有冲突时,新的Entry依然会被安放在对应的索引下标内,并替换原有的值。同时,为了保证旧值不丢失,会将新的Entry的next指向旧值。这便实现了在一个数组索引空间内存放多个值项。HashMap实际上是一个链表的数组。
基于HashMap的这种实现机制,只要hashCode()和hash()方法实现的足够好,能够尽可能地减少冲突的产生,那么对HashMap的操作几乎等价于对数组的随机访问操作,具有很好的性能。但是,如果hashCode()或者hash()方法实现较差,在大量冲突产生的情况下,HashMap事实上就退化为几个链表,对HashMap的操作等价于遍历链表,此时性能很差。
容量参数
:
默认情况下,HashMap初始大小为16,负载因子为0.75。在HashMap内部,还维护了一个threshold变量,它始终被定义为当前数组总容量和负载因子的乘积,它表示HashMap的阀值。当HashMap的实际容量超过阀值时,HashMap便会进行扩容。因此,HashMap的实际填充率不会超过负载因子。
LinkedHashMap—有序的HashMap
:
LinkedHashMap继承自HashMap,因此,它具备了HashMap的优良特性—高性能。在HashMap的基础上,LinkedHashMap又在内部增加了一个链表,用以存放元素的顺序。因此,LinkedHashMap可以简单地理解为一个维护了元素次序表的HashMap。
LinkedHashMap可以提供两种类型的顺序:一是元素插入时的顺序;二是最近访问的顺序;
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
其中accessOrder为true时,按照元素最后访问时间排序;当accessOrder为false时,按照元素插入顺序排序,默认为false;
在内部实现中,LinkedHashMap通过继承HashMap.Entry类,实现了LinkedHashMap.Entry,为HashMap.Entry增加了before和after属性用以记录某一表项的前驱和后继,并构成循环列表。
在这里还值得一提的是LinkedHashMap还可以根据元素最后访问时间进行排序。即,每当使用get()方法访问某一元素时,该元素便被移动到链表的尾端。如下:
for (Iterator iterator = map.keySet().iterator(); iterator.hasNext();) {
String name = (String) iterator.next();
System.out.println(name + "->" + map.get(name));
}
上面这段代码,运行结果出人意料,LinkedHashMao非但没有排序,程序反而抛出了ConcurrentModificationException异常,并终止了。ConcurrentModificationException异常一般会在集合迭代过程中被修改是抛出。不仅仅是LinkedHashMap,所有的集合都不允许在迭代器模式中修改集合的结构。问题就出在get()方法上。虽然一般认为get()方法是只读的,但是当前的LinkedHashMap却工作在按照元素访问顺序排序的模式中,get()方法会修改LinkedHashMap中的链表结构,以便将最近访问的元素放置到链表的末尾。因此,这个操作便引起了这个错误。
不要在迭代器模式中修改被迭代的集合。如果这么做,就会抛出ConcurrentModificationException异常。这个特性适用于所有的集合类,包括HashMap,Vector,ArrayList等。
TreeMap—另一种Map实现
:
在功能上讲,TreeMap有着比HashMap更为强大的功能,它实现了SortedMap接口,这意味着它可以对元素进行排序。对TreeMap的迭代输出将会以元素顺序进行。
TreeMap排序方式和LinkedHashMap是不同的。LinkedHashMap是基于元素进入集合的顺序或者被访问的先后顺序排序;而TreeMap则是基于元素的固有顺序(由Comparator或者Comparable确定)
public TreeMap(Comparator<? super K> comparator)
对于TreeMap而言,排序是一个必须进行的过程。因此,要正常使用TreeMap,一定要通过其中一种方式将排序规则传递给TreeMap。如果既不指定Comparator,元素又不去实现Comparable接口,那么在put()操作时,就会抛出java.lang.ClassCastException异常。
TreeMap的内部实现是基于红黑树的。红黑树是一种平衡查找树,它的统计性能要优于平衡二叉树。它具有良好的最坏情况运行时间,可以在O(log n)时间内做查找,插入和删除,n表示树中元素的数目。
Set接口并没有在Collection接口之上增加额外的操作,Set集合中的元素是不能重复的。其中最为重要的是HashSet,LinkedHashSet和TreeSet的实现。所有这些Set的实现,都只是对应的Map的一种封装而已。
HashSet对应HashMap的封装,代码如下:
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
以此类推,LinkedHashSet对应LinkedHashMap,TreeSet对应TreeMap。
分离循环中被重复调用的代码;
省略相同操作;
减少方法调用:如果可以,则尽量直接访问内部元素,而不要调用对应的接口。函数调用是需要消耗资源的,直接访问元素会更高效。
RandomAccess接口是一个标志接口,本身并没有提供任何方法,任何实现RandomAccess接口的对象都可以认为是支持快速随机访问的对象。此接口的主要目的标示那些可支持快速随机访问的List实现。
在JDK的实现中,任何一个基于数组的List实现都实现了RandomAccess接口,而基于链表的实现则都没有。因为只有数组能够进行快速的随机访问,而对链表的随机访问需要进行链表的遍历。
通过RandomAccess可以知道List是否支持快速随机访问。同时,需要记住,如果应用程序需要通过索引下标对List做快速随机访问,尽量不要使用LinkedList,ArrayList和Vector都是不错的选择。
在软件系统中,由于IO的速度要内存速度慢,因此,IO读写在很多场合都会成为系统的瓶颈。提升IO速度,对提升系统整体性能有着很大的好处。
在Java的标准IO中,提供了基于流的IO实现,即InputStream和OutputStream。这种基于流的实现以字节为单位处理数据,并且非常容易建立各种过滤器。
NIO是New IO的简称,与旧式的基于流的IO方法相对,它表示新的一套Java IO标准。它是在Java 1.4中被纳入到JDK中的,并具有以下特性:
与流式的IO不同,NIO是基于块(Block)的,它以块为基本单位处理数据。在NIO中,最为重要的两个组件是缓冲Buffer和通道Channel。缓冲是一块连续的内存块,是NIO读写数据的中转地。通道表示缓冲数据的源头或者目的地,它用于向缓冲读取或者写入数据,是访问缓冲的接口。
在NIO的实现中,Buffer是一个抽象类。JDK为每一种Java原生类型都创建了一个Buffer,如:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer;
除了ByteBuffer外,其他每一种Buffer都具有完全一样的操作,唯一的区别仅仅在于它们所对应的数据类型。因为ByteBuffer多用于绝大多数标准IO操作的接口,因此它有些特殊的方法。
在NIO中,和Buffer配合使用的还有Channel。Channel是一个双向通道,既可读,也可写。有点类似Stream,但Stream是单向的。应用程序中不能直接对Channel进行读写操作,而必须通过Buffer来进行。
public static void nioCopyFile(String resource, String destination) throws IOException {
FileInputStream fis = new FileInputStream(resource);
FileOutputStream fos = new FileOutputStream(destination);
// 读文件通道
FileChannel readChannel = fis.getChannel();
// 写文件通道
FileChannel writeChannel = fos.getChannel();
// 读入数据缓冲
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
// 读入数据
int len = readChannel.read(buffer);
if (len == -1) {
// 读取完毕
break;
}
// 重置POSITION
buffer.flip();
// 写入文件
writeChannel.write(buffer);
}
}
Buffer中有3个重要的参数,如下:
(1)位置(position):当前缓冲区写入/读取的位置,将从此位置后写入/读取数据;
(2)容量(capacity):缓冲区的总容量上限;
(3)上限(limit):缓冲区的实际上限,它总是小于等于容量。通常情况下,和容量相等;
public class BufferTest {
public static void main(String [] args) {
// 15个字节大小的缓冲区
ByteBuffer b = ByteBuffer.allocate(15);
System.out.println("limit=" + b.limit() + ", capacity=" + b.capacity() + ", position=" + b.position());
// 存入10个字节数据
for (int i = 0; i < 10; i++) {
b.put((byte)i);
}
System.out.println("limit=" + b.limit() + ", capacity=" + b.capacity() + ", position=" + b.position());
// 重置position
b.flip();
System.out.println("limit=" + b.limit() + ", capacity=" + b.capacity() + ", position=" + b.position());
// 读取5个字节数据
for (int i = 0; i < 5; i++) {
System.out.print(b.get());
}
System.out.println();
System.out.println("limit=" + b.limit() + ", capacity=" + b.capacity() + ", position=" + b.position());
b.flip();
System.out.println("limit=" + b.limit() + ", capacity=" + b.capacity() + ", position=" + b.position());
}
}
通常,将Buffer从写模式转换为读模式时,需要执行flip()方法。flip()方法不仅重置了当前的position为0,还将limit设置到当前position的位置,这样做的目的是防止在读模式中,读到应用程序根本没有进行操作的区域。
Buffer的创建
:
// 从堆中分配
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从既有数组中创建
byte [] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
重置和清空缓冲区
:
这里所谓的重置,只是指重置了Buffer的各项标志位,并不真正清空Buffer的内容。
buffer.rewind():将position置零,并清除标志位(mark),主要作用于提取Buffer的有效数据做准备;
buffer.clear():将position置零,同时将limit设置为capacity的大小,并清除了标志位(mark),主要作用于为重新写Buffer做准备;
buffer.flip():将limit设置到position所在位置,然后将position置零,并清除标志位(mark),主要作用于在读写转换时使用; 读写缓冲区
:
对Buffer进行读写操作是Buffer最为重要的操作。以ByteBuffer为例,JDK提供了以下常用的读写操作:
public byte get(); // 返回当前position上的数据,并将position位置向后移一位
public ByteBuffer get(byte [] dst); // 读取当前Buffer的数据到dst中,并恰当地移动position位置
public byte get(int index); // 读取给定index索引上的数据,不改变position的位置
public ByteBuffer put(byte b); // 当前位置写入给定的数据,position位置向后移一位
public ByteBuffer put(int index, byte b); // 将数据b写入到Buffer的index位置,不改变position的位置
public final ByteBuffer put(byte [] src); // 将给定的数据写入当前Buffer,并恰当地移动position位置
标志缓冲区
:
标志(mark)缓冲区是一项在数据处理时很有用的功能。buffer.mark()用于记录当前位置;buffer.reset()用于恢复到mark所在的位置。
public static void testMark() {
// 15个字节大小的缓冲区
ByteBuffer b = ByteBuffer.allocate(15);
// 存入10个字节数据
for (int i = 0; i < 10; i++) {
b.put((byte)i);
}
b.flip();
for (int i = 0; i < b.limit(); i++) {
System.out.print(b.get());
if (i == 4) {
// 在第4个位置做mark
b.mark();
System.out.print("{mark at " + i + "}");
}
}
// 回到mark的位置,并处理后续数据
b.reset();
System.out.println("\nreset to mark");
while (b.hasRemaining()) {
System.out.print(b.get());
}
}
复制缓冲区
:
复制缓冲区是指以原缓冲区为基础,生成一个完全一样的新缓冲区。方法如下:
public ByteBuffer duplicate();
这个函数对处理复杂的Buffer数据很有好处。因为新生成的缓冲区和原缓冲共享相同的内存数据,所以对任意一方的数据改动都是相互可见的,但两者又独立维护了各自的position,limit和mark。大大增加了程序的灵活性,为多方同时处理数据提供了可能。
public static void testDuplicate() {
// 15个字节大小的缓冲区
ByteBuffer b = ByteBuffer.allocate(15);
// 存入10个字节数据
for (int i = 0; i < 10; i++) {
b.put((byte)i);
}
// 复制当前缓冲区
ByteBuffer c = b.duplicate();
System.out.println("After b.duplicate()");
System.out.println(b);
System.out.println(c);
// 重置缓冲区c
c.flip();
System.out.println("After c.flip()");
System.out.println(b);
System.out.println(c);
// 向缓冲区c存入数据
c.put((byte)100);
System.out.println("After c.put((byte)100)");
System.out.println("b.get(0)=" + b.get(0));
System.out.println("c.get(0)=" + c.get(0));
}
缓冲区分片
:
缓冲区分片使用slice()方法实现,它将在现有的缓冲区中,创建新的子缓冲区,子缓冲区和父缓冲区共享数据。这个方法有助于将系统模块化。当需要处理一个Buffer的一个片段时,可以使用slice()方法取得一个子缓冲区,然后像处理普通的缓冲区一样处理这个片段,而无需考虑缓冲区的边界问题。
缓冲区分片可以将一个大缓冲区进行分割处理,得到的子缓冲区都具有完整的缓冲区模型结构。因此,这个操作有利于系统的模块化。
public static void testSlice() {
// 15个字节大小的缓冲区
ByteBuffer b = ByteBuffer.allocate(15);
// 存入10个字节数据
for (int i = 0; i < 10; i++) {
b.put((byte)i);
}
b.position(2);
b.limit(6);
// 生成子缓冲区
ByteBuffer subBuffer = b.slice();
for (int i = 0; i < subBuffer.capacity(); i++) {
byte bb = subBuffer.get(i);
bb *= 10;
subBuffer.put(i, bb);
}
// 重置父缓冲区,并查看父缓冲区的数据
b.position(0);
b.limit(b.capacity());
while (b.hasRemaining()) {
System.out.print(b.get());
}
}
只读缓冲区
:
使用缓冲区对象的asReadOnlyBuffer()方法得到一个与当前缓冲区一致的,并且共享内存数据的只读缓冲区。只读缓冲区对于数据安全非常有用。并且,对原始缓冲区的修改,只读缓冲区也是可见的。
public static void testReadOnlyBuffer() {
// 15个字节大小的缓冲区
ByteBuffer b = ByteBuffer.allocate(15);
// 存入10个字节数据
for (int i = 0; i < 10; i++) {
b.put((byte)i);
}
// 创建只读缓冲区
ByteBuffer readOnly = b.asReadOnlyBuffer();
readOnly.flip();
while (readOnly.hasRemaining()) {
System.out.println(readOnly.get());
}
System.out.println();
// 修改原始缓冲区数据
b.put(2, (byte)20);
// readOnly.put(2, (byte)20); 会抛出ReadOnlyBufferException
readOnly.flip();
// 新的改动,在只读缓冲区内可见
while (readOnly.hasRemaining()) {
System.out.println(readOnly.get());
}
}
文件映射到内存
:
NIO提供了一种将文件映射到内存的方法进行IO操作,它可以比普通的基于流的IO快很多。
public static void testMappedBuffer() throws IOException {
RandomAccessFile raf = new RandomAccessFile("/home/taomk/abc.txt", "rw");
FileChannel fc = raf.getChannel();
// 将文件映射到内存
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());
while (mbb.hasRemaining()) {
System.out.print((char)mbb.get());
}
mbb.put(0, (byte) 98);
raf.close();
}
使用文件映射的方式,将文本文件通过FileChannel映射到内存中。然后从内存中读取文件的内容。在程序的最后,通过修改Buffer,将实际数据写到对应的磁盘文件中。 处理结构化数据
:
NIO还提供了处理结构化数据的方法,称之为散射(Scattering)和聚集(Gathering)。散射是指将数据读入到一组Buffer中,而不仅仅是一个。聚集与之相反,指将数据写入一组Buffer中。散射和聚集的基本使用方法和对应单个Buffer操作时的使用方法相当类似。在JDK中,通过ScatteringByteChannel和GatheringByteChannel接口提供相关操作。
ScatteringByteChannel主要方法:
public long read(ByteBuffer [] dsts) throws IOException;
public long read(ByteBuffer [] dsts, int offset, int length) throws IOException;
GatheringByteChannel主要方法:
public long write(ByteBuffer [] srcs) throws IOException;
public long write(ByteBuffer [] srcs, int offset, int length) throws IOException;
在散射读取中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。在JDK提供的各种通道中,DatagramChannel,FileChannel和SocketChannel都实现了这两个接口。
/**
* 缓冲区:聚集
*/
public static void testGathering() throws Exception {
ByteBuffer bookBuf = ByteBuffer.wrap("java性能优化技巧".getBytes("UTF-8"));
ByteBuffer autBuf = ByteBuffer.wrap("葛一鸣".getBytes("UTF-8"));
ByteBuffer [] bufs = new ByteBuffer[]{bookBuf, autBuf};
File file = new File("");
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
FileChannel fc = fos.getChannel();
fc.write(bufs);
fos.close();
}
/**
* 缓冲区:散射
*/
public static void testScattering() throws Exception {
ByteBuffer bookBuf = ByteBuffer.allocate(16);
ByteBuffer autBuf = ByteBuffer.allocate(6);
ByteBuffer [] bufs = new ByteBuffer[]{bookBuf, autBuf};
File file = new File("");
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
fc.read(bufs);
String bookName = new String(bufs[0].array(), "UTF-8");
String authName = new String(bufs[1].array(), "UTF-8");
System.out.println(bookName + ", " + authName);
}
在基于Buffer的实现中,额外又增加了这些数据转换开销,其性能也好于基于流的实现。在NIO还额外提供了一种将文件直接映射到内存的方法:MappedByteBuffer,其性能远远高于前两者。这其中,由于ByteBuffer是将文件一次性读入内存再做后续处理,而Stream方式则是边读文件边处理数据(虽然也使用了缓冲组件BufferedInputStream),这也是导致性能差距的原因之一。
NIO的Buffer还提供了一个可以直接访问系统物理内存的类—DirectBuffer。DirectBuffer继承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍然在JVM堆上分配空间,其最大内存,收到最大堆的限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。
在对普通的ByteBuffer访问时,系统总是会使用一个“内核缓冲区”进行间接的操作。而DirectBuffer所出的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuffer是一种更接近系统底层的方法,所以,它的速度比普通的ByteBuffer更快。
/**
* 缓冲区:直接内存
*/
public static void testDirectBuffer() {
// 分配DirectBuffer
ByteBuffer b = ByteBuffer.allocateDirect(500);
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < 99; j++) {
b.putInt(j);
}
b.flip();
for (int j = 0; j < 99; j++) {
b.getInt();
}
b.clear();
}
}
由于DirectBuffer占用的内存空间并不在堆中,因此对堆空间的操作就相对较少(注意,DirectBuffer对象本身还是在堆上分配的)。在需要频繁创建Buffer的场合,由于创建和销毁DirectBuffer的代价要远远大于在堆上分配ByteBuffer空间,是不宜使用DirectBuffer的,但是如果能进DirectBuffer进行复用,那么,在读写频繁的情况下,它完全可以大幅改善系统性能。
一段可用于DirectBuffer监控的代码
:
/**
* 缓冲区:直接内存监控
*/
public static void monitorDirectBuffer() throws Exception {
Class c = Class.forName("java.nio.Bits");
// 通过反射取得私有属性
Field maxMemory = c.getDeclaredField("maxMemory");
maxMemory.setAccessible(true);
Field reservedMemory = c.getDeclaredField("reservedMemory");
reservedMemory.setAccessible(true);
synchronized (c) {
// 总大小
Long maxMemoryVal = (Long) maxMemory.get(null);
// 剩余大小
Long reservedMemoryVal = (Long) reservedMemory.get(null);
System.out.println("maxMemoryVal: " + maxMemoryVal);
System.out.println("reservedMemoryVal: " + reservedMemoryVal);
}
}
使用参数-XX:MaxDirectMemorySize=10M
可以指定DirectBuffer的最大可用空间,DirectBuffer的空间不在堆上分配,但DirectBuffer对象是在堆上分配的。因此可以使应用程序突破最大堆的内存限制。对DirectBuffer的读写操作比普通Buffer快,但是对它的创建和销毁却比普通Buffer慢。
StringBuffer str = new StringBuffer("Hello World");
(1)强引用可以直接访问对象;
(2)强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不回收强引用所指向的对象;
(3)强引用可能导致内存泄漏;
软引用是除了强引用外,最强的引用类型。可以通过java.lang.ref.SoftReference使用软引用。一个软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阀值时,才会去回收软引用的对象。只要有足够的内存,软引用便可能在内存中存活相当长一段时间。因此,软引用可以用于实现对内存敏感的Cache。
public class MyObject {
@Override
protected void finalize() throws Throwable {
super.finalize();
// 被回收时输出
System.out.println("MyObject finalize called");
}
@Override
public String toString() {
return "I am MyObject";
}
}
public class CheckRefQueue extends Thread {
private ReferenceQueue<MyObject> referenceQueue;
public CheckRefQueue(ReferenceQueue<MyObject> referenceQueue) {
this.referenceQueue = referenceQueue;
}
@Override
public void run() {
try {
Reference<MyObject> obj = (Reference<MyObject>) referenceQueue.remove();
if (obj != null) {
System.out.println("Object for SoftReference is " + obj.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SoftReferenceTest {
public static void main(String [] args) {
MyObject obj = new MyObject();
ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();
SoftReference<MyObject> softObjRef = new SoftReference<>(obj, softQueue);
new CheckRefQueue(softQueue).start();
obj = null;
System.gc();
System.out.println("After GC: Soft Get= " + softObjRef.get());
System.out.println("分配大块内存");
byte [] b = new byte[1450 * 1024 * 925];
System.out.println("After new byte[]: Soft Get= " + softObjRef.get());
}
}
在系统内存紧张的情况下,软引用被回收。此例中软引用被回收时,会被加入注册的引用队列。
弱引用是一种比软引用还弱的引用类型。在系统GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
public class WeakReferenceTest {
public static void main(String [] args) {
MyObject obj = new MyObject();
ReferenceQueue<MyObject> weakQueue = new ReferenceQueue<>();
WeakReference<MyObject> weakObjRef = new WeakReference<>(obj, weakQueue);
new CheckRefQueue(weakQueue).start();
obj = null;
System.out.println("Before GC: Weak Get= " + weakObjRef.get());
System.gc();
System.out.println("After new byte[]: Soft Get= " + weakObjRef.get());
}
}
软引用和弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
虚引用是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的。随时都可能被垃圾回收器回收。当试图使用虚引用的get()方法取得强引用时,总是会失败。并且虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,销毁这个对象时,将这个虚引用加入引用队列。
public class PhantomReferenceTest {
public static void main(String [] args) throws InterruptedException {
MyObject obj = new MyObject();
ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();
PhantomReference<MyObject> phantomObjRef = new PhantomReference<>(obj, phantomQueue);
System.out.println("Phantom Get: " + phantomObjRef.get());
new CheckRefQueue(phantomQueue).start();
obj = null;
Thread.sleep(1000);
int i = 1;
while (true) {
System.out.println("第" + i++ + "次GC");
System.gc();
Thread.sleep(1000);
}
}
}
在第一次GC时,系统找到了垃圾对象,并调用其finalize()方法回收内存,但没有立即加入回收队列。第二次GC时,该对象真正被GC清除,此时,加入虚引用队列。
虚引用最大的作用在于跟踪对象回收,清理被销毁对象的相关资源。通常,当对象不被使用时,重载该类的finalize()方法可以回收对象的资源。但是,如果finalize()方法使用不慎,可能导致该对象复活。由于finalize()只会被调用一次,因此,再进行下一次GC时,对象就没有机会再度复活了。所以,在第一次GC时对象复活后,再进行下一次GC前,必须要手动将使用obj=null,去除该对象的强引用,并且不会再次执行finalize()方法了。
使用虚引用来清理对象所占用的资源,是对finalize()方法的一种可行替代方案。
WeakHashMap是弱引用的一种典型应用,可以作为简单的缓存表解决方案。因为WeakHashMap会在系统内存范围内,保存所有表项,而一旦内存不足,在GC时,没有被引用的表项又会很快被清除掉,从而避免系统内存溢出。
WeakHashMap使用弱引用,可以自动释放已经被回收的key所在的表项,但如果WeakHashMap的key都在系统内持有强引用,那么WeakHashMap就退化为普通的HashMap,因此所有的表项都无法被自动清理。
try-cache语句对系统性能而言是非常糟糕的,虽然在一次try-cache中,无法察觉到它对性能带来的影响,但是,一旦try-cache语句被应用于循环中,就会给系统性能带来极大伤害。切记循环中使用。
int a = 0;
for (int i = 0; i < 10000000; i++) {
try {
a ++;
} cache (Exception e) {
}
}
应改为:
int a = 0;
try {
for (int i = 0; i < 10000000; i++) {
a ++;
}
} cache (Exception e) {
}
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量,实例变量等,都在堆(Heap)中创建,速度较慢。
在所有运算中,位运算是最为高效的。
一维数组访问速度要优于二维数组。
尽可能让程序少做重复的计算,尤其要关注在循环体内的代码,从循环体内提取重复的代码可以有效地提升性能。
展开循环是一种在极端情况下使用的优化手段,因为展开循环很可能会影响代码的可读性和维护性。
虽然位运算的速度远远高于算术运算,但是在条件判断时,使用位运算替代布尔运算却是非常错误的选择。布尔运算可进行短路运算。
System.arrayCopy()函数是native函数,通常native函数的性能要优于普通的函数。仅处于性能考虑,在软件开发时,应尽可能调用native函数。
无论对于读取还是写入文件,适当地使用缓冲,可以提升系统的文件读写性能。
对于重量级对象,由于对象在构造函数中可能会进行一些复杂且耗时的操作,因此,构造函数的执行时间可能会比较长。
Object.clone()方法可以绕过对象构造函数,快速复制一个对象实例。由于不需要调用对象构造函数,因此,clone()方法不会受到构造函数性能的影响,能够快速生成一个实例。但是默认情况下,clone()方法生成的实例只是原对象的浅拷贝。如果需要深拷贝,则需要重新实现clone()方法。
在Java中,由于实例方法需要维护一张类似虚函数表的结构,以实现对多态的支持。与静态方法相比,实例方法的调用需要更多的资源。对于一些常用的工具类方法,没有对其进行重载的必要,那么将它们声明为static,便可以加速方法的调用。