>  기사  >  Java  >  Java 코드에 대한 자세한 설명과 실행 효율성 향상을 위한 최적화

Java 코드에 대한 자세한 설명과 실행 효율성 향상을 위한 최적화

黄舟
黄舟원래의
2017-07-27 10:49:00971검색

프로그램에 사용 가능한 리소스(메모리, CPU 시간, 네트워크 대역폭 등)는 제한되어 있습니다. 최적화의 목적은 프로그램이 가능한 적은 리소스로 예약된 작업을 완료할 수 있도록 하는 것입니다. 최적화에는 일반적으로 코드 크기를 줄이는 것과 코드의 운영 효율성을 높이는 두 가지 측면이 포함됩니다. 이 기사에서는 주로 코드 효율성을 향상시키는 방법에 대해 설명합니다.

Java 프로그램에서 성능 문제의 원인은 대부분 Java 언어에 있는 것이 아니라 프로그램 자체에 있습니다. java.lang.String 클래스와 java.util.Vector 클래스를 정확하고 능숙하게 사용하는 등 좋은 코딩 습관을 기르는 것은 매우 중요하며 이는 프로그램 성능을 크게 향상시킬 수 있습니다. 아래에서 이 문제를 자세히 분석해 보겠습니다.

1. 클래스의 최종 수정자를 지정해 보세요. 최종 수정자가 있는 클래스는 파생될 수 없습니다. Java Core API에는 java.lang.String 등 final을 적용한 예가 많이 있습니다. String 클래스에 final을 지정하면 사람들이 length() 메서드를 재정의하는 것을 방지할 수 있습니다. 또한 클래스가 final로 지정되면 해당 클래스의 모든 메서드가 final이 됩니다. Java 컴파일러는 모든 최종 메소드를 인라인할 수 있는 기회를 찾습니다(이는 특정 컴파일러 구현에 따라 다름). 이를 통해 성능을 평균 50% 향상할 수 있습니다.

2. 객체를 최대한 재사용하세요. 특히 String 객체를 사용하는 경우 문자열 연결이 발생할 경우 StringBuffer를 대신 사용해야 합니다. 시스템이 객체를 생성하는 데 시간이 걸릴 뿐만 아니라 나중에 이러한 객체를 가비지 수집하고 처리하는 데에도 시간이 걸릴 수 있습니다. 따라서 너무 많은 개체를 생성하면 프로그램 성능에 큰 영향을 미칩니다.

3. 로컬 변수를 사용해 보세요. 메소드 호출 시 전달된 매개변수와 호출 중에 생성된 임시 변수가 모두 스택(Stack)에 저장되므로 속도가 더 빨라집니다. 정적 변수, 인스턴스 변수 등과 같은 다른 변수는 힙에 생성되며 속도가 느립니다. 또한 특정 컴파일러/JVM에 따라 지역 변수가 더욱 최적화될 수 있습니다. 가능한 경우 스택 변수 사용을 참조하세요.

4. 변수를 반복적으로 초기화하지 마세요. 기본적으로 클래스 생성자를 호출하면 Java는 변수를 특정 값으로 초기화합니다. 모든 개체는 null로 설정되고 정수 변수(byte, short, int, long)가 설정됩니다. 0으로 설정하면 float, double 변수는 0.0으로, 논리값은 false로 설정됩니다. 클래스가 다른 클래스에서 파생될 때 특히 주의해야 합니다. 왜냐하면 new 키워드를 사용하여 객체를 생성하면 생성자 체인의 모든 생성자가 자동으로 호출되기 때문입니다.

5. JAVA + Oracle 응용 시스템 개발 시 Java에 포함된 SQL 문은 Oracle 파서의 구문 분석 부담을 줄이기 위해 최대한 대문자를 사용해야 합니다.

6. Java 프로그래밍 중에는 사용 후 릴리스 리소스에 가까운 경우에도 데이터베이스 연결 및 I/O 스트림 작업을 수행할 때 주의하세요. 이러한 대형 개체를 작동하면 시스템 오버헤드가 많이 발생하고 조금만 부주의하면 심각한 결과를 초래할 수 있기 때문입니다.

