为JAVA性能而设计的示例分析
为JAVA性能而设计的示例分析
这篇文章主要介绍为JAVA性能而设计的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!
第一部分: 接口事宜
概要
许多通常的 Java 性能问题都起源于在设计过程早期中的类设计的思想, 早在许多开发者开始考虑性能问题之前. 在这个系列中, Brian Goetz讨论了通常的 Java性能上的冒险以及怎么在设计时候避免它们.
许多程序员在开发周期的后期才可是考虑性能管理. 他们常常把性能优化拖延到最后, 希望能完全避免 -- 有时候这种策略是成功的. 但是早期的设计思想可以影响性能优化的需求及其成功. 如果性能是你的程序的一个重要指标, 那么性能管理应该从第一天起就和设开发周期整合在一起.
这个系列探索一些早期的设计思想能够极大影响应用程序性能的方法. 在这篇文章中, 我专注于最通常的性能问题中的一个: 临时变量的创建. 一个类的对象创建方式常常在设计时候就确定了的 -- 但不是故意的 --, 就为后来的性能问题种下了种子.
性能问题有各种形式. 最容易调整的是那些你简单地为计算选择了一个错误的算法 -- 就象使用使用冒泡算法来对一个大数据集进行排序, 或者在使用一个经常使用的数据项时不是做缓冲, 而是每次都计算. 你可以使用概要分析来简单地找出这些瓶颈, 一旦找到了,你可以很容易地改正. 但是, 许多 Java 性能问题来自一个更深的, 更难改正的源头 -- 一个程序组件的接口设计.
今天大多数程序是由内部开发的或者外部买来的组件构建而成. 甚至在程序不是很大地依于已经存在的组件时, 面向对象的设计过程也鼓励应用程序包装成组件, 这样就简化了设计, 开发和测试过程. 这些优势是不可否认的, 你应该认识到这些组件实现的接口可能极大地影响使用它们的程序的行为和性能.
在这一点上, 你可能要问什么样的接口和性能相关. 一个类的接口不仅定义了这个类可以实现那些功能, 也可以定义它的对象创建行为和使用它的方法调用序列. 一个类怎样定义它的构造函数和方法决定了一个对象是否可以重用, 它的方法是否要创建 -- 或者要求它的客户端创建 -- 中间对象, 以及一个客户端需要调用多少方法来使用这个类.这些因素都会影响程序的性能.
注意对象的创建
一个最基本的 Java 性能管理原则就是: 避免大量的对象创建. 这不是说你应该不创建任何对象而放弃面向对象的好处. 但是你必须在执行性能相关的代码时, 在紧循环中注意对象的创建. 对象的创建是如此地高代价, 以至于你应该在要求性能的情况下避免不必要的临时或者中间对象的创建.
String 类是在那些处理文本的程序中对象创建的主要来源. 因为 String 是不可修改的,每当一个 String 修改或创建, 就必须创建一个新的对象. 结果就是, 关注性能的程序应该避免大量 String 的使用. 但是, 这通常是不可能的. 甚至当你从你的代码中完全除去对 String 的依赖, 你常常会发现你自己在使用一些具有根据 String 定义的接口的组件.所以, 你最后不得不使用 String.
例子: 正规表达式匹配
作为一个例子, 假设你写一个叫做 MailBot 的邮件服务器. MailBot 需要处理 MIME 头格式 -- 象发送日期或者发送者的 email 地址 -- 在每个信息的顶部. 使用一个匹配正规表达式的组件来使处理 MIME 头的过程简单一些. MailBot 足够聪明, 不为每个头的行或者头的元素创建一个 String 对象. 相反, 它用输入的文本填充了一个字符缓冲区, 通过对缓冲区的索引来确定要处理的头的位置. MailBot 会调用正规表达式匹配器来处理每个头行, 所以匹配器的性能就非常重要. 我们以一个正规表达式匹配器类的拙劣的接口作为例子:
public class AwfulRegExpMatcher {
/** Create a matcher with the given regular expression and which will
operate on the given input string */
public AwfulRegExpMatcher(String regExp, String inputText);
/** Retrieve the next match of the pattern against the input text,
returning the matched text if possible or null if not */
public String getNextMatch();
}
甚至在这个类实现了一个有效的正规表达式匹配的算法的时候, 任何大量使用它的程序仍然难以忍受. 既然匹配器对象和输入的文本联系起来, 每一次你调用它, 你必须创建一个新的匹配器对象. 既然你的目标是减少不必要的对象的创建, 那么使这个匹配器可以赜将会是一个明显的开始.
下面的类定义演示了你的匹配器的另一个可能的接口, 允许你重用这个匹配器, 但仍然很坏.
public class BadRegExpMatcher {
public BadRegExpMatcher(String regExp);
/** Attempts to match the specified regular expression against the input
text, returning the matched text if possible or null if not */
public String match(String inputText);
/** Get the next match against the input text, or return null if no match */
public String getNextMatch();
}
忽略正规表达式匹配中的精细点 -- 象返回匹配的子表达式, 这个看起来无害的类定义会出什么问题呢? 从功能上来看, 没有. 但是从性能的角度来看, 许多. 首先, 匹配器需要它的调用者创建一个 String 来代表要匹配的文本. MailBot 试图避免创建 String对象, 但是当它要找到一个要做正规表达式解析的头时, 它不得不创建一个 String 来满足 BadRegExpMatcher:
BadRegExpMatcher dateMatcher = new BadRegExpMatcher(...);
while (...) {
...
String headerLine = new String(myBuffer, thisHeaderStart,
thisHeaderEnd-thisHeaderStart);
String result = dateMatcher.match(headerLine);
if (result == null) { ... }
}
第二, 匹配器创建了结果字符串甚至当 MailBot 只关心是否匹配了, 不需要匹配的文本时,这意味着要简单使用 BadRegExpMatcher 来确认一个日期头是否匹配一个特定的格式, 你必须创建两个 String 对象 -- 匹配器的输入和匹配的结果. 两个对象可能看起来不多,但是如果你给 MailBot 处理的每个邮件的每个头行都创建两个对象, 这会极大地影响性能. 错误不在于 MailBot 的设计, 而在于 BadRegExpMatcher 类的设计 -- 或者使用.
注意返回一个轻量型的 Match 对象 -- 可以提供 getOffset(), getLength(), egetMatchString() 方法 -- 而不是返回一个 String, 这不会很大提高性能. 因为创建一个 Match 对象可能比创建一个 String 代价要小 -- 包括产生一个 char[] 数组和复制数据, 你仍然创建了一个中间对象, 对你的调用者来说没有价值.
这已经足够坏了, BadREgExpMatcher 强迫你使用它想看到的输入形式, 而不是你可以提供的更有效的形式. 但是使用 BadRegExpMathcer 还有另一个危险, 潜在地给 MailBot的性能带来更大的冒险: 在处理邮件头的时候, 你开始有避免使用 String 的倾向. 但是既然你被迫创建许多 String 对象来满足 BadRegExpMatcher, 你可能被引诱而放弃这个目标, 更加自由地使用 String. 现在, 一个组件的糟糕的设计已经影响了使用它的程序.
甚至你后来找到了一个更好的正规表达式的组件, 不需要你提供一个 String, 那时你的整个程序都会受影响.
一个好一些的接口
你怎样定义 BadRegExpMatcher, 而不引起这样的问题呢? 首先, BadRegExpMatcher 应该不规定它的输入. 它应该可以接受它的调用者能够有效提供的各种输入格式. 第二, 它不应该自动给匹配结果产生一个 String; 应该返回足够的信息, 这样调用者如果愿意的话可以生成它. (为方便着想, 它可以提供一个方法来做这件事, 但不是必须的) 这里有一个好一些的接口:
class BetterRegExpMatcher {
public BetterRegExpMatcher(...);
/** Provide matchers for multiple formats of input -- String,
character array, and subset of character array. Return -1 if no
match was made; return offset of match start if a match was
made. */
public int match(String inputText);
public int match(char[] inputText);
public int match(char[] inputText, int offset, int length);
/** Get the next match against the input text, if any */
public int getNextMatch();
/** If a match was made, returns the length of the match; between
the offset and the length, the caller should be able to
reconstruct the match text from the offset and length */
public int getMatchLength();
/** Convenience routine to get the match string, in the event the
caller happens to wants a String */
public String getMatchText();
}
新的接口减少了调用者把输入转换成匹配器希望的格式这个要求. MailBot 现在可以象下面这样调用 match():
int resultOffset = dateMatcher.match(myBuffer, thisHeaderStart,
thisHeaderEnd-thisHeaderStart);
if (resultOffset < 0) { ... }
这就解决了不创建任何新对象的目标. 作为一个附加的奖励, 它的接口设计风格加到了Java 的 "lots-of-simgle-methos" 设计哲学中.
额外的对象创建给性能的确切的冲击依赖于 matth() 所作的工作量. 你可以通过创建和计时两个正规表达式匹配器类, 来确定一个性能差别的上限. 在 Sun JDK 1.3 中, 上面的代码片段在 BetterRegExpMatcher 类中大约比 BadRegExpMatcher 类要快 50 倍左右. 使用一个简单的字串匹配的实现, BetterRegExpMatcher 比相对应的 BadRegExpMatcher 要快5倍。
交换类型
BadRegExpMatcher 强迫 MailBot 把输入文本从字符数组转换成 String, 结果是造成了一些不必要的对象的创建. 更具讽刺意味的是, BadRegExpMatcher 的许多实现都立即把 String 转换成一个字符数组, 使它容易对输入文本进行访问. 这样不仅仅申请了另一龆象, 并且还意味着你做完了所有的工作, 最后的形式和开始时一样. MailBot 和 BadRegExpMatcher都不想处理 String -- String 只是看起来象是在组件之间传递文本的很明显的格式.
在上面的 BadRegExpMatcher 例子中, String 类是作为一个交换类型的. 一个交换类型是一种不管是调用者还是被调用者都不想使用或者以它作为数据格式的一种类型, 但是两个都能很容易地转换它或者从它转换. 以交换类型定义接口在保持灵活性的同时减少了接口的复杂性, 但是有时简单性导致了高代价的性能.
一个交换类型最典型的例子是 JDBC ResultSet 接口. 它不可能象任何本地数据库提供的数据集一样提供它的 ResultSet 接口, 但是 JDBC 驱动通过实现一个 ResultSet 可以很容易地把数据库提供的本地数据表示包装起来. 同样, 客户端程序也不能象这样表示数据记录, 但是你几乎可以没有困难地把 ResultSet 转换为想要的数据表示. 在 JDBC 的例子中,你接受了这个层次的花费, 因为它带来了标准化和跨数据库实现的可移植性的好处. 但是,要注意交换类型带来的性能代价.
这完全不值得, 使用交换类型对性能的冲击不容易度量. 如果你对上面调用 BadRegExpMatcher的代码片段做测试的话, 它会在运行时创建 MailBot 的输入 String; 但是, String 的产生只用来满足 BadRegExpMatcher. 如果你想评定一个组件对程序性能的真正的冲击, 你应该不仅仅度量它的代码的资源使用状况, 还有那些使用它和恢复的代码. 这对于标准的测试工具此很难完成.
结论
不是所有的程序都关注于性能的, 不是所有的程序都有性能问题. 但是对那些关注这些的程序, 这篇文章所提到的都很重要, 因为它们不是在最后一分钟就可以修改的. 既然在你编写写代码使用一个类以后再修改它的接口非常困难, 那么在你的设计时期就花费一点额外的时间来考虑性能特性.
在第二部分, 我会演示一些利用可修改性和不可修改性来减少不必要的对象创建的方法.
第二部分: 减少对象创建
概要
许多通常的 Java 性能问题都起源于在设计过程早期中的类设计的思想, 早在许多开发者开始考虑性能问题之前. 在这个系列中, Brian Goetz 讨论了通常的 Java 性能上的冒险以及怎么在设计时候避免它们. 在第二部分, 他讨论了减少临时对象创建的一些技术。
虽然许多程序员把性能管理一直推迟到开发过程的最后, 性能考虑应该从第一天起就和设计周期结合在一起. 这个系列探索一些早期的设计思想能够极大影响应用程序性能的方法.在这篇文章里, 我继续探索大量临时对象创建的问题, 并且提供一些避免它们的一些技术.
临时对象就是一些生命周期比较短的对象, 一般用于保存其他数据而再没有其他用途. 程序员一般用临时变量向一个方法传递数据或者从一个方法返回数据. 第一部分探讨了临时对象是怎样给一个程序的性能带来负面的冲击, 以及一些类接口的设计思想怎样提供了临时对象的创建. 避免了那些接口的创建, 你就能极大地减少那些影响你的程序性能的临时对象创建的需求,
只是对 String 说不吗?
当它要创建临时变量时, String 类是最大的罪人中的一个. 为了演示, 在第一部分我写了一个正规表达式匹配的例子, 通过和一个类似的但是经过仔细设计的接口相比较, 演示了看起来无害的接口是怎样引起大量对象的创建, 而慢上几倍. 这里是原来的和好一些的类的接口:
BadRegExpMatcher
public class BadRegExpMatcher {
public BadRegExpMatcher(String regExp);
/** Attempts to match the specified regular expression against the input
text, returning the matched text if possible or null if not */
public String match(String inputText);
}
BetterRegExpMatcher
class BetterRegExpMatcher {
public BetterRegExpMatcher(...);
/** Provide matchers for multiple formats of input -- String,
character array, and subset of character array. Return -1 if no
match was made; return offset of match start if a match was
made. */
public int match(String inputText);
public int match(char[] inputText);
public int match(char[] inputText, int offset, int length);
/** If a match was made, returns the length of the match; between
the offset and the length, the caller should be able to
reconstruct the match text from the offset and length */
public int getMatchLength();
/** Convenience routine to get the match string, in the event the
caller happens to wants a String */
public String getMatchText();
}
大量使用 BadREgExpMatcher 的程序比使用 BtterRegExpMatcher 的要慢好多. 首先,调用者不得不创建一个 String 传入 match(), 接着 match() 又创建了一个 String 来返回匹配的文本. 结果是每次调用都有两个对象创建, 看起来不多, 但是如果要经常调用match(), 这些对象创建带给性能的代价就太打了. BadRegExpMatcher 的性能问题不是在它的实现中, 而是它的接口; 象它定义的接口, 没有办法避免一些临时变量的创建.
BetterRegExpMatcher 的 match() 用原类型(整数和字符数组)代替了 String 对象; 不需要创建中间对象来在调用者和 match() 之间传递信息.
既然在设计时候避免性能问题要比写完整个系统以后再修改要容易一些, 你应该注意你的类中控制对象创建的方法. 在 RegExpMatcher 的例子中, 它的方法要求和返回 String 对象, 就应该为潜在的性能冒险提个警告信号. 因为 String 类是不可变的, 除了最常用以外, 所有的 String 参数在每次调用处理函数时都需要创建一个新的 String.
不可变性对于性能来说是否很坏?
因为 String 经常和大量的对象创建联系在一起, 一般来说归咎于它的不可变性. 许多程序员认为不可变的对象与生俱来对性能没有好处. 但是, 事实多少会更复杂一些. 实际上, 不可变性有时候提供了性能上的优势, 可变性的对象有时候导致性能问题. 不管可变性对性能来说有帮助或者有害, 依赖于对象是怎么使用的.
程序经常处理和修改文本字符串 -- 和不可变性非常不匹配. 每次你想处理一个 String --想查找和解析出前缀或者子串, 变小写或者大写, 或者把两个字符串合并 -- 你必须创建一个新的 String 对象. (在合并的情况下, 编译器也会隐藏地创建一个 StringBuffer() 对象)
另一个方面, 一个不可变的对象的一个引用可以自由共享, 而不用担心被引用的对象要被修改, 这个比可变对象提供性能优势, 就象下一节例子所说的.
可变的对象有它们自己的临时对象问题.
在 RegExpMatcher 的例子中, 你看见了 当一个方法返回一个 String 类型时, 它通常需要新建一个 String 对象. BadRegExpMatcher 的一个问题就是 match() 返回一个对象而不是一个原类型 -- 但是只因为一个方法返回一个对象, 不意味着必须有一个新对象创建. 考虑一下 java.awt 中的几何类, 象 Point 和 Rectangle. 一个 Rectangle只是四个整数(x, y, 宽度, 长度)的容器. AWT Component 类存储组件的位置, 通过getBounds()作为一个Rectangle 返回
public class Component {
...
public Rectangle getBounds();
}
在上面的例子中, getBounds() 只是一个存储元 -- 它只使一些 Component 内部的一些状态信息可用. getBounds() 需要创建它返回的 Rectangle 吗? 可能. 考虑一下下面getBounds() 可能的实现.
public class Component {
...
protected Rectangle myBounds;
public Rectangle getBounds() { return myBounds; }
}
当一个调用者调用上面例子中的 getBounds(), 没有新对象创建 -- 因为组件已经知道它在哪里 -- 所以 getBounds() 效率很高. 但是 Rectangle 的可变性又有了其他问题. 当一个调用者运行一下程序会发生什么呢?
Rectangle r = component.getBounds();
...
r.height *= 2;
因为 Rectangle 是可变的, 它在 Component 不知道的情况下使 Component 移动. 对象AWT 这样的 GUI 工具箱来说, 这是个灾难, 因为当一个组件移动以后, 屏幕需要重绘, 件监听器需要被通知, 等等. 所以上面的实现 Component.getBounds() 的代码看起来很危险. 一个安全一点的实现就象下面这样:
public Rectangle getBounds() {
return new Rectangle(myBounds.x, myBounds.y,
myBounds.height, myBounds.width);
}
但是现在, 每一个 getBounds() 的调用都创建一个新对象, 就象 RegExpMatcher 一样.实际上, 下面的代码片段创建了 4 个临时对象:
int x = component.getBounds().x;
int y = component.getBounds().y;
int h = component.getBounds().height;
int w = component.getBounds().width;
在 String 的情况中, 对象创建是必要的, 因为 String 是不可变的. 但在这个例子中,对象的创建也是必要的, 因为 Rectangle 是可变的. 我们使用 String 避免了这个问题,在我们的接口中没有使用对象. 虽然在 RegExpMatcher 的情况下很好, 这个方法不总是可行的或者是希望的. 幸运的是, 你可以在实际类的时候可以使用一些技巧, 来免除太多小对象的问题, 而不是完全避免小对象.
减少对象的技巧 1: 加上好的存取函数
在 Swing 工具箱的初始版本中, 对象小对象的临时创建, 象 Point, Rectangle 和 Dimension极大地阻碍了性能. 把它们放在一个 Point 或者 Rectangle 中来一次返回多个值, 看起来更有效, 实际上, 对象的创建比多个方法调用代价更高. 在 Swing 的最后发布之前, 通过给 Component 和其他一些类加一些新的存取方法, 问题就简单地解决了, 就象下面这样:
public int getX() { return myBounds.x; }
public int getY() { return myBounds.y; }
public int getHeight() { return myBounds.height; }
public int getWidth() { return myBounds.width; }
现在一个调用者可以这样获取边界而不用创建对象:
int x = component.getX();
int y = component.getY();
int h = component.getHeight();
int w = component.getWidth();
getBounds() 的旧形式仍然支持; 好的存取方法简单地提供了有效的方法来达到相同的目的. 结果是, Rectangle 的接口全部在 Component 中使用. 当修改 Swing 包支持和使用这样的存取函数后, 在许多 Swing 操作中比以前要快到两倍. 这很好, 因为 GUI 代码非常注意性能 -- 用户等待发生一些事, 希望 UI 操作瞬间完成.
使用这个技术不好的地方就是你的对象提供了更多的方法, 有多于一个的方法来得到相同的信息, 就使文档更大更复杂, 可能使用户害怕. 但是就象 Swing 的例子显示的, 在关注性能的情况下, 这样的优化技术是有效的.
技巧 2: 利用可变性
除了给 Component 加上原类型的存储函数 -- 象上面讨论的 getX() 函数 -- 以外, Java 2 在 AWT 和 Swing 中也使用了另一种技术来减少对象创建, 允许一个调用者把边界作为一个 Rectangle 得到, 但是不需要任何临时对象的创建.
public Rectangle getBounds(Rectangle returnVal) {
returnVal.x = myBounds.x;
returnVal.y = myBounds.y;
returnVal.height = myBounds.height;
returnVal.width = myBounds.width;
return returnVal;
}
调用者仍然需要创建一个 Rectangle 对象, 但它可以在后来的调用中重用. 如果一个调用者在一系列的 Component 中循环, 可以只创建一个 Rectangle 对象, 在每个 Component 中重用. 注意这个技术只用于可变性对象; 你不能用这种方法消除 String 的创建.
技巧 3: 得到两个中的最好的.
一个解决在简单类(象 Point 之类)的对象创建的问题, 更好的方法是使 Point 对象不可? 但是定义一个可变的子类, 就象下面这样:
public class Point {
protected int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public final int getX() { return x; }
public final int getY() { return y; }
}
public class MutablePoint extends Point {
public final void setX(int x) { this.x = x; }
public final void setY(int y) { this.y = y; }
}
public class Shape {
private MutablePoint myLocation;
public Shape(int x, int y) { myLocation = new MutablePoint(x, y); }
public Point getLocation() { return (Point) myLocation; }
}
在上面的例子中, Shape 可以安全返回一个 myLocation 的引用, 因为调用者试图修改域或者调用设置函数会失败. (当然, 调用者仍然可以把 Point 转换为 MutablePoint, 但这明显不安全, 这样的调用者可能得到他们想要的) C++ 程序员可能注意到了这个技巧很象 C++ 中返回一个 Rectangle 的常量引用(cong Rectangle&) -- 一个 Java 不支持的特点.
这个技巧 -- 返回一个具有可变的和不可变的类, 只允许读的对象, 而不创建新对象 --在 Java 1.3 类库 java.math.BigInteger 类中使用. MutableBigInteger 类不可见 --它是一个只在 java.math 类库中内部使用的私有类型. 但是既然 BigInteger 的一些方法(象 gcd()) 在许多数学操作中都有, 在一个地方操作比创建上百个临时变量性能提高非常大.
结论
所有的性能优化的建议中, 值得记住的是有许多程序的性能可以完全接受的情况. 在这些情况下, 不值得牺牲可读性, 可维护性, 抽象, 或者其他可取的程序属性来获得性能. 但是, 既然许多性能问题的种子在设计时就种下了, 要注意到设计思想潜在地对性能的冲?当你设计的类在关注性能的情况性爱使用, 你可以有效地使用这里提到的技巧来减少临时对象的创建,
在第三部分中, 我将看看分布式的应用程序中特有的性能问题, 怎样在设计时候找出和消除它们。
第三部分: 远程接口
概述
许多 Java 的通常性能问题来源于设计过程早期的类设计想法中, 早在开发者开始考虑性能问题之前. 在这个系列中, Brian Goetz 讨论了一些通常的 Java 性能的冒险, 解释了怎样在设计时间避免它们. 在这篇文章中, 它检验了远程应用程序中的特定的性能问题.
远程调用的概念
在分布式的应用程序中, 一个运行在一个系统中的对象可以调用另一个系统中的一个对象的方法. 这个通过很多使远程对象表现为本地的结构的帮助而实现. 要访问一个远程对象,你首先要找到它, 可以通过使用目录或者命名服务来实现, 象 RMI 注册, JNDI, 或者 CORBA命名服务.
当你通过目录服务得到一个远程对象的引用时, 你并没有得到那个对象的实际的引用, 而是一个实现了和远程对象同样接口的stub对象的引用. 当你调用一个stub对象的方法时, 对象把方法的所有参数汇集起来 -- 把它们转化成一个字节流的表现形式, 类似于序列化过程. 这个stub对象把汇集的参数通过网络传递给一个skeleton对象, 把参数分解出来, 你想调用的实际的对象的方法. 然后这个方法向skeleton对象返回一个值, skeleton对象把它传送给stub对象, stub对象把它分解出来, 传递给调用者. Phew! 一个单独的调用要做这么多的工作. 很明显, 除去表面的相似性, 一个远程方法调用比本地方法调用更大.
以上描述浏览了一些对于程序性能非常重要的细节. 当一个远程方法返回的不是一个原类? 而是一个对象时, 会发生什么? 不一定. 如果返回的对象是一种支持远程方法调用的类型, 它就创建一个中stub对象和一个skeleton对象, 在这种情况下需要在注册表中查找一个远潭韵,这显然是一个高代价的操作. (远程对象支持一种分布式的垃圾回收的形式, 包括了每一个参与的 JVM 维护一个线程来和其他 JVM 的维护线程进行通讯, 来回传递引用信息). 如果返回的对象不支持远程调用, 这个对象所有的域和引用的对象都要汇集起来, 这也是一个代价的操作.
远程和本地方法调用的性能比较
远程对象访问的性能特征和本地的不一样:远程对象的创建比本地对象创建代价要高. 不仅仅是当它不存在时要创建它, 而且stub对和skeleton对象也要创建, 还要互相感知.
远程方法调用还包括网络的传递 -- 汇集起来的参数必须发送到远程系统, 而且响应也需汇集起来, 在调用程序重新得到控制权之前发送回来. 汇集, 分解, 网络延时, 实际的远调用所导致的延迟都加在一起; 客户端通常是等待所有这些而步骤完成. 一个远程调用也大地依赖于底层网络的延时.
不同的数据类型有不同的汇集开支. 汇集原类型相对来说花费少一些; 汇集简单的对象, Point 或者 String 要多一些; 汇集远程对象要多得多, 而汇集那些引用非常多的对象的对象(象 collection 等)要更多. 这和本地调用完全矛盾, 因为传递一个简单对象的引用比一个复杂对象的引用花费多.
接口设计是关键
设计不好的远程接口可能完全消除一个程序的性能. 不幸的是, 对本地对象来说好的接口的特性对远程对象可能不适合. 大量的临时对象创建, 就象在本系列的第一, 二部分讨论,也能阻碍分布式的应用程序, 但是大量的传递更是一个性能问题. 所以, 调用一个在一个时对象(比如一个 Point)中返回多个值的方法比多次调用来分别得到它们可能更有效.
实际远程应用程序的一些重要的性能指导:
提防不必要的数据传递. 如果一个对象要同时得到几个相关的项, 如果可能的话, 在一个远程调用中实现可能容易一些.
当调用者可能不必要保持一个远程对象的引用时, 提防返回远程的对象.当远程对象不需要一个对象的拷贝时, 提防传递复杂对象.
幸运的是, 你可以通过简单查看远程对象的接口来找出所有的问题. 要求做任何高层动作的方法调用序列可以从类接口中明显看到. 如果你看到一个通常的高层操作需要许多连续的远程方法调用, 这就是一个警告信号, 可能你需要重新查看一下类接口.
减少远程调用代价的技巧
一个例子, 考虑下面假定的管理一个组织目录的应用程序: 一个远程的 Directory 对象包含了 DirectoryEntry 对象的引用, 表现了电话簿的入口.
public interface Directory extends Remote {
DirectoryEntry[] getEntries();
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
public interface DirectoryEntry extends Remote {
String getName();
String getPhoneNumber();
String getEmailAddress();
}
现在假设你想在一个 GUI email 程序中使用 Directory 的东西. 程序首先调用getEntries() 来得到入口的列表, 接着在每个入口中调用 getName(), 计算结果的列表,当用户选择一个时, 应用程序在相应的入口调用 getEmailAdress() 来得到 email 地址.
在你能够写一封 email 之前有多少远程方法调用必须发生? 你必须调用 getEntries() 一次, 地址簿中每个入口调用一次 getName(), 一次 getEmailAddress(). 所以如果在地址中有 N 个入口, 你必须进行 N + 2 次远程调用. 注意你也需要创建 N + 1 个远程对象引用, 也是一个代价很高的操作. 如果你的地址簿有许多入口的话, 不仅仅是打开 email 窗口的时候非常慢, 也造成了网络阻塞, 给你的目录服务程序造成高负载, 导致可扩展性的问题.
现在考虑增强的 Directory 接口:
public interface Directory extends Remote {
String[] getNames();
DirectoryEntry[] getEntries();
DirectoryEntry getEntryByName(String name);
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
这将减少多少你的 email 程序所造成的花费呢? 现在你可以调用 Directory.getNames()一次就可以同时得到所有的名字, 只需要给你想要发送 email 的容器调用 getEntryByName() .这个过程需要 3 个远程方法调用, 而不是 N + 2, 和两个远程对象, 而不是 N + 1 个.如果地址簿有再多一点的名字, 这个调用的减少在程序的响应和网络负载和系统负载有很大的不同.
用来减少远程调用和引用传递的代价的技术叫做使用次要对象标识符. 使用一个对象的标属性, -- 在这个例子中, 是 name -- 而不是传回一个远程对象, 作为对象的一个轻量级晔斗?次要标识符包含了它描述的对象足够的信息, 这样你只需要获取你实际需要的远程对象.在这个目录系统的例子中, 一个人的名字是一个好的次要标识符. 在另一个例子中, 一个安全皮包管理系统, 一个采购标识号可能是一个好的次要标识符.
另一个减少远程调用数量的技巧是块获取. 你可以进一步给 Directory 接口加个方法, 来一次获取多个需要的 DirectoryEntry 对象:
public interface Directory extends Remote {
String[] getNames();
DirectoryEntry[] getEntries();
DirectoryEntry getEntryByName(String name);
DirectoryEntry[] getEntriesByName(String names[]);
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
现在你不仅可以得到需要的远程 DirectoryEntry , 也可以用单独一个远程方法调用得到要的所有的入口. 虽然这并不减少汇集的代价, 但极大地较少了网络往返的次数. 如果网延迟很重要的话, 就可以产生一个响应更快的系统(也能减少这个网络的使用).
照亮去向 RMI 层次的路径的第三的技巧是不把 DirectoryEntry 作为一个远程对象, 而把它定义为一个通常的对象, 带有访问 name, address, email address 和其他域的访问函数.(在 CORBA 系统中, 我可能要使用类似的 object-by-value 机制.) 然后, 当 email 应用程序调用 getEntryName() 时, 它会获取一个 entry 对象的值 -- 不需要创建一个stub对象或者skeleton对象, getEmailAddress() 的调用也是一个本地的调用而不是一个远程的.
当然, 所有这些技巧都都依赖于对远程对象实际上是怎样使用的理解上的, 但是对于这个理解, 你甚至不需要看一看远程类的实现就可以找出一些潜在的严重性能问题.
以上是“为JAVA性能而设计的示例分析”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注恰卡编程网行业资讯频道!