7. JVM에는 자체 GC 메커니즘이 있으므로 프로그램 개발자가 너무 많은 고려를 필요로 하지 않으므로 개발자의 부담이 어느 정도 줄어들지만 동시에 과도한 객체 생성도 놓칠 수 있습니다. 많은 시스템 리소스를 소비합니다. 메모리는 심각한 경우 메모리 누수로 이어질 수 있으므로 만료된 개체를 적시에 재활용하는 것이 매우 중요합니다. JVM이 가비지를 수집하는 조건은 해당 객체가 더 이상 참조되지 않는다는 것입니다. 그러나 JVM의 GC는 그리 스마트하지 않으며 객체가 가비지 수집 조건을 충족하더라도 즉시 재활용되지 않을 수 있습니다. 따라서 객체를 사용한 후에는 수동으로 null로 설정하는 것이 좋습니다.

8. 동기화 메커니즘을 사용할 때는 코드 블록 동기화 대신 메서드 동기화를 사용해야 합니다.

9. 반복되는 변수 계산을 줄여보세요

예:

for(int i = 0;i < list.size; i ++) {
             …
}

for(int i = 0,int len = list.size();i < len; i ++) {
             …
}

10으로 바꿔야 합니다. 즉, 필요할 때 생성을 시작하세요.

예:

String str = “aaa”;
             if(i == 1) {
                 list.add(str);
}

   if(i == 1) {
String str = “aaa”;
                 list.add(str);
}

11로 바꿔야 합니다. 예외는 주의해서 사용하세요.

예외는 성능에 해롭습니다. 먼저 예외를 발생시키면 새 객체가 생성됩니다. Throwable 인터페이스의 생성자는 fillInStackTrace()라는 Native 메서드를 호출합니다. fillInStackTrace() 메서드는 스택을 확인하고 호출 추적 정보를 수집합니다. 예외가 발생할 때마다 처리 중에 새 개체가 생성되므로 VM은 호출 스택을 조정해야 합니다. 예외는 오류 처리에만 사용해야 하며 프로그램 흐름을 제어하는 ​​데 사용해서는 안 됩니다.

12. 루프에서 사용하지 마세요.

Try {
} catch() {
}

는 가장 바깥쪽 레이어에 배치되어야 합니다.

13. StringBuffer 사용:

StringBuffer는 쓰기 가능한 문자열을 나타냅니다.

세 가지 구성 방법이 있습니다.

StringBuffer ();             //默认分配16个字符的空间
StringBuffer (int size);   //分配size个字符的空间
StringBuffer (String str);   //分配16个字符+str.length()个字符空间

StringBuffer 생성자를 통해 초기 용량을 설정하면 성능이 크게 향상될 수 있습니다. 여기에 언급된 생성자는 StringBuffer(int length)이고, length 매개변수는 현재 StringBuffer가 보유할 수 있는 문자 수를 나타냅니다. 또한 verifyCapacity(int maximumcapacity) 메서드를 사용하여 StringBuffer 객체가 생성된 후 객체의 용량을 설정할 수도 있습니다. 먼저 StringBuffer의 기본 동작을 살펴보고 성능을 향상시키는 더 나은 방법을 찾아보겠습니다.

StringBuffer在内部维护一个字符数组,当你使用缺省的构造函数来创建StringBuffer对象的时候,因为没有设置初始化字符长度,StringBuffer的容量被初始化为16个字符,也就是说缺省容量就是16个字符。当StringBuffer达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,也就是(2*旧值+2)。如果你使用缺省值,初始化之后接着往里面追加字符,在你追加到第16个字符的时候它会将容量增加到34(2*16+2),当追加到34个字符的时候就会将容量增加到70(2*34+2)。无论何事只要StringBuffer到达它的最大容量它就不得不创建一个新的字符数组然后重新将旧字符和新字符都拷贝一遍――这也太昂贵了点。所以总是给StringBuffer设置一个合理的初始化容量值是错不了的,这样会带来立竿见影的性能增益。

StringBuffer初始化过程的调整的作用由此可见一斑。所以,使用一个合适的容量值来初始化StringBuffer永远都是一个最佳的建议。

14、合理的使用Java类 java.util.Vector。

简单地说,一个Vector就是一个java.lang.Object实例的数组。Vector与数组相似,它的元素可以通过整数形式的索引访问。但是,Vector类型的对象在创建之后,对象的大小能够根据元素的增加或者删除而扩展、缩小。请考虑下面这个向Vector加入元素的例子:

Object obj = new Object();
Vector v = new Vector(100000);
for(int I=0;
I<100000; I++) { v.add(0,obj); }

  除非有绝对充足的理由要求每次都把新元素插入到Vector的前面,否则上面的代码对性能不利。在默认构造函数中,Vector的初始存储能力是10个元素,如果新元素加入时存储能力不足,则以后存储能力每次加倍。Vector类就象StringBuffer类一样,每次扩展存储能力时,所有现有的元素都要复制到新的存储空间之中。下面的代码片段要比前面的例子快几个数量级:

Object obj = new Object();
Vector v = new Vector(100000);
for(int I=0; I<100000; I++) { v.add(obj); }

  同样的规则也适用于Vector类的remove()方法。由于Vector中各个元素之间不能含有“空隙”,删除除最后一个元素之外的任意其他元素都导致被删除元素之后的元素向前移动。也就是说,从Vector删除最后一个元素要比删除第一个元素“开销”低好几倍。

  假设要从前面的Vector删除所有元素,我们可以使用这种代码:

for(int I=0; I<100000; I++)
{
 v.remove(0);
}

  但是,与下面的代码相比,前面的代码要慢几个数量级:

for(int I=0; I<100000; I++)
{
 v.remove(v.size()-1);
}

  从Vector类型的对象v删除所有元素的最好方法是:

v.removeAllElements();

  假设Vector类型的对象v包含字符串“Hello”。考虑下面的代码,它要从这个Vector中删除“Hello”字符串:

String s = "Hello";
int i = v.indexOf(s);
if(I != -1) v.remove(s);

  这些代码看起来没什么错误,但它同样对性能不利。在这段代码中,indexOf()方法对v进行顺序搜索寻找字符串“Hello”,remove(s)方法也要进行同样的顺序搜索。改进之后的版本是:

String s = "Hello";
int i = v.indexOf(s);
if(I != -1) v.remove(i);

  这个版本中我们直接在remove()方法中给出待删除元素的精确索引位置,从而避免了第二次搜索。一个更好的版本是:

String s = "Hello"; v.remove(s);

  最后,我们再来看一个有关Vector类的代码片段:

for(int I=0; I++;I < v.length)

  如果v包含100,000个元素,这个代码片段将调用v.size()方法100,000次。虽然size方法是一个简单的方法,但它仍旧需要一次方法调用的开销,至少JVM需要为它配置以及清除堆栈环境。在这里,for循环内部的代码不会以任何方式修改Vector类型对象v的大小,因此上面的代码最好改写成下面这种形式:

int size = v.size(); for(int I=0; I++;I<size)

  虽然这是一个简单的改动,但它仍旧赢得了性能。毕竟,每一个CPU周期都是宝贵的。

15、当复制大量数据时,使用System.arraycopy()命令。

16、代码重构:增强代码的可读性。

     例如:

public class ShopCart {
         private List carts ;
         …
         public void add (Object item) {
             if(carts == null) {
                 carts = new ArrayList();
}
crts.add(item);
}
public void remove(Object item) {
     if(carts. contains(item)) {
         carts.remove(item);
}
}
public List getCarts() {
     //返回只读列表
     return Collections.unmodifiableList(carts);
}
//不推荐这种方式
//this.getCarts().add(item);
     }

17、不用new关键词创建类的实例

用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。

在使用设计模式(Design Pattern)的场合,如果用Factory模式创建对象,则改用clone()方法创建新的对象实例非常简单。例如,下面是Factory模式的一个典型实现:

public static Credit getNewCredit() {
     return new Credit();
}

改进后的代码使用clone()方法,如下所示:

private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
     return (Credit) BaseCredit.clone();
}

上面的思路对于数组处理同样很有用。

18、乘法和除法

考虑下面的代码:

for (val = 0; val < 100000; val +=5) {
alterX = val * 8; myResult = val * 2;
}

用移位操作替代乘法操作可以极大地提高性能。下面是修改后的代码:

for (val = 0; val < 100000; val += 5) {
alterX = val << 3; myResult = val << 1;
}

修改后的代码不再做乘以8的操作,而是改用等价的左移3位操作,每左移1位相当于乘以2。相应地,右移1位操作相当于除以2。值得一提的是,虽然移位操作速度快,但可能使代码比较难于理解,所以最好加上一些注释。

19、在JSP页面中关闭无用的会话。

     一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 dfa91ee3dc7e70be31b6621145c9e8db 关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的session对象的来历。由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。

对于那些无需跟踪会话状态的页面,关闭自动创建的会话可以节省一些资源。使用如下page指令:0a3b7182a31ab96bc63fe317bf2e190f

20、JDBC与I/O

如果应用程序需要访问一个规模很大的数据集,则应当考虑使用块提取方式。默认情况下,JDBC每次提取32行数据。举例来说,假设我们要遍历一个5000行的记录集,JDBC必须调用数据库157次才能提取到全部数据。如果把块大小改成512,则调用数据库的次数将减少到10次。

[p][/p]21、Servlet与内存使用

许多开发者随意地把大量信息保存到用户会话之中。一些时候,保存在会话中的对象没有及时地被垃圾回收机制回收。从性能上看,典型的症状是用户感到系统周期性地变慢,却又不能把原因归于任何一个具体的组件。如果监视JVM的堆空间,它的表现是内存占用不正常地大起大落。

解决这类内存问题主要有二种办法。第一种办法是,在所有作用范围为会话的Bean中实现HttpSessionBindingListener接口。这样,只要实现valueUnbound()方法,就可以显式地释放Bean使用的资源。另外一种办法就是尽快地把会话作废。大多数应用服务器都有设置会话作废间隔时间的选项。另外,也可以用编程的方式调用会话的setMaxInactiveInterval()方法,该方法用来设定在作废会话之前,Servlet容器允许的客户请求的最大间隔时间,以秒计。

22、使用缓冲标记

一些应用服务器加入了面向JSP的缓冲标记功能。例如,BEA的WebLogic Server从6.0版本开始支持这个功能,Open Symphony工程也同样支持这个功能。JSP缓冲标记既能够缓冲页面片断,也能够缓冲整个页面。当JSP页面执行时,如果目标片断已经在缓冲之中,则生成该片断的代码就不用再执行。页面级缓冲捕获对指定URL的请求,并缓冲整个结果页面。对于购物篮、目录以及门户网站的主页来说,这个功能极其有用。对于这类应用,页面级缓冲能够保存页面执行的结果,供后继请求使用。

23、选择合适的引用机制

在典型的JSP应用系统中,页头、页脚部分往往被抽取出来,然后根据需要引入页头、页脚。当前,在JSP页面中引入外部资源的方法主要有两种:include指令,以及include动作。

include指令:例如a63acf318e7c4fe9d717a9fe10196222。该指令在编译时引入指定的资源。在编译之前,带有include指令的页面和指定的资源被合并成一个文件。被引用的外部资源在编译时就确定,比运行时才确定资源更高效。

include动作:例如bf91f76f86770bb1caad1176c08485a8。该动作引入指定页面执行后生成的结果。由于它在运行时完成,因此对输出结果的控制更加灵活。但时,只有当被引用的内容频繁地改变时,或者在对主页面的请求没有出现之前,被引用的页面无法确定时,使用include动作才合算。

24、及时清除不再需要的会话

为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多会话时,如果内存容量不足,操作系统会把部分内存数据转移到磁盘,应用服务器也可能根据“最近最频繁使用”(Most Recently Used)算法把部分不活跃的会话转储到磁盘,甚至可能抛出“内存不足”异常。在大规模系统中,串行化会话的代价是很昂贵的。当会话不再需要时,应当及时调用HttpSession.invalidate()方法清除会话。HttpSession.invalidate()方法通常可以在应用的退出页面调用。

25、不要将数组声明为:public static final 。

26、HashMap的遍历效率讨论

经常遇到对HashMap中的key和value值对的遍历操作,有如下两种方法:

Map<String, String[]> paraMap = new HashMap<String, String[]>();
................//第一个循环
Set<String> appFieldDefIds = paraMap.keySet();
for (String appFieldDefId : appFieldDefIds) {
String[] values = paraMap.get(appFieldDefId);
......
}
//第二个循环
for(Entry<String, String[]> entry : paraMap.entrySet()){
String appFieldDefId = entry.getKey();
String[] values = entry.getValue();
.......
}

第一种实现明显的效率不如第二种实现。

分析如下 Setf7e83be87db5cd2d9a8a0b8117b38cd4 appFieldDefIds = paraMap.keySet(); 是先从HashMap中取得keySet

代码如下:

public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
private class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}

其实就是返回一个私有类KeySet, 它是从AbstractSet继承而来,实现了Set接口。

再来看看for/in循环的语法

for(declaration : expression_r)
statement

在执行阶段被翻译成如下各式

for(Iterator<E> #i = (expression_r).iterator(); #i.hashNext();){
declaration = #i.next();
statement
}

因此在第一个for语句for (String appFieldDefId : appFieldDefIds) 中调用了HashMap.keySet().iterator() 而这个方法调用了newKeyIterator()

Iterator<K> newKeyIterator() {
return new KeyIterator();
}
private class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}

所以在for中还是调用了

在第二个循环for(Entryb3de66495e219efcd34160624a719d47 entry : paraMap.entrySet())中使用的Iterator是如下的一个内部类

private class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}

此时第一个循环得到key,第二个循环得到HashMap的Entry

效率就是从循环里面体现出来的第二个循环此致可以直接取key和value值

而第一个循环还是得再利用HashMap的get(Object key)来取value值

现在看看HashMap的get(Object key)方法

public V get(Object key) {
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length); //Entry[] table
Entry<K,V> e = table;
while (true) {
if (e == null)
return null;
if (e.hash == hash && eq(k, e.key))
return e.value;
         e = e.next;
}
}

其实就是再次利用Hash值取出相应的Entry做比较得到结果,所以使用第一中循环相当于两次进入HashMap的Entry中

而第二个循环取得Entry的值之后直接取key和value,效率比第一个循环高。其实按照Map的概念来看也应该是用第二个循环好一点,它本来就是key和value的值对,将key和value分开操作在这里不是个好选择。

27、array(数组) 和 ArryList的使用

array([]):最高效;但是其容量固定且无法动态改变;

ArrayList:容量可动态增长;但牺牲效率;

基于效率和类型检验,应尽可能使用array,无法确定数组大小时才使用ArrayList!

ArrayList是Array的复杂版本

ArrayList内部封装了一个Object类型的数组,从一般的意义来说,它和数组没有本质的差别,甚至于ArrayList的许多方法,如Index、IndexOf、Contains、Sort等都是在内部数组的基础上直接调用Array的对应方法。

ArrayList存入对象时,抛弃类型信息,所有对象屏蔽为Object,编译时不检查类型,但是运行时会报错。

注:jdk5中加入了对泛型的支持,已经可以在使用ArrayList时进行类型检查。

从这一点上看来,ArrayList与数组的区别主要就是由于动态增容的效率问题了

28、尽量使用HashMap 和ArrayList ,除非必要,否则不推荐使用HashTable和Vector ,后者由于使用同步机制,而导致了性能的开销。

29、StringBuffer 和StringBuilder的区别:

     java.lang.StringBuffer线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。StringBuilder。与该类相比,通常应该优先使用 java.lang.StringBuilder类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。为了获得更好的性能,在构造 StirngBuffer 或 StirngBuilder 时应尽可能指定它的容量。当然,如果你操作的字符串长度不超过 16 个字符就不用了。 相同情况下使用 StirngBuilder 相比使用 StringBuffer 仅能获得 10%-15% 左右的性能提升,但却要冒多线程不安全的风险。而在现实的模块化编程中,负责某一模块的程序员不一定能清晰地判断该模块是否会放入多线程的环境中运行,因此:除非你能确定你的系统的瓶颈是在 StringBuffer 上,并且确定你的模块不会运行在多线程模式下,否则还是用 StringBuffer 吧。

위 내용은 Java 코드에 대한 자세한 설명과 실행 효율성 향상을 위한 최적화의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.