1.用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();}
面的思路对于数组处理同样很有用。
2. 使用非阻塞I/O
版本较低的JDK不支持非阻塞I/O API。为避免I/O阻塞,一些应用采用了创建大量线程的办法(在较好的情况下,会使用一个缓冲池)。这种技术可以在许多必须支持并发I/O流的应用中见到,如Web服务器、报价和拍卖应用等。然而,创建Java线程需要相当可观的开销。
3. 慎用异常
异常对性能不利。抛出异常首先要创建一个新的对象。Throwable接口的构造函数调用名为fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,VM就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
4. 不要重复初始化变量
默认情况下,调用类的构造函数时, Java会把变量初始化成确定的值:所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键词创建一个对象时,构造函数链中的所有构造函数都会被自动调用。
5. 尽量指定类的final修饰符
带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String。为String类指定final防止了人们覆盖length()方法。另外,如果指定一个类为final,则该类所有的方法都是final。Java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。
6. 尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。另外,依赖于具体的编译器/JVM,局部变量还可能得到进一步优化。
7. 乘法和除法
考虑下面的代码:
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。值得一提的是,虽然移位操作速度快,但可能使代码比较难于理解,所以最好加上一些注释。
3. 选择合适的引用机制
在典型的JSP应用系统中,页头、页脚部分往往被抽取出来,然后根据需要引入页头、页脚。当前,在JSP页面中引入外部资源的方法主要有两种:include指令,以及include动作。
include指令:例如
<%@ include file="copyright.html" %>
该指令在编译时引入指定的资源。在编译之前,带有include指令的页面和指定的资源被合并成一个文件。被引用的外部资源在编译时就确定,比运行时才确定资源更高效。
include动作:例如
<jsp:include page="copyright.jsp" />
该动作引入指定页面执行后生成的结果。由于它在运行时完成,因此对输出结果的控制更加灵活。但时,只有当被引用的内容频繁地改变时,或者在对主页面的请求没有出现之前,被引用的页面无法确定时,使用include动作才合算。
4. 在spring中对orm层的动作设置只读属性
将 (只对数据库进行读取的操作) 设置只读属性
10. Servlet与内存使用
许多开发者随意地把大量信息保存到用户会话之中。一些时候,保存在会话中的对象没有及时地被垃圾回收机制回收。从性能上看,典型的症状是用户感到系统周期性地变慢,却又不能把原因归于任何一个具体的组件。如果监视JVM的堆空间,它的表现是内存占用不正常地大起大落。解决这类内存问题主要有二种办法。第一种办法是,在所有作用范围为会话的Bean中实现HttpSessionBindingListener接口。这样,只要实现valueUnbound()方法,就可以显式地释放Bean使用的资源。
另外一种办法就是尽快地把会话作废。大多数应用服务器都有设置会话作废间隔时间的选项。另外,也可以用编程的方式调用会话的setMaxInactiveInterval()方法,该方法用来设定在作废会话之前,Servlet容器允许的客户请求的最大间隔时间,以秒计算。
11. HTTP Keep-Alive
Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。市场上的大部分Web服务器,包括iPlanet、IIS和Apache,都支持HTTP Keep-Alive。对于提供静态内容的网站来说,这个功能通常很有用。但是,对于负担较重的网站来说,这里存在另外一个问题:虽然为客户保留打开的连接有一定的好处,但它同样影响了性能,因为在处理暂停期间,本来可以释放的资源仍旧被占用。当Web服务器和应用服务器在同一台机器上运行时,Keep-Alive功能对资源利用的影响尤其突出。
12. JDBC与Unicode
想必你已经了解一些使用JDBC时提高性能的措施,比如利用连接池、正确地选择存储过程和直接执行的SQL、从结果集删除多余的列、预先编译SQL语句,等等。除了这些显而易见的选择之外,另一个提高性能的好选择可能就是把所有的字符数据都保存为Unicode(代码页13488)。Java以Unicode形式处理所有数据,因此,数据库驱动程序不必再执行转换过程。但应该记住:如果采用这种方式,数据库会变得更大,因为每个Unicode字符需要2个字节存储空间。另外,如果有其他非Unicode的程序访问数据库,性能问题仍旧会出现,因为这时数据库驱动程序仍旧必须执行转换过程。
13. JDBC与I/O
如果应用程序需要访问一个规模很大的数据集,则应当考虑使用块提取方式。默认情况下,JDBC每次提取32行数据。举例来说,假设我们要遍历一个5000行的记录集,JDBC必须调用数据库157次才能提取到全部数据。如果把块大小改成512,则调用数据库的次数将减少到10次。在一些情形下这种技术无效。例如,如果使用可滚动的记录集,或者在查询中指定了FOR UPDATE,则块操作方式不再有效。
Java代码优化--尽可能地使用stack(栈)变量(方法内部的局部变量)
Java程序包含了大量的对象,我们需要了解它们是从哪里被访问的,变量存储于何处对程序的性能有显著的影响--尤其是某些需要被频繁访问的变量。
我们写一个Java类,在其内部方法中定义的局部变量或对象是存储在stack(堆栈)中的,且JVM是一种stack-based的,因此访问和操纵stack中的数据时性能最佳。而Java类的instance变量(这个类的field)和static变量是在constant pool(常量池)中存储和得到访问的。constant pool中保存了所有的符号引用(symbolic references),指向所有型别(types)、值域(field),以及每个型别所使用的所有函数(mothods)。访问instance和static变量时,由于它们存放于constant pool中,所以JVM需要使用更多更耗时的操作码(分析程序生成的bytecode可以看出来)来访问它们。
下面给出一段代码示例,对比后说明怎么尽可能地使用stack变量:
package test;
public class StackVars {
private int x; // instance变量
private static int staticX; //static 变量
public void stackAccess(int val) { //访问和操作stack变量j
int j = 0;
for (int i = 0; i < val; i++) {
j += 1;
}
}
public void instanceAccess(int val) {//访问和操作instance变量x
for (int i = 0; i < val; i++) {
x += 1;
}
}
public void staticAccess(int val) {//访问和操作static变量staticX
for (int i = 0; i < val; i++) {
staticX += 1;
}
}
}
经测试,发现运行instanceAccess()和staticAccess()方法的时间大约相同,但却比运行stackAccess()方法慢了2~3倍。因此我们对instanceAccess()、staticAccess()两个方法的代码作以下调整,以得到更快的性能:
public void instanceAccess(int val) {//访问和操作instance变量x
int tempX=x;
for (int i = 0; i < val; i++) {
tempX += 1;
}
x=tempX;
}
public void staticAccess(int val) {//访问和操作static变量staticX
int tempStaticX=staticX;
for (int i = 0; i < val; i++) {
tempStaticX += 1;
}
staticX=tempStaticX;
}
改善之处就是将instance和static变量放到循环之外,而用一个stack变量来完成多次局部运算,最后再将这个stack变量的值传回instance或static变量,从而提高了代码的性能。
Sun JDK自带JVM内存使用分析工具HProf
使用Sun JDK自带JVM内存使用分析工具HProf可以分析JVM堆栈,从而找到占用内存较大的对象。这对应经常出现内存泄漏(OOM)的JAVA系统进行调优很有帮助。
HProf使用方法
• 在WeblogicServer启动脚本中增加-Xrunhprof:heap=sites,重新启动WeblogicServer。
• 使用kill -3 <pid> 或退出WeblogicServer均会生成java.hprof.txt文件,直接打开此文件便可分析JVM的具体运行情况。
从java.hprof.txt记录的JVM堆栈结果中可以发现JVM占用内存较大的对象:
percent live alloc'ed stack class
rank self accum bytes objs bytes objs trace name
1 4.57% 4.57% 2289696 47702 8392224 174838 4251 [C
2 3.99% 8.57% 2000016 1 2000016 1 12308 [C
3 3.65% 12.22% 1827552 9622 1852672 10082 43265 [C
4 2.58% 14.80% 1293912 53913 3929424 163726 4258 java.lang.String
5 2.05% 16.85% 1028664 7585 3207272 24923 4252 [C
6 2.03% 18.88% 1015816 159 1015816 159 18694 [B
7 1.88% 20.77% 942080 230 2740224 669 20416 [B
8 1.61% 22.37% 805752 2142 2150856 4635 45318 [B
9 1.60% 23.98% 802880 772 802880 772 24710 weblogic.servlet.utils.URLMatchMap$URLMatchNode
10 1.60% 25.57% 799400 19985 2781400 69535 45073 cnc.util.Field
11 1.36% 26.93% 679360 3805 679360 3805 494 [B
12 1.35% 28.28% 674856 28119 5181240 215885 2985 java.util.HashMap$Entry
……
……
96 0.19% 63.73% 94776 3112 94776 3112 9146 [C
97 0.19% 63.92% 93456 3894 123936 5164 23631 java.lang.String
98 0.19% 64.10% 93224 3884 123968 5165 23644 java.lang.String
99 0.19% 64.29% 93192 3883 123936 5164 23636 java.lang.String
100 0.18% 64.47% 89528 238 240264 520 33227 [B
101 0.17% 64.64% 86448 1901 103472 2255 18715 java.lang.Object
102 0.17% 64.81% 85464 676 85768 695 18715 [S
103 0.17% 64.98% 85184 1331 85184 1331 28266 weblogic.ejb20.internal.MethodDescriptor
104 0.17% 65.15% 84224 752 84224 752 24148 weblogic.servlet.internal.dd.ServletDescriptor
105 0.17% 65.32% 84136 528 50471136 348769 63 [C
106 0.16% 65.48% 79968 1428 388976 6946 5503 java.lang.reflect.Method
107 0.15% 65.63% 77520 1615 77520 1615 27967 weblogic.ejb20.deployer.mbimpl.MethodInfoImpl
108 0.15% 65.79% 77056 4816 469808 29363 20250 java.lang.Object
109 0.15% 65.94% 76960 74 76960 74 23695 [B
110 0.15% 66.09% 76104 3171 215040 8960 45071 cnc.util.FyCol
111 0.15% 66.24% 74688 3112 74688 3112 9152 java.util.Hashtable$Entry
112 0.15% 66.39% 74688 3112 74688 3112 9147 java.lang.String
113 0.15% 66.54% 74280 61 794328 788 45313 [C
114 0.14% 66.68% 72480 1510 436032 9084 45353 [C
115 0.14% 66.82% 70720 68 70720 68 25869 [B
116 0.14% 66.97% 70720 68 70720 68 27448 [B
117 0.14% 67.11% 70272 1279 142672 2439 5503 [C
118 0.14% 67.24% 69256 86 69256 86 6584 [S
119 0.13% 67.38% 67056 66 67056 66 28882 java.lang.Object
120 0.13% 67.51% 66176 752 66176 752 24170 weblogic.servlet.internal.dd.UIDescriptor
121 0.13% 67.64% 65688 715 65688 715 25389 [C
122 0.13% 67.77% 65600 4 885600 54 23939 [C
123 0.13% 67.90% 65600 4 623200 38 40639 [C
124 0.13% 68.03% 65576 367 65576 367 51686 [C
125 0.13% 68.17% 65568 2 65568 2 30610 java.util.HashMap$Entry
126 0.13% 68.30% 65568 2 130816 16 43271 java.util.HashMap$Entry
127 0.13% 68.43% 65552 1 65552 1 16617 [B
128 0.13% 68.56% 64600 1615 64600 1615 27969 java.util.HashMap
129 0.13% 68.68% 63888 2662 64032 2668 16951 java.util.HashMap$Entry
130 0.13% 68.81% 63888 2662 64032 2668 16997 java.util.HashMap$Entry
131 0.13% 68.94% 63888 2662 64032 2668 16996 weblogic.rmi.internal.ClientMethodDescriptor
132 0.13% 69.07% 63888 2662 99120 4130 16949 java.lang.String
133 0.13% 69.19% 63888 2662 64032 2668 16976 java.lang.String
134 0.13% 69.32% 63232 152 63232 152 9655 weblogic.utils.collections.ConcurrentHashMap$Entry
135 0.13% 69.45% 63232 152 63232 152 9704 weblogic.utils.collections.ConcurrentHashMap$Entry
136 0.12% 69.57% 62168 3885 82632 5164 23628 [B
137 0.12% 69.69% 61680 406 66904 468 1 [C
138 0.12% 69.82% 61504 4 246016 16 47372 [B
139 0.12% 69.94% 61144 36 91019160 23904 92 [B
140 0.12% 70.06% 61040 763 61040 763 24194 weblogic.servlet.internal.dd.ServletMappingDescriptor
141 0.12% 70.18% 60400 1510 363360 9084 45338 java.util.Hashtable
142 0.12% 70.30% 59544 827 59544 827 24746 weblogic.servlet.internal.ServletRuntimeMBeanImpl
143 0.12% 70.42% 59248 1058 484984 8664 33236 oracle.jdbc.ttc7.TTCItem
144 0.12% 70.53% 58152 232 187176 764 748 [C
145 0.12% 70.65% 57888 2412 161904 6746 16621 java.lang.String
146 0.11% 70.77% 57400 1435 57400 1435 16855 java.util.HashMap
……
……
根据以上的结果,在java.hprof.txt中定位到导致分配大内存的操作如下:
TRACE 63:
java.lang.StringBuffer.expandCapacity(StringBuffer.java:202)
java.lang.StringBuffer.append(StringBuffer.java:401)
java.util.zip.ZipFile.getEntry(ZipFile.java:148)
java.util.jar.JarFile.getEntry(JarFile.java:198)
TRACE 92:
java.util.zip.InflaterInputStream.<init>(InflaterInputStream.java:71)
java.util.zip.ZipFile$1.<init>(ZipFile.java:240)
java.util.zip.ZipFile.getInputStream(ZipFile.java:212)
java.util.zip.ZipFile.getInputStream(ZipFile.java:183)
再进一步分析则需要应用开发人员对应用代码做相应的分析定位。
注意:使用HProf非常消耗资源,切记不要在生产系统使用。
Java优化编程(第二版)
2.1 垃圾回收堆内存
内存管理的话题在C或C++程序设计中讨论得相对较多,因为在C与C++程序设计中需要开发人员自己申请并管理内存,开发人员可以申请/借用(Apply)系统内存并且负责释放/归还(Release)系统内存,如果“只借不还”就会造成系统内存泄露的问题。在Java程序设计中,这些工作由Java虚拟机(JVM)负责处理。所有内存的申请、分配、释放都由JVM负责完成。因此,开发人员就省去了这部分工作,不过这并不意味着开发人员可以完全依赖于JVM的内存管理功能,如果你这样想并在实际的应用开发中也这样做,你所开发的应用的性能,就有可能不是最优的。这是因为,无论配置多么优良的硬件环境其自身资源都是有限的,由于没有合理、科学的使用内存资源,即使是Java应用也会出现内存枯竭的现象。例如,我们经常会遇到的OutOfMemoryException。再者Java语言(其实不只Java语言)的性能极大程度上依赖于其运行的硬件环境资源,而内存又是硬件环境资源中重要的一部分,因此说,如果开发人员开发的Java应用没能有效、合理地使用系统内存,那么这个应用就不可能具备较高的性能,甚至会导致整个系统在运行一段时间后崩溃。本章将对Java应用开发中与内存管理相关的技术做详细的讲解。
2.1 垃圾回收
谈到Java内存管理的话题,就必然会提到垃圾回收的概念,垃圾回收的英文名称为Garbage Collection,简称GC,它是Java程序设计中有关内存管理的核心概念,Java虚拟机(JVM)的内存管理机制被称为垃圾回收机制。因此,要想掌握在开发Java应用时怎样才能合理地管理内存,首先应该了解Java虚拟机的内存管理机制——垃圾回收机制。否则,在不了解垃圾回收具体实现机制的情况下讨论Java程序设计中的内存管理,优化Java应用性能,就有些纸上谈兵,舍本逐末了。
上面我们提到Java程序设计中的内存管理机制是通过垃圾回收来完成的,那么在JVM运行环境中什么样的对象是垃圾呢?下面我们给出了在JVM运行环境中垃圾对象的定义:
一个对象创建后被放置在JVM的堆内存(heap)中,当永远不再引用这个对象时,它将被JVM在堆内存(heap)中回收。被创建的对象不能再生,同时也没有办法通过程序语句释放它们。
我们也可以这样给JVM中的垃圾对象下定义:
当对象在JVM运行空间中无法通过根集合(root set)到达(找到)时,这个对象就被称为垃圾对象。根集合是由类中的静态引用域与本地引用域组成的。JVM中通过根集合索引对象如图2-1所示。
图2-1 JVM中通过根集合索引对象
&注意 图2-1中打了X标记的对象就是不可到达的对象,这些对象就被JVM视为垃圾对象并被JVM回收。JVM将给这些对象打上相应的标记,然后清扫回收这些对象,并将散碎的内存单元收集整合。
这里提到了堆内存的概念,它是JVM管理的一种内存类型,在做Java应用开发时经常会用到由JVM管理的两种类型的内存:堆内存(heap)与栈内存(stack)。有关堆内存的概念,在前面的相关章节中,已经做过相应的介绍。简单地讲,堆内存主要用来存储程序在运行时创建或实例化的对象与变量,例如:我们通过new MyClass()创建的类MyClass的对象。而栈内存(stack)则是用来存储程序代码中声明为静态(static)(或非静态)的方法,JVM、堆内存(heap)与栈内存(stack)三者的关系如图2-2所示。
图2-2 JVM、堆内存与栈内存的关系
下面通过一个实例来看一下堆内存(heap)与栈内存(stack)中所存储对象的类型有哪些不同。
… …
public class BirdTest {
static Vector birdList = new Vector();
static void makeBird () {
Object bird= new Bird ();
birdList.addElement(bird);
}
public static void main(String[] arg) {
makeBird ();
…
}
}
… …
在上面的代码中声明了一些静态的变量与方法,同时也通过关键字new创建了一些对象实例,下面给出这个简单的类在运行时JVM中堆内存(heap)与栈内存(stack)中所存储的对象情况,如图2-3所示。
图2-3 JVM中堆内存与栈内存中所存储的对象情况
在图2-3中,可以看到我们在类BirdTest中声明的Vector 类的birdList对象,以及在运行时创建的Bird对象都被放在了堆内存(heap)中,而把两个静态方法main()与makeBird()放在了栈内存(stack)中。这说明birdList对象占用了堆内存,静态方法main()与makeBird()则占用了栈内存。在对Java程序设计中内存管理技术做更为深入的讨论之前,有必要再详细地讲一下堆内存(heap)的相关知识。
堆内存
堆内存(heap)在JVM启动的时候就被创建,它是JVM中非常关键的一个内存管理区域。堆内存中所存储的对象可以被JVM自动回收,不能通过其他外部手段回收,也就是说开发人员无法通过添加相关代码的手段,回收位于堆内存中的对象。堆内存(heap)通常情况下被分为两个区域:新对象(new object)区域与老对象(old object)区域。这里又引入了两个有关JVM内存管理的新概念:新对象(new object)区域与老对象(old object)区域。下面分别对这两个概念做一下介绍。
新对象(new object)区域。又可以细分为三个小区域:伊甸园(Eden)区域、From区域与To区域。伊甸园区域用来保存新创建的对象,它就像一个堆栈,新的对象被创建,就像指向该栈的指针(如果你熟悉C语言,应该非常熟悉指针的概念)在不断增长一样,当伊甸园区域中的对象满了之后,JVM系统将要做可到达性测试,主要任务是检测有哪些对象由根集合出发是不可到达的,这些对象就可以被JVM回收,并且将所有的活动对象从伊甸园区域拷到To区域,此时一些对象将发生状态交换,有的对象就从To区域被转移到From区域,此时From区域就有了对象。上面对象迁移的整个过程,都是由JVM控制完成的。当我们在使用一些Java应用服务器软件时,通过其所提供的内存与性能监控界面,会看到这一过程引起的系统内存的变化。在这个过程执行期间,Java虚拟机的性能是非常低下的,这个过程会严重影响正在运行的应用的性能。
老对象(old object)区域。在老对象区域中的对象仍然会有一个较长的生命周期,大多数JVM系统中的垃圾对象,都来源于“短命”对象,经过一段时间后,被转入老对象区域的对象,就变成了垃圾对象。此时,它们都被打上相应的标记,JVM系统将会自动回收这些垃圾对象,建议你不要频繁地强制系统做垃圾回收,这是因为JVM会利用有限的系统资源,优先完成垃圾回收工作,致使应用无法快速地响应来自用户端的请求,这样会影响系统的整体性能,这也正是我们不建议读者自己频繁强制做垃圾回收的原因。
为了使读者能够更清楚地了解垃圾回收的过程,根据上面的讲解,给出了JVM做垃圾回收的过程示意图,如图2-4所示。
图2-4 JVM做垃圾回收的过程示意
通过上面的学习,我们知道垃圾回收与对象的生命周期是紧紧联系在一起的,那么JVM中的对象生命周期是怎样的呢?下面就讲解一下JVM中对象的生命周期的相关知识。
2.2 JVM中对象的生命周期
在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。
2.2.1 创建阶段
在对象创建阶段,系统要通过下面的步骤,完成对象的创建过程:
(1)为对象分配存储空间。
(2)开始构造对象。
(3)递归调用其超类的构造方法。
(4)进行对象实例初始化与变量初始化。
(5)执行构造方法体。
上面的5个步骤中的第3步就是指递归地调用该类所扩展的所有父类的构造方法,一个Java类(除Object类外)至少有一个父类(Object),这个规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。实际上,在Java程序设计中,任何一个Java类都直接或间接的是Object类的子类。例如下面的代码:
public class A {
…
}
这个声明等同于下面的声明:
public class A extends java.lang.Object {
…
}
上面讲解了对象处于创建阶段时,系统所做的一些处理工作,其中有些过程与应用的性能密切相关,因此在创建对象时,我们应该遵循一些基本的规则,以提高应用的性能。
下面是在创建对象时的几个关键应用规则:
(1)避免在循环体中创建对象,即使该对象占用内存空间不大。
(2)尽量及时使对象符合垃圾回收标准。
(3)不要采用过深的继承层次。
(4)访问本地变量优于访问类中的变量。
关于规则(1)避免在循环体中创建对象,即使该对象占用内存空间不大,需要提示一下,这种情况在我们的实际应用中经常遇到,而且我们很容易犯类似的错误,例如下面的代码:
… …
for (int i = 0; i < 10000; ++i) {
Object obj = new Object();
System.out.println("obj= "+ obj);
}
… …
上面代码的书写方式相信对你来说不会陌生,也许在以前的应用开发中你也这样做过,尤其是在枚举一个Vector对象中的对象元素的操作中经常会这样书写,但这却违反了上述规则(1),因为这样会浪费较大的内存空间,正确的方法如下所示:
… …
Object obj = null;
for (int i = 0; i < 10000; ++i) {
obj = new Object();
System.out.println("obj= "+ obj);
}
… …
采用上面的第二种编写方式,仅在内存中保存一份对该对象的引用,而不像上面的第一种编写方式中代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。因此在循环体中声明创建对象的编写方式应该尽量避免。
另外,不要对一个对象进行多次初始化,这同样会带来较大的内存开销,降低系统性能,如:
public class A {
private Hashtable table = new Hashtable ();
public A() {
// 将Hashtable对象table初始化了两次
table = new Hashtable();
}
}
正确的方式为:
public class B {
private Hashtable table = new Hashtable ();
public B() {
}
}
不要小看这个差别,它却使应用软件的性能相差甚远,如图2-5所示。
图2-5 初始化对象多次所带来的性能差别
看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则我们开发出来的应用也是低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。因此,我们在日常的应用开发中,应该认真对待每一行代码,采用最优化的编写方式,不要忽视细节,不要忽视潜在的问题。
2.2.2 应用阶段
当对象的创建阶段结束之后,该对象通常就会进入对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身“存在价值”的时期。在对象的应用阶段,对象具备下列特征:
— 系统至少维护着对象的一个强引用(Strong Reference);
— 所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。
上面提到了几种不同的引用类型。可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。在讲解这几种不同类型的引用之前,我们必须先了解一下Java中对象引用的结构层次。
Java对象引用的结构层次示意如图2-6所示。
图2-6 对象引用的结构层次示意
由图2-6我们不难看出,上面所提到的几种引用的层次关系,其中强引用处于顶端,而虚引用则处于底端。下面分别予以介绍。
1.强引用
强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。
2.软引用
软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。再者,软可到达对象的所有软引用都要保证在虚拟机抛出OutOfMemoryError之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。下面是软引用的实现代码:
… …
import java.lang.ref.SoftReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,将它设置为soft 引用类型,并且释放强引用;
SoftReference sr = new SoftReference(a);
a = null;
…
// 下次使用时
if (sr!=null) {
a = sr.get();
}
else{
// GC由于内存资源不足,可能系统已回收了a的软引用,
// 因此需要重新装载。
a = new A();
sr=new SoftReference(a);
}
… …
软引用技术的引进,使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃(crash)。因此在处理一些占用内存较大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建(这里是指那些没有保留运行过程中状态的对象),提高应用对内存的使用效率,提高系统稳定性。但事物总是带有两面性的,有利亦有弊。在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。
3.弱引用
弱引用(Weak Reference)对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。虽然,GC在运行时一定回收Weak引用对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map数据结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null时,对这个对象引用就不存在了,GC能够快速地回收该对象空间。与软引用类似我们也可以给出相应的应用代码:
… …
import java.lang.ref.WeakReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,将它设置为weak 引用类型,并且释放强引用;
WeakReference wr = new WeakReference (a);
a = null;
…
// 下次使用时
if (wr!=null) {
a = wr.get();
}
else{
a = new A();
wr = new WeakReference (a);
}
… …
弱引用技术主要适用于实现无法防止其键(或值)被回收的规范化映射。另外,弱引用分为“短弱引用(Short Week Reference)”和“长弱引用(Long Week Reference)”,其区别是长弱引用在对象的Finalize方法被GC调用后依然追踪对象。基于安全考虑,不推荐使用长弱引用。因此建议使用下面的方式创建对象的弱引用。
… …
WeakReference wr = new WeakReference(obj);
或
WeakReference wr = new WeakReference(obj, false);
… …
4.虚引用
虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。虚引用主要适用于以某种比 Java 终结机制更灵活的方式调度 pre-mortem 清除操作。
&注意 在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
2.2.3 不可视阶段
在一个对象经历了应用阶段之后,那么该对象便处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。
… …
public void process () {
try {
Object obj = new Object();
obj.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
while (isLoop) { // ... loops forever
// 这个区域对于obj对象来说已经是不可视的了
// 因此下面的代码在编译时会引发错误
obj.doSomething();
}
}
… …
如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行obj.doSomething();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。
2.2.4 不可到达阶段
处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。
2.2.5 可收集阶段、终结阶段与释放阶段
对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:
(1)垃圾回收器发现该对象已经不可到达。
(2)finalize方法已经被执行。
(3)对象空间已被重用。
当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。
2.3 Java中的析构方法finalize
在C++程序设计中有构造函数与析构函数的概念,并且是内存管理技术中相当重要的一部分,而在Java语言中只有构造器(也可以称为构造函数)的概念,却没有析构器或析构函数的概念。这是因为,理论上JVM负责对象的析构(销毁与回收)工作。也就是上面讲到的垃圾回收的概念。那么Java语言中是否真的不存在与C++中析构函数职能类似的方法?其实Java语言中的finalize 方法与C++语言中的析构函数的职能就极为类似。finalize方法是Java语言根基类Object类中所包含的一个方法,这个方法是保护类型的方法(protected),由于在Java应用中开发的所有类都为Object的子类,因此用户类都从Object对象中隐式地继承了该方法。因此,我们在Java类中可以调用其父类的finalize方法,并且可以覆盖自身继承来的finalize方法。虽然我们可以在一个Java类中调用其父类的finalize方法,但是由于finalize方法没有自动实现递归调用,我们必须手动实现,因此finalize函数的最后一个语句通常是super.finalize()语句。通过这种方式,我们可以实现从下到上finalize的迭代调用,即先释放用户类自身的资源,然后再释放父类的资源。通常我们可以在finalize方法中释放一些不容易控制,并且非常重要的资源,例如:一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。
finalize方法最终是由JVM中的垃圾回收器调用的,由于垃圾回收器调用finalize的时间是不确定或者不及时的,调用时机对我们来说是不可控的,因此,有时我们需要通过其他的手段来释放程序中所占用的系统资源,比如自己在类中声明一个destroy()方法,在这个方法中添加释放系统资源的处理代码,当你使用完该对象后可以通过调用这个destroy()方法来释放该对象内部成员占用的系统资源。虽然我们可以通过调用自己定义的destroy()方法释放系统资源,但是还是建议你最好将对destroy()方法的调用放入当前类的finalize()方法体中,因为这样做更保险,更安全。在类深度继承的情况下,这种方法就显得更为有效了,我们可以通过递归调用destroy的方法在子类被销毁的时候释放父类所占用的资源,例如下面的代码:
1.原始基类A
public class A {
Object a = null;
public A() {
a = new Object();
System.out.println("创建a对象");
}
protected void destroy() {
System.out.println("释放a对象");
a = null;
// 释放自身所占用的资源
…
}
protected void finalize() throws java.lang.Throwable {
destroy();
// 递归调用超类中的finalize方法
super.finalize();
}
}
2.一级子类B
public class B extends A {
Object b = null;
public B() {
b = new Object();
System.out.println("创建b对象");
}
protected void destroy() {
b = null;
// 释放自身所占用的资源
System.out.println("释放b对象");
super.destroy();
}
protected void finalize() throws java.lang.Throwable {
destroy();
// 递归调用超类中的finalize方法
super.finalize();
}
}
3.二级子类C
public class C extends B {
Object c = null;
public C() {
c = new Object();
System.out.println("创建c对象");
}
protected void destroy() {
c = null;
// 释放自身所占用的资源
System.out.println("释放c对象");
super.destroy();
}
protected void finalize()throws java.lang.Throwable {
destroy();
// 递归调用超类中的finalize方法
super.finalize();
}
}
上面的三个类的继承关系是非常明晰的:A->B->C,类A是原始基类(这是一种习惯叫法),类B继承了类A,类C又继承了类B。其实类A并不是真正意义上的原始基类,上面我们已经提到过Java语言中的原始基类是Object类,尽管我们并没有显式的声明,但这已经是系统约定俗成的了。
为了简单清楚地说明问题,我们在这三个类中分别声明了3个方法,用来论证上面所讲解的知识点,在类A的构造器中我们初始化了一个对象a,在destroy方法中通过a = null;释放其自身所占用的资源。并且在finalize方法中,我们调用了destroy方法用来释放其自身所占用的资源,然后调用其超类Object的finalize方法,这是我们以上所提到的“双保险”的内存释放方法;类B与类C的结构与类A极为相似,它们除了释放自身所占用的资源外,它们还在其对应的方法中调用其超类的destroy方法与finalize方法,用来释放超类所占用的资源。如在类B中调用其超类A的destroy方法与finalize方法与在类C中调用其超类B的destroy方法与finalize方法。但是类A与类B、类C有一点不同,那就是在其destroy方法中没有super.destroy()语句,这是因为其超类Object并没有destroy方法。下面看一下当我们调用初始化与销毁类C时,会有什么样的情况发生。以下是调用完成这个过程的测试类Test的源代码:
public class Test {
c = null;
public Test () {
c = new C();
}
public static void main(String args[]) {
MyClass me = new MyClass();
me.destroy();
}
protected void destroy () {
if (c != null) {
c.destroy();
}else {
System.out.println("c对象已被释放");
}
}
}
编译执行Test.java:
> javac Test.java
> java Test
下面是这个程序的运行结果:
创建a对象
创建b对象
创建c对象
释放c对象
释放b对象
释放a对象
我们注意到当在Test类中初始化类C的对象时,其构造器产生了递归调用,并且是由基类开始依次调用、初始化成员对象的,而当调用C类对象的destroy方法时系统同样产生了递归调用,但调用的顺序却与初始化调用的顺序完全相反,释放资源的调用顺序是由子类开始的,依次调用其超类的资源释放方法destroy()。由此可见,我们在设计类时应尽可能地避免在类的默认构造器中创建、初始化大量的对象。一个原因是在实例化自身的情况下,造成较大的资源开销;另一个原因是其子类在被实例化时,也同样会带来较大的系统资源开销。因为即使我们没有想调用父类的构造器创建大量无用的对象(至少有时候这些对象对我们是没有意义的),但是系统会自动创建它们,而这些操作与过程对于我们来说是隐含的。为了防止上述情况的发生,造成不必要的内存资源浪费,我们应当尽量不在类的构造器中创建、初始化大量的对象或执行某种复杂、耗时的运算逻辑。
2.4 数组的创建
数组空间的申请也是一个与内存管理关系密切的技术话题。数组空间的申请分为显式申请与隐式申请两种。显式申请是指在程序中直接给出数组的类型与长度,例如下面的代码:
… …
int[] intArray = new int[1024];
… …
上面的这行代码的意义是,显式地向系统一次性申请了大小为1KB的整数类型的内存空间,这样的声明方式一般出现在对文件或网络资源数据读取的处理代码中,往往用数组来作为数据读取的缓冲区,以提高读取效率。由于我们不知道具体读取的内容的长度,因此,我们只能通过这种方式来读取相关资源,这样做显然有些弊端。例如,文件、网络资源的长度小于你所申请的数组的长度,这就造成了系统内存资源浪费。隐式申请是在声明数组对象时不知道将要得到的数组的具体长度,例如下面的代码:
… …
int[] intArray = obj.getIntArray();
System.out.println("整型数组长度:"+intArray.length());
… …
在这行代码中我们事先并不知道obj.getIntArray()到底返回的数组长度是多少,这是在程序运行时才能确定的,因此,这里不存在上面的显式申请数组浪费内存的问题,因为数组的长度是由系统决定的,因此,这种方法是值得提倡使用的。但是这种隐式申请的方法只适用于接收某个方法返回值为数组的情况。
如果遇到数组中所保存的元素占用内存空间较大或数组本身长度较大的情况,我们可以采用上面所讲到的软引用的技术来引用数组,以“提醒”JVM及时回收垃圾内存,维护系统的稳定性。例如下面的代码:
… …
Object obj = new char[1000000];
SoftReference ref = new SoftReference(obj);
… …
由于数组对象长度较长,占用了较大的内存空间,因此我们对obj采用了软引用的处理方式,由JVM根据运行时的内存资源的使用情况,来把握是否回收该对象,释放该内存。虽然这会对应用程序产生一些影响(如当我们想使用该数组对象的时候,该对象被回收了)。但是这样做却能保证应用整体的稳健性,达到合理使用系统内存的目的。
2.5 共享静态变量存储空间
我们知道类中的静态变量(Static Variable)在程序运行期间,其内存空间对所有该类的对象实例而言是共享的,因此在某些时候为了节省系统内存开销、共享资源,将类中的一些变量声明为静态变量,通过下面的例子,你可以发现合理应用静态变量带来的好处:
public class WeekA{
static class Data {
private int week;
private String name;
Data(int i, String s) {
month = i;
name = s;
}
}
Data weeks[] = {
new Data(1, "Monday"),
new Data(2, "Tuesay"),
new Data(3, "Wednesday"),
new Data(4, "Thursday"),
new Data(5, "Friday"),
new Data(6, "Saturday")
new Data(7, "Sunday")
};
public static void main(String args[]) {
final int N = 200000;
WeekA weekinstance;
for (int i = 1; i <= N; i++){
weekinstance = new WeekA ();
}
}
}
在上面这段代码中,没有将Data weeks声明为静态变量,因此当创建WeekA对象时将会得到200 000个weeks对象的副本,这些对象被保存在内存中,但是weeks对象中的数据却从来没有被更改过,而且十分稳定。因此,如果能使所有对象共享该数据对象是个不错的解决办法,请看下面的代码:
public class WeekB{
static class Data {
private int week;
private String name;
Data(int i, String s) {
month = i;
name = s;
}
}
static Data weeks[] = {
new Data(1, "Monday"),
new Data(2, "Tuesay"),
new Data(3, "Wednesday"),
new Data(4, "Thursday"),
new Data(5, "Friday"),
new Data(6, "Saturday")
new Data(7, "Sunday")
};
public static void main(String args[]) {
final int N = 200000;
WeekB weekinstance;
for (int i = 1; i <= N; i++){
weekinstance = new WeekB ();
}
}
}
请注意在类WeekB中,在Data weeks[]之前添加了static关键字,将该对象变量声明为静态的,因此当你创建200 000个WeekB对象时系统中只保存着该对象的一份拷贝,而且该类的所有对象实例共享这份拷贝,这无疑节约了大量的不必要的内存开销,同时实现了要完成的系统功能。
那么是不是我们应该尽量地多使用静态变量呢?其实不是这样的,因为静态变量生命周期较长,而且不易被系统回收,因此如果不能合理地使用静态变量,就会适得其反,造成大量的内存浪费,所谓过犹不及。因此,建议在具备下列全部条件的情况下,尽量使用静态变量:
(1)变量所包含的对象体积较大,占用内存较多。
(2)变量所包含的对象生命周期较长。
(3)变量所包含的对象数据稳定。
(4)该类的对象实例有对该变量所包含的对象的共享需求。
如果变量不具备上述特点建议你不要轻易地使用静态变量,以免弄巧成拙。
2.8 不要提前创建对象
为了节省系统内存资源,不提前申请并不急需的内存空间。我们应当尽量在需要的时候创建对象。重复地分配、构造对象可能会因垃圾回收(GC)做额外的工作,降低系统性能,例如下面的代码:
… …
void f() {
int i;
A a = new A();
// 类A 的对象a被创建
// 在判断语句之外没有
// 应用过a对象
...
if (...) {
// 类A 的对象a仅在此处被应用
a.showMessage();
...
}
...
}
… …
正确的书写方式为:
void f() {
int i;
...
if (...) {
A a = new A();
// 类A的对象a被创建
// 在判断语句中
// 使用了a对象
a.showMessage();
}
...
}
上面的代码是在使用a对象的时候才去初始化了a,而不是提前初始化。这样的代码更健壮、高效。
2.9 JVM内存参数调优
我们前面所提到的堆内存(heap)是由Java虚拟机控制管理的,因此,这些参数对JVM而言都有一个默认值,但在某些情况下这些参数的默认值并不是最优的,这就需要我们通过调整这些参数的值来提高JVM的性能,最终提高应用的性能指标。
在实际的应用开发中,如果应用所使用的系统内存较大,经常会引发内存溢出的错误:
…
java.lang.OutOfMemoryError <<no stack trace available>>
java.lang.OutOfMemoryError <<no stack trace available>>
Exception in thread "main"
…
这可能是因为应用要使用的堆内存(heap)超过了JVM所管理内存范围,如果我们适当追加内存值有时就可以避免这种致命错误的出现。
在WINDOWS系统上你可以通过参数-verbosegc查看JVM回收内存的信息,在HP UNIX系统上你可以通过-Xverbosegc:file=/tmp/gc$$.out参数将信息重定向到一个文件中。然后查看相应的信息,例如下面的这个类。
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
}
System.out.println("this is a GC test");
}
}
在类A的main方法中创建了100 000个A对象,然后我们看一下JVM回收内存的情况,编译并执行这个类:
>java -verbosegc A
[GC 512K->91K(1984K), 0.0027537 secs]
this is a GC test
从输出信息中可以看出总共有1984KB的内存被回收,耗时0.002 753 7秒。现在我们将类A添加一行清除对象引用的代码:
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
a = null;
}
System.out.println("this is a GC test");
}
}
编译并执行这个类:
>java -verbosegc A
[GC 512K->91K(1 984K), 0.0 027 450 secs]
this is a GC test
我们看到被回收内存的数量并没有变化,但是回收所需要的时间却变成了0.002 745 0秒,后者比前者节省了0.000 008 7秒,千万不要小看这0.000 008 7秒,当你的应用足够复杂时这个时间就会成指数级增长,看来我们主动清除对象引用的方法,确实可以加速JVM对垃圾内存的回收。
如果再在类A中加入一行强制系统内存回收的代码,结果又会怎样呢?如下所示:
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
a = null;
}
System.gc();
System.out.println("this is a GC test");
}
}
编译并执行这个类:
>java -verbosegc A
[GC 512K->91K(1984K), 0.0 027 272 secs]
[Full GC 487K->91K(1984K), 0.0 070 730 secs]
this is a GC test
系统这次做了两次内存回收,第一次是程序中强制系统内存回收的代码System.gc()导致的内存回收,而后者是系统最终的内存回收操作,我们看到强制内存回收耗时不长,可是却导致了系统最终垃圾回收的时间加长了很多,因此我们在采用强制系统垃圾回收(通过显式调用方法System.gc())的办法来回收系统垃圾内存的办法,还是存在一些弊端的,应尽量少用,或者说只在必要的时候应用。
上面我们提到的内存回收操作就是回收JVM所管理的堆内存(heap)。当系统连续申请内存并且超过JVM所管理的堆内存(heap)的最大值时,就会产生系统内存溢出的致命异常,下面我们来看一下怎样通过设置JVM的内存参数来优化JVM对内存的管理,避免内存溢出异常的发生。表2-1所示的就是与JVM内存相关的参数及其说明。
表2-1 与JVM内存相关的参数及其说明
JVM堆内存(heap)设置选项 参数格式 说 明
设置新对象生产堆内存(Setting the Newgeneration heap size) -XX:NewSize 通过这个选项可以设置Java新对象生产堆内存。在通常情况下这个选项的数值为1 024的整数倍并且大于1MB。这个值的取值规则为,一般情况下这个值-XX:NewSize是最大堆内存(maximum heap size)的四分之一。增加这个选项值的大小是为了增大较大数量的短生命周期对象
增加Java新对象生产堆内存相当于增加了处理器的数目。并且可以并行地分配内存,但是请注意内存的垃圾回收却是不可以并行处理的
续表
JVM堆内存(heap)设置选项 参数格式 说 明
设置最大新对象生产堆内存(Setting the maximum New generation heap size) -XX:MaxNewSize 通过这个选项可以设置最大Java新对象生产堆内存。通常情况下这个选项的数值为1 024的整数倍并且大于1MB
其功用与上面的设置新对象生产堆内存-XX:NewSize相同
设置新对象生产堆内存的比例(Setting New heap size ratios) -XX:SurvivorRatio 新对象生产区域通常情况下被分为3个子区域:伊甸园,与两个残存对象空间,这两个空间的大小是相同的。通过用-XX:SurvivorRatio=X选项配置伊甸园与残存对象空间(Eden/survivor)的大小的比例。你可以试着将这个值设置为8,然后监控、观察垃圾回收的工作情况
设置堆内存池的最小值
(Setting minimum heap size) -Xms 通过这个选项可以要求系统为堆内存池分配内存空间的最小值。通常情况下这个选项的数值为1 024的整数倍并且大于1MB。这个值的取值规则为,一般情况下这个值(-Xms)与最大堆内存相同,以降低垃圾回收的频度
设置堆内存池的最大值(Setting maximum heap size) -Xmx 通过这个选项可以要求系统为堆内存池分配内存空间的最大值。通常情况下这个选项的数值为1 024的整数倍并且大于1 MB
一般情况下这个值(-Xmx)与最小堆内存(minimum heap size –Xms)相同,以降低垃圾回收的频度
取消垃圾回收 -Xnoclassgc 这个选项用来取消系统对特定类的垃圾回收。它可以防止当这个类的所有引用丢失之后,这个类仍被引用时不会再一次被重新装载,因此这个选项将增大系统堆内存的空间
设置栈内存的大小 -Xss 这个选项用来控制本地线程栈的大小,当这个选项被设置的较大(>2MB)时将会在很大程度上降低系统的性能。因此在设置这个值时应该格外小心,调整后要注意观察系统的性能,不断调整以期达到最优
根据表2-1中所描述的参数意义,我们可以在启动应用时为JVM设置相应的参数值以提高系统的性能,例如下面的例子:
java -XX:NewSize=128m -XX:MaxNewSize=128m -XX:SurvivorRatio=8 -Xms512m
-Xmx512m MyApplication
类文件(.class)的大小
由Java源文件.java文件编译成JVM可解释执行的Java字节文件.class。因所采用的编译方式的不同而大小也不同。通常.class文件的大小也存在是否占用较大内存的问题。通过降低.class文件的大小,不但可以降低系统内存的开销,还可以节省网络开销,虽然这部分内容与JVM内存管理联系不大,但是我觉得还是有必要提一下,因为这在你开发Applet应用时会有帮助(注:在本书后续的章节中,将会对如何减小Java类尺寸的技术话题做更为深入的探讨)。因为一般来说,Applet应用都是靠网络分布式传输由客户端浏览器装载运行的,如果类文件较大,无疑将会增大网络开销,降低传输速度无法满足用户的需求,并且如果类文件较大,无疑也会消耗客户端内存资源。我们可以通过在Java编译器javac中添加相应的参数,来缩小类文件的大小,解决上面的问题。
通常有三种编译方式会影响类文件的大小。
(1)默认编译方式: javac A.java。
(2)调试编译方式: javac –g A.java。
(3)代码编译方式: javac –g:none A.java。
例如如下所示的简单的类A:
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
}
}
}
通过上面这三种方式编译后的类文件的大小分别为:
默认编译方式:291字节。
调试编译方式:422字节。
代码编译方式:207字节。
采用三种不同的方式,编译产生的类文件的大小差异非常大,这是什么原因导致的呢?原来在于.class文件中包含多个不同的部分或属性。
代码(Code)属性包含实际的方法字节码。源文件信息(SourceFile Information)包含用于生成.class的源文件名称。代码行序号表(LineNumberTable)用来映射源文件中的代码行序号与字节码文件中的序号偏移。本地变量表(LocalVariableTable)用来映射本地变量与栈桢的偏移。
&注意 如果你想了解字节码文件.class的文件结构详细信息,请参考相关的技术资料,这里就不详细讲解了。
正是由于上面这三种编译方式生成的类文件所包含的信息不同,才导致了类文件的大小差异较大,其包含的信息分别如下所示。
默认编译方式:代码(Code)、源文件信息(SourceFile Information)、代码行序号表(LineNumberTable)。
调试编译方式:代码(Code)、源文件信息(SourceFile Information)、代码行序号表(LineNumberTable)、本地变量表(LocalVariableTable)。
代码编译方式:代码(Code)。
这就是三种编译方式产生类文件大小不同的根本原因。而这三种编译方式在程序开发的不同阶段却都起着非常重要的作用,例如,调试编译方式在程序的调试开发过程中应采用,以获取更为详细的调试信息。因此具体应用上面的三种编译方式中的哪一种,应该适时而定。
2.10 Java程序设计中有关内存管理的其他经验
根据上面讲解的JVM内存管理系统的工作原理,我们可以通过一些技巧和方式,让JVM做GC处理时更加有效率,更加符合应用程序的要求。以下就是程序设计的一些经验。
(1)最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null。我们在使用这种方式时,必须特别注意一些复杂的对象图,例如数组、队列、树、图等,这些对象之间的相互引用关系较为复杂。对于这类对象,GC回收它们的效率一般较低。如果程序允许,尽早将不用的引用对象赋为null。这样可以加速GC的工作。 例如:
… …
A a = new A();
// 应用a对象
a = null; // 当使用对象a之后主动将其设置为空
… …
但要注意,如果a是方法的返回值,千万不要做这样的处理,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现。因此这时很难及时抓住、排除NullPointerException异常。
(2)尽量少用finalize函数。finalize函数是Java给程序员提供一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。
(3)如果需要使用经常用到的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。
(4)注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象,造成内存浪费。
(5)尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其自类的构造器时造成不必要的内存资源浪费。
(6)尽量避免强制系统做垃圾内存的回收(通过显式调用方法System.gc()),增长系统做垃圾回收的最终时间,降低系统性能。
(7)尽量避免显式申请数组空间,当不得不显式地申请数组空间时尽量准确地估计出其合理值,以免造成不必要的系统内存开销。
(8)尽量在做远程方法调用(RMI)类应用开发时使用瞬间值(transient)变量,除非远程调用端需要获取该瞬间值(transient)变量的值。
(9)尽量在合适的场景下使用对象池技术以提高系统性能,缩减系统内存开销,但是要注意对象池的尺寸不易过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行环境所提供内存资源的数量。
在使用设计模式(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();}
面的思路对于数组处理同样很有用。
2. 使用非阻塞I/O
版本较低的JDK不支持非阻塞I/O API。为避免I/O阻塞,一些应用采用了创建大量线程的办法(在较好的情况下,会使用一个缓冲池)。这种技术可以在许多必须支持并发I/O流的应用中见到,如Web服务器、报价和拍卖应用等。然而,创建Java线程需要相当可观的开销。
3. 慎用异常
异常对性能不利。抛出异常首先要创建一个新的对象。Throwable接口的构造函数调用名为fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,VM就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
4. 不要重复初始化变量
默认情况下,调用类的构造函数时, Java会把变量初始化成确定的值:所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键词创建一个对象时,构造函数链中的所有构造函数都会被自动调用。
5. 尽量指定类的final修饰符
带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String。为String类指定final防止了人们覆盖length()方法。另外,如果指定一个类为final,则该类所有的方法都是final。Java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。
6. 尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。另外,依赖于具体的编译器/JVM,局部变量还可能得到进一步优化。
7. 乘法和除法
考虑下面的代码:
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。值得一提的是,虽然移位操作速度快,但可能使代码比较难于理解,所以最好加上一些注释。
3. 选择合适的引用机制
在典型的JSP应用系统中,页头、页脚部分往往被抽取出来,然后根据需要引入页头、页脚。当前,在JSP页面中引入外部资源的方法主要有两种:include指令,以及include动作。
include指令:例如
<%@ include file="copyright.html" %>
该指令在编译时引入指定的资源。在编译之前,带有include指令的页面和指定的资源被合并成一个文件。被引用的外部资源在编译时就确定,比运行时才确定资源更高效。
include动作:例如
<jsp:include page="copyright.jsp" />
该动作引入指定页面执行后生成的结果。由于它在运行时完成,因此对输出结果的控制更加灵活。但时,只有当被引用的内容频繁地改变时,或者在对主页面的请求没有出现之前,被引用的页面无法确定时,使用include动作才合算。
4. 在spring中对orm层的动作设置只读属性
将 (只对数据库进行读取的操作) 设置只读属性
10. Servlet与内存使用
许多开发者随意地把大量信息保存到用户会话之中。一些时候,保存在会话中的对象没有及时地被垃圾回收机制回收。从性能上看,典型的症状是用户感到系统周期性地变慢,却又不能把原因归于任何一个具体的组件。如果监视JVM的堆空间,它的表现是内存占用不正常地大起大落。解决这类内存问题主要有二种办法。第一种办法是,在所有作用范围为会话的Bean中实现HttpSessionBindingListener接口。这样,只要实现valueUnbound()方法,就可以显式地释放Bean使用的资源。
另外一种办法就是尽快地把会话作废。大多数应用服务器都有设置会话作废间隔时间的选项。另外,也可以用编程的方式调用会话的setMaxInactiveInterval()方法,该方法用来设定在作废会话之前,Servlet容器允许的客户请求的最大间隔时间,以秒计算。
11. HTTP Keep-Alive
Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。市场上的大部分Web服务器,包括iPlanet、IIS和Apache,都支持HTTP Keep-Alive。对于提供静态内容的网站来说,这个功能通常很有用。但是,对于负担较重的网站来说,这里存在另外一个问题:虽然为客户保留打开的连接有一定的好处,但它同样影响了性能,因为在处理暂停期间,本来可以释放的资源仍旧被占用。当Web服务器和应用服务器在同一台机器上运行时,Keep-Alive功能对资源利用的影响尤其突出。
12. JDBC与Unicode
想必你已经了解一些使用JDBC时提高性能的措施,比如利用连接池、正确地选择存储过程和直接执行的SQL、从结果集删除多余的列、预先编译SQL语句,等等。除了这些显而易见的选择之外,另一个提高性能的好选择可能就是把所有的字符数据都保存为Unicode(代码页13488)。Java以Unicode形式处理所有数据,因此,数据库驱动程序不必再执行转换过程。但应该记住:如果采用这种方式,数据库会变得更大,因为每个Unicode字符需要2个字节存储空间。另外,如果有其他非Unicode的程序访问数据库,性能问题仍旧会出现,因为这时数据库驱动程序仍旧必须执行转换过程。
13. JDBC与I/O
如果应用程序需要访问一个规模很大的数据集,则应当考虑使用块提取方式。默认情况下,JDBC每次提取32行数据。举例来说,假设我们要遍历一个5000行的记录集,JDBC必须调用数据库157次才能提取到全部数据。如果把块大小改成512,则调用数据库的次数将减少到10次。在一些情形下这种技术无效。例如,如果使用可滚动的记录集,或者在查询中指定了FOR UPDATE,则块操作方式不再有效。
Java代码优化--尽可能地使用stack(栈)变量(方法内部的局部变量)
Java程序包含了大量的对象,我们需要了解它们是从哪里被访问的,变量存储于何处对程序的性能有显著的影响--尤其是某些需要被频繁访问的变量。
我们写一个Java类,在其内部方法中定义的局部变量或对象是存储在stack(堆栈)中的,且JVM是一种stack-based的,因此访问和操纵stack中的数据时性能最佳。而Java类的instance变量(这个类的field)和static变量是在constant pool(常量池)中存储和得到访问的。constant pool中保存了所有的符号引用(symbolic references),指向所有型别(types)、值域(field),以及每个型别所使用的所有函数(mothods)。访问instance和static变量时,由于它们存放于constant pool中,所以JVM需要使用更多更耗时的操作码(分析程序生成的bytecode可以看出来)来访问它们。
下面给出一段代码示例,对比后说明怎么尽可能地使用stack变量:
package test;
public class StackVars {
private int x; // instance变量
private static int staticX; //static 变量
public void stackAccess(int val) { //访问和操作stack变量j
int j = 0;
for (int i = 0; i < val; i++) {
j += 1;
}
}
public void instanceAccess(int val) {//访问和操作instance变量x
for (int i = 0; i < val; i++) {
x += 1;
}
}
public void staticAccess(int val) {//访问和操作static变量staticX
for (int i = 0; i < val; i++) {
staticX += 1;
}
}
}
经测试,发现运行instanceAccess()和staticAccess()方法的时间大约相同,但却比运行stackAccess()方法慢了2~3倍。因此我们对instanceAccess()、staticAccess()两个方法的代码作以下调整,以得到更快的性能:
public void instanceAccess(int val) {//访问和操作instance变量x
int tempX=x;
for (int i = 0; i < val; i++) {
tempX += 1;
}
x=tempX;
}
public void staticAccess(int val) {//访问和操作static变量staticX
int tempStaticX=staticX;
for (int i = 0; i < val; i++) {
tempStaticX += 1;
}
staticX=tempStaticX;
}
改善之处就是将instance和static变量放到循环之外,而用一个stack变量来完成多次局部运算,最后再将这个stack变量的值传回instance或static变量,从而提高了代码的性能。
Sun JDK自带JVM内存使用分析工具HProf
使用Sun JDK自带JVM内存使用分析工具HProf可以分析JVM堆栈,从而找到占用内存较大的对象。这对应经常出现内存泄漏(OOM)的JAVA系统进行调优很有帮助。
HProf使用方法
• 在WeblogicServer启动脚本中增加-Xrunhprof:heap=sites,重新启动WeblogicServer。
• 使用kill -3 <pid> 或退出WeblogicServer均会生成java.hprof.txt文件,直接打开此文件便可分析JVM的具体运行情况。
从java.hprof.txt记录的JVM堆栈结果中可以发现JVM占用内存较大的对象:
percent live alloc'ed stack class
rank self accum bytes objs bytes objs trace name
1 4.57% 4.57% 2289696 47702 8392224 174838 4251 [C
2 3.99% 8.57% 2000016 1 2000016 1 12308 [C
3 3.65% 12.22% 1827552 9622 1852672 10082 43265 [C
4 2.58% 14.80% 1293912 53913 3929424 163726 4258 java.lang.String
5 2.05% 16.85% 1028664 7585 3207272 24923 4252 [C
6 2.03% 18.88% 1015816 159 1015816 159 18694 [B
7 1.88% 20.77% 942080 230 2740224 669 20416 [B
8 1.61% 22.37% 805752 2142 2150856 4635 45318 [B
9 1.60% 23.98% 802880 772 802880 772 24710 weblogic.servlet.utils.URLMatchMap$URLMatchNode
10 1.60% 25.57% 799400 19985 2781400 69535 45073 cnc.util.Field
11 1.36% 26.93% 679360 3805 679360 3805 494 [B
12 1.35% 28.28% 674856 28119 5181240 215885 2985 java.util.HashMap$Entry
……
……
96 0.19% 63.73% 94776 3112 94776 3112 9146 [C
97 0.19% 63.92% 93456 3894 123936 5164 23631 java.lang.String
98 0.19% 64.10% 93224 3884 123968 5165 23644 java.lang.String
99 0.19% 64.29% 93192 3883 123936 5164 23636 java.lang.String
100 0.18% 64.47% 89528 238 240264 520 33227 [B
101 0.17% 64.64% 86448 1901 103472 2255 18715 java.lang.Object
102 0.17% 64.81% 85464 676 85768 695 18715 [S
103 0.17% 64.98% 85184 1331 85184 1331 28266 weblogic.ejb20.internal.MethodDescriptor
104 0.17% 65.15% 84224 752 84224 752 24148 weblogic.servlet.internal.dd.ServletDescriptor
105 0.17% 65.32% 84136 528 50471136 348769 63 [C
106 0.16% 65.48% 79968 1428 388976 6946 5503 java.lang.reflect.Method
107 0.15% 65.63% 77520 1615 77520 1615 27967 weblogic.ejb20.deployer.mbimpl.MethodInfoImpl
108 0.15% 65.79% 77056 4816 469808 29363 20250 java.lang.Object
109 0.15% 65.94% 76960 74 76960 74 23695 [B
110 0.15% 66.09% 76104 3171 215040 8960 45071 cnc.util.FyCol
111 0.15% 66.24% 74688 3112 74688 3112 9152 java.util.Hashtable$Entry
112 0.15% 66.39% 74688 3112 74688 3112 9147 java.lang.String
113 0.15% 66.54% 74280 61 794328 788 45313 [C
114 0.14% 66.68% 72480 1510 436032 9084 45353 [C
115 0.14% 66.82% 70720 68 70720 68 25869 [B
116 0.14% 66.97% 70720 68 70720 68 27448 [B
117 0.14% 67.11% 70272 1279 142672 2439 5503 [C
118 0.14% 67.24% 69256 86 69256 86 6584 [S
119 0.13% 67.38% 67056 66 67056 66 28882 java.lang.Object
120 0.13% 67.51% 66176 752 66176 752 24170 weblogic.servlet.internal.dd.UIDescriptor
121 0.13% 67.64% 65688 715 65688 715 25389 [C
122 0.13% 67.77% 65600 4 885600 54 23939 [C
123 0.13% 67.90% 65600 4 623200 38 40639 [C
124 0.13% 68.03% 65576 367 65576 367 51686 [C
125 0.13% 68.17% 65568 2 65568 2 30610 java.util.HashMap$Entry
126 0.13% 68.30% 65568 2 130816 16 43271 java.util.HashMap$Entry
127 0.13% 68.43% 65552 1 65552 1 16617 [B
128 0.13% 68.56% 64600 1615 64600 1615 27969 java.util.HashMap
129 0.13% 68.68% 63888 2662 64032 2668 16951 java.util.HashMap$Entry
130 0.13% 68.81% 63888 2662 64032 2668 16997 java.util.HashMap$Entry
131 0.13% 68.94% 63888 2662 64032 2668 16996 weblogic.rmi.internal.ClientMethodDescriptor
132 0.13% 69.07% 63888 2662 99120 4130 16949 java.lang.String
133 0.13% 69.19% 63888 2662 64032 2668 16976 java.lang.String
134 0.13% 69.32% 63232 152 63232 152 9655 weblogic.utils.collections.ConcurrentHashMap$Entry
135 0.13% 69.45% 63232 152 63232 152 9704 weblogic.utils.collections.ConcurrentHashMap$Entry
136 0.12% 69.57% 62168 3885 82632 5164 23628 [B
137 0.12% 69.69% 61680 406 66904 468 1 [C
138 0.12% 69.82% 61504 4 246016 16 47372 [B
139 0.12% 69.94% 61144 36 91019160 23904 92 [B
140 0.12% 70.06% 61040 763 61040 763 24194 weblogic.servlet.internal.dd.ServletMappingDescriptor
141 0.12% 70.18% 60400 1510 363360 9084 45338 java.util.Hashtable
142 0.12% 70.30% 59544 827 59544 827 24746 weblogic.servlet.internal.ServletRuntimeMBeanImpl
143 0.12% 70.42% 59248 1058 484984 8664 33236 oracle.jdbc.ttc7.TTCItem
144 0.12% 70.53% 58152 232 187176 764 748 [C
145 0.12% 70.65% 57888 2412 161904 6746 16621 java.lang.String
146 0.11% 70.77% 57400 1435 57400 1435 16855 java.util.HashMap
……
……
根据以上的结果,在java.hprof.txt中定位到导致分配大内存的操作如下:
TRACE 63:
java.lang.StringBuffer.expandCapacity(StringBuffer.java:202)
java.lang.StringBuffer.append(StringBuffer.java:401)
java.util.zip.ZipFile.getEntry(ZipFile.java:148)
java.util.jar.JarFile.getEntry(JarFile.java:198)
TRACE 92:
java.util.zip.InflaterInputStream.<init>(InflaterInputStream.java:71)
java.util.zip.ZipFile$1.<init>(ZipFile.java:240)
java.util.zip.ZipFile.getInputStream(ZipFile.java:212)
java.util.zip.ZipFile.getInputStream(ZipFile.java:183)
再进一步分析则需要应用开发人员对应用代码做相应的分析定位。
注意:使用HProf非常消耗资源,切记不要在生产系统使用。
Java优化编程(第二版)
2.1 垃圾回收堆内存
内存管理的话题在C或C++程序设计中讨论得相对较多,因为在C与C++程序设计中需要开发人员自己申请并管理内存,开发人员可以申请/借用(Apply)系统内存并且负责释放/归还(Release)系统内存,如果“只借不还”就会造成系统内存泄露的问题。在Java程序设计中,这些工作由Java虚拟机(JVM)负责处理。所有内存的申请、分配、释放都由JVM负责完成。因此,开发人员就省去了这部分工作,不过这并不意味着开发人员可以完全依赖于JVM的内存管理功能,如果你这样想并在实际的应用开发中也这样做,你所开发的应用的性能,就有可能不是最优的。这是因为,无论配置多么优良的硬件环境其自身资源都是有限的,由于没有合理、科学的使用内存资源,即使是Java应用也会出现内存枯竭的现象。例如,我们经常会遇到的OutOfMemoryException。再者Java语言(其实不只Java语言)的性能极大程度上依赖于其运行的硬件环境资源,而内存又是硬件环境资源中重要的一部分,因此说,如果开发人员开发的Java应用没能有效、合理地使用系统内存,那么这个应用就不可能具备较高的性能,甚至会导致整个系统在运行一段时间后崩溃。本章将对Java应用开发中与内存管理相关的技术做详细的讲解。
2.1 垃圾回收
谈到Java内存管理的话题,就必然会提到垃圾回收的概念,垃圾回收的英文名称为Garbage Collection,简称GC,它是Java程序设计中有关内存管理的核心概念,Java虚拟机(JVM)的内存管理机制被称为垃圾回收机制。因此,要想掌握在开发Java应用时怎样才能合理地管理内存,首先应该了解Java虚拟机的内存管理机制——垃圾回收机制。否则,在不了解垃圾回收具体实现机制的情况下讨论Java程序设计中的内存管理,优化Java应用性能,就有些纸上谈兵,舍本逐末了。
上面我们提到Java程序设计中的内存管理机制是通过垃圾回收来完成的,那么在JVM运行环境中什么样的对象是垃圾呢?下面我们给出了在JVM运行环境中垃圾对象的定义:
一个对象创建后被放置在JVM的堆内存(heap)中,当永远不再引用这个对象时,它将被JVM在堆内存(heap)中回收。被创建的对象不能再生,同时也没有办法通过程序语句释放它们。
我们也可以这样给JVM中的垃圾对象下定义:
当对象在JVM运行空间中无法通过根集合(root set)到达(找到)时,这个对象就被称为垃圾对象。根集合是由类中的静态引用域与本地引用域组成的。JVM中通过根集合索引对象如图2-1所示。
图2-1 JVM中通过根集合索引对象
&注意 图2-1中打了X标记的对象就是不可到达的对象,这些对象就被JVM视为垃圾对象并被JVM回收。JVM将给这些对象打上相应的标记,然后清扫回收这些对象,并将散碎的内存单元收集整合。
这里提到了堆内存的概念,它是JVM管理的一种内存类型,在做Java应用开发时经常会用到由JVM管理的两种类型的内存:堆内存(heap)与栈内存(stack)。有关堆内存的概念,在前面的相关章节中,已经做过相应的介绍。简单地讲,堆内存主要用来存储程序在运行时创建或实例化的对象与变量,例如:我们通过new MyClass()创建的类MyClass的对象。而栈内存(stack)则是用来存储程序代码中声明为静态(static)(或非静态)的方法,JVM、堆内存(heap)与栈内存(stack)三者的关系如图2-2所示。
图2-2 JVM、堆内存与栈内存的关系
下面通过一个实例来看一下堆内存(heap)与栈内存(stack)中所存储对象的类型有哪些不同。
… …
public class BirdTest {
static Vector birdList = new Vector();
static void makeBird () {
Object bird= new Bird ();
birdList.addElement(bird);
}
public static void main(String[] arg) {
makeBird ();
…
}
}
… …
在上面的代码中声明了一些静态的变量与方法,同时也通过关键字new创建了一些对象实例,下面给出这个简单的类在运行时JVM中堆内存(heap)与栈内存(stack)中所存储的对象情况,如图2-3所示。
图2-3 JVM中堆内存与栈内存中所存储的对象情况
在图2-3中,可以看到我们在类BirdTest中声明的Vector 类的birdList对象,以及在运行时创建的Bird对象都被放在了堆内存(heap)中,而把两个静态方法main()与makeBird()放在了栈内存(stack)中。这说明birdList对象占用了堆内存,静态方法main()与makeBird()则占用了栈内存。在对Java程序设计中内存管理技术做更为深入的讨论之前,有必要再详细地讲一下堆内存(heap)的相关知识。
堆内存
堆内存(heap)在JVM启动的时候就被创建,它是JVM中非常关键的一个内存管理区域。堆内存中所存储的对象可以被JVM自动回收,不能通过其他外部手段回收,也就是说开发人员无法通过添加相关代码的手段,回收位于堆内存中的对象。堆内存(heap)通常情况下被分为两个区域:新对象(new object)区域与老对象(old object)区域。这里又引入了两个有关JVM内存管理的新概念:新对象(new object)区域与老对象(old object)区域。下面分别对这两个概念做一下介绍。
新对象(new object)区域。又可以细分为三个小区域:伊甸园(Eden)区域、From区域与To区域。伊甸园区域用来保存新创建的对象,它就像一个堆栈,新的对象被创建,就像指向该栈的指针(如果你熟悉C语言,应该非常熟悉指针的概念)在不断增长一样,当伊甸园区域中的对象满了之后,JVM系统将要做可到达性测试,主要任务是检测有哪些对象由根集合出发是不可到达的,这些对象就可以被JVM回收,并且将所有的活动对象从伊甸园区域拷到To区域,此时一些对象将发生状态交换,有的对象就从To区域被转移到From区域,此时From区域就有了对象。上面对象迁移的整个过程,都是由JVM控制完成的。当我们在使用一些Java应用服务器软件时,通过其所提供的内存与性能监控界面,会看到这一过程引起的系统内存的变化。在这个过程执行期间,Java虚拟机的性能是非常低下的,这个过程会严重影响正在运行的应用的性能。
老对象(old object)区域。在老对象区域中的对象仍然会有一个较长的生命周期,大多数JVM系统中的垃圾对象,都来源于“短命”对象,经过一段时间后,被转入老对象区域的对象,就变成了垃圾对象。此时,它们都被打上相应的标记,JVM系统将会自动回收这些垃圾对象,建议你不要频繁地强制系统做垃圾回收,这是因为JVM会利用有限的系统资源,优先完成垃圾回收工作,致使应用无法快速地响应来自用户端的请求,这样会影响系统的整体性能,这也正是我们不建议读者自己频繁强制做垃圾回收的原因。
为了使读者能够更清楚地了解垃圾回收的过程,根据上面的讲解,给出了JVM做垃圾回收的过程示意图,如图2-4所示。
图2-4 JVM做垃圾回收的过程示意
通过上面的学习,我们知道垃圾回收与对象的生命周期是紧紧联系在一起的,那么JVM中的对象生命周期是怎样的呢?下面就讲解一下JVM中对象的生命周期的相关知识。
2.2 JVM中对象的生命周期
在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。
2.2.1 创建阶段
在对象创建阶段,系统要通过下面的步骤,完成对象的创建过程:
(1)为对象分配存储空间。
(2)开始构造对象。
(3)递归调用其超类的构造方法。
(4)进行对象实例初始化与变量初始化。
(5)执行构造方法体。
上面的5个步骤中的第3步就是指递归地调用该类所扩展的所有父类的构造方法,一个Java类(除Object类外)至少有一个父类(Object),这个规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。实际上,在Java程序设计中,任何一个Java类都直接或间接的是Object类的子类。例如下面的代码:
public class A {
…
}
这个声明等同于下面的声明:
public class A extends java.lang.Object {
…
}
上面讲解了对象处于创建阶段时,系统所做的一些处理工作,其中有些过程与应用的性能密切相关,因此在创建对象时,我们应该遵循一些基本的规则,以提高应用的性能。
下面是在创建对象时的几个关键应用规则:
(1)避免在循环体中创建对象,即使该对象占用内存空间不大。
(2)尽量及时使对象符合垃圾回收标准。
(3)不要采用过深的继承层次。
(4)访问本地变量优于访问类中的变量。
关于规则(1)避免在循环体中创建对象,即使该对象占用内存空间不大,需要提示一下,这种情况在我们的实际应用中经常遇到,而且我们很容易犯类似的错误,例如下面的代码:
… …
for (int i = 0; i < 10000; ++i) {
Object obj = new Object();
System.out.println("obj= "+ obj);
}
… …
上面代码的书写方式相信对你来说不会陌生,也许在以前的应用开发中你也这样做过,尤其是在枚举一个Vector对象中的对象元素的操作中经常会这样书写,但这却违反了上述规则(1),因为这样会浪费较大的内存空间,正确的方法如下所示:
… …
Object obj = null;
for (int i = 0; i < 10000; ++i) {
obj = new Object();
System.out.println("obj= "+ obj);
}
… …
采用上面的第二种编写方式,仅在内存中保存一份对该对象的引用,而不像上面的第一种编写方式中代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。因此在循环体中声明创建对象的编写方式应该尽量避免。
另外,不要对一个对象进行多次初始化,这同样会带来较大的内存开销,降低系统性能,如:
public class A {
private Hashtable table = new Hashtable ();
public A() {
// 将Hashtable对象table初始化了两次
table = new Hashtable();
}
}
正确的方式为:
public class B {
private Hashtable table = new Hashtable ();
public B() {
}
}
不要小看这个差别,它却使应用软件的性能相差甚远,如图2-5所示。
图2-5 初始化对象多次所带来的性能差别
看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则我们开发出来的应用也是低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。因此,我们在日常的应用开发中,应该认真对待每一行代码,采用最优化的编写方式,不要忽视细节,不要忽视潜在的问题。
2.2.2 应用阶段
当对象的创建阶段结束之后,该对象通常就会进入对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身“存在价值”的时期。在对象的应用阶段,对象具备下列特征:
— 系统至少维护着对象的一个强引用(Strong Reference);
— 所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。
上面提到了几种不同的引用类型。可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。在讲解这几种不同类型的引用之前,我们必须先了解一下Java中对象引用的结构层次。
Java对象引用的结构层次示意如图2-6所示。
图2-6 对象引用的结构层次示意
由图2-6我们不难看出,上面所提到的几种引用的层次关系,其中强引用处于顶端,而虚引用则处于底端。下面分别予以介绍。
1.强引用
强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。
2.软引用
软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。再者,软可到达对象的所有软引用都要保证在虚拟机抛出OutOfMemoryError之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。下面是软引用的实现代码:
… …
import java.lang.ref.SoftReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,将它设置为soft 引用类型,并且释放强引用;
SoftReference sr = new SoftReference(a);
a = null;
…
// 下次使用时
if (sr!=null) {
a = sr.get();
}
else{
// GC由于内存资源不足,可能系统已回收了a的软引用,
// 因此需要重新装载。
a = new A();
sr=new SoftReference(a);
}
… …
软引用技术的引进,使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃(crash)。因此在处理一些占用内存较大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建(这里是指那些没有保留运行过程中状态的对象),提高应用对内存的使用效率,提高系统稳定性。但事物总是带有两面性的,有利亦有弊。在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。
3.弱引用
弱引用(Weak Reference)对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。虽然,GC在运行时一定回收Weak引用对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map数据结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null时,对这个对象引用就不存在了,GC能够快速地回收该对象空间。与软引用类似我们也可以给出相应的应用代码:
… …
import java.lang.ref.WeakReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,将它设置为weak 引用类型,并且释放强引用;
WeakReference wr = new WeakReference (a);
a = null;
…
// 下次使用时
if (wr!=null) {
a = wr.get();
}
else{
a = new A();
wr = new WeakReference (a);
}
… …
弱引用技术主要适用于实现无法防止其键(或值)被回收的规范化映射。另外,弱引用分为“短弱引用(Short Week Reference)”和“长弱引用(Long Week Reference)”,其区别是长弱引用在对象的Finalize方法被GC调用后依然追踪对象。基于安全考虑,不推荐使用长弱引用。因此建议使用下面的方式创建对象的弱引用。
… …
WeakReference wr = new WeakReference(obj);
或
WeakReference wr = new WeakReference(obj, false);
… …
4.虚引用
虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。虚引用主要适用于以某种比 Java 终结机制更灵活的方式调度 pre-mortem 清除操作。
&注意 在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
2.2.3 不可视阶段
在一个对象经历了应用阶段之后,那么该对象便处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。
… …
public void process () {
try {
Object obj = new Object();
obj.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
while (isLoop) { // ... loops forever
// 这个区域对于obj对象来说已经是不可视的了
// 因此下面的代码在编译时会引发错误
obj.doSomething();
}
}
… …
如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行obj.doSomething();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。
2.2.4 不可到达阶段
处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。
2.2.5 可收集阶段、终结阶段与释放阶段
对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:
(1)垃圾回收器发现该对象已经不可到达。
(2)finalize方法已经被执行。
(3)对象空间已被重用。
当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。
2.3 Java中的析构方法finalize
在C++程序设计中有构造函数与析构函数的概念,并且是内存管理技术中相当重要的一部分,而在Java语言中只有构造器(也可以称为构造函数)的概念,却没有析构器或析构函数的概念。这是因为,理论上JVM负责对象的析构(销毁与回收)工作。也就是上面讲到的垃圾回收的概念。那么Java语言中是否真的不存在与C++中析构函数职能类似的方法?其实Java语言中的finalize 方法与C++语言中的析构函数的职能就极为类似。finalize方法是Java语言根基类Object类中所包含的一个方法,这个方法是保护类型的方法(protected),由于在Java应用中开发的所有类都为Object的子类,因此用户类都从Object对象中隐式地继承了该方法。因此,我们在Java类中可以调用其父类的finalize方法,并且可以覆盖自身继承来的finalize方法。虽然我们可以在一个Java类中调用其父类的finalize方法,但是由于finalize方法没有自动实现递归调用,我们必须手动实现,因此finalize函数的最后一个语句通常是super.finalize()语句。通过这种方式,我们可以实现从下到上finalize的迭代调用,即先释放用户类自身的资源,然后再释放父类的资源。通常我们可以在finalize方法中释放一些不容易控制,并且非常重要的资源,例如:一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。
finalize方法最终是由JVM中的垃圾回收器调用的,由于垃圾回收器调用finalize的时间是不确定或者不及时的,调用时机对我们来说是不可控的,因此,有时我们需要通过其他的手段来释放程序中所占用的系统资源,比如自己在类中声明一个destroy()方法,在这个方法中添加释放系统资源的处理代码,当你使用完该对象后可以通过调用这个destroy()方法来释放该对象内部成员占用的系统资源。虽然我们可以通过调用自己定义的destroy()方法释放系统资源,但是还是建议你最好将对destroy()方法的调用放入当前类的finalize()方法体中,因为这样做更保险,更安全。在类深度继承的情况下,这种方法就显得更为有效了,我们可以通过递归调用destroy的方法在子类被销毁的时候释放父类所占用的资源,例如下面的代码:
1.原始基类A
public class A {
Object a = null;
public A() {
a = new Object();
System.out.println("创建a对象");
}
protected void destroy() {
System.out.println("释放a对象");
a = null;
// 释放自身所占用的资源
…
}
protected void finalize() throws java.lang.Throwable {
destroy();
// 递归调用超类中的finalize方法
super.finalize();
}
}
2.一级子类B
public class B extends A {
Object b = null;
public B() {
b = new Object();
System.out.println("创建b对象");
}
protected void destroy() {
b = null;
// 释放自身所占用的资源
System.out.println("释放b对象");
super.destroy();
}
protected void finalize() throws java.lang.Throwable {
destroy();
// 递归调用超类中的finalize方法
super.finalize();
}
}
3.二级子类C
public class C extends B {
Object c = null;
public C() {
c = new Object();
System.out.println("创建c对象");
}
protected void destroy() {
c = null;
// 释放自身所占用的资源
System.out.println("释放c对象");
super.destroy();
}
protected void finalize()throws java.lang.Throwable {
destroy();
// 递归调用超类中的finalize方法
super.finalize();
}
}
上面的三个类的继承关系是非常明晰的:A->B->C,类A是原始基类(这是一种习惯叫法),类B继承了类A,类C又继承了类B。其实类A并不是真正意义上的原始基类,上面我们已经提到过Java语言中的原始基类是Object类,尽管我们并没有显式的声明,但这已经是系统约定俗成的了。
为了简单清楚地说明问题,我们在这三个类中分别声明了3个方法,用来论证上面所讲解的知识点,在类A的构造器中我们初始化了一个对象a,在destroy方法中通过a = null;释放其自身所占用的资源。并且在finalize方法中,我们调用了destroy方法用来释放其自身所占用的资源,然后调用其超类Object的finalize方法,这是我们以上所提到的“双保险”的内存释放方法;类B与类C的结构与类A极为相似,它们除了释放自身所占用的资源外,它们还在其对应的方法中调用其超类的destroy方法与finalize方法,用来释放超类所占用的资源。如在类B中调用其超类A的destroy方法与finalize方法与在类C中调用其超类B的destroy方法与finalize方法。但是类A与类B、类C有一点不同,那就是在其destroy方法中没有super.destroy()语句,这是因为其超类Object并没有destroy方法。下面看一下当我们调用初始化与销毁类C时,会有什么样的情况发生。以下是调用完成这个过程的测试类Test的源代码:
public class Test {
c = null;
public Test () {
c = new C();
}
public static void main(String args[]) {
MyClass me = new MyClass();
me.destroy();
}
protected void destroy () {
if (c != null) {
c.destroy();
}else {
System.out.println("c对象已被释放");
}
}
}
编译执行Test.java:
> javac Test.java
> java Test
下面是这个程序的运行结果:
创建a对象
创建b对象
创建c对象
释放c对象
释放b对象
释放a对象
我们注意到当在Test类中初始化类C的对象时,其构造器产生了递归调用,并且是由基类开始依次调用、初始化成员对象的,而当调用C类对象的destroy方法时系统同样产生了递归调用,但调用的顺序却与初始化调用的顺序完全相反,释放资源的调用顺序是由子类开始的,依次调用其超类的资源释放方法destroy()。由此可见,我们在设计类时应尽可能地避免在类的默认构造器中创建、初始化大量的对象。一个原因是在实例化自身的情况下,造成较大的资源开销;另一个原因是其子类在被实例化时,也同样会带来较大的系统资源开销。因为即使我们没有想调用父类的构造器创建大量无用的对象(至少有时候这些对象对我们是没有意义的),但是系统会自动创建它们,而这些操作与过程对于我们来说是隐含的。为了防止上述情况的发生,造成不必要的内存资源浪费,我们应当尽量不在类的构造器中创建、初始化大量的对象或执行某种复杂、耗时的运算逻辑。
2.4 数组的创建
数组空间的申请也是一个与内存管理关系密切的技术话题。数组空间的申请分为显式申请与隐式申请两种。显式申请是指在程序中直接给出数组的类型与长度,例如下面的代码:
… …
int[] intArray = new int[1024];
… …
上面的这行代码的意义是,显式地向系统一次性申请了大小为1KB的整数类型的内存空间,这样的声明方式一般出现在对文件或网络资源数据读取的处理代码中,往往用数组来作为数据读取的缓冲区,以提高读取效率。由于我们不知道具体读取的内容的长度,因此,我们只能通过这种方式来读取相关资源,这样做显然有些弊端。例如,文件、网络资源的长度小于你所申请的数组的长度,这就造成了系统内存资源浪费。隐式申请是在声明数组对象时不知道将要得到的数组的具体长度,例如下面的代码:
… …
int[] intArray = obj.getIntArray();
System.out.println("整型数组长度:"+intArray.length());
… …
在这行代码中我们事先并不知道obj.getIntArray()到底返回的数组长度是多少,这是在程序运行时才能确定的,因此,这里不存在上面的显式申请数组浪费内存的问题,因为数组的长度是由系统决定的,因此,这种方法是值得提倡使用的。但是这种隐式申请的方法只适用于接收某个方法返回值为数组的情况。
如果遇到数组中所保存的元素占用内存空间较大或数组本身长度较大的情况,我们可以采用上面所讲到的软引用的技术来引用数组,以“提醒”JVM及时回收垃圾内存,维护系统的稳定性。例如下面的代码:
… …
Object obj = new char[1000000];
SoftReference ref = new SoftReference(obj);
… …
由于数组对象长度较长,占用了较大的内存空间,因此我们对obj采用了软引用的处理方式,由JVM根据运行时的内存资源的使用情况,来把握是否回收该对象,释放该内存。虽然这会对应用程序产生一些影响(如当我们想使用该数组对象的时候,该对象被回收了)。但是这样做却能保证应用整体的稳健性,达到合理使用系统内存的目的。
2.5 共享静态变量存储空间
我们知道类中的静态变量(Static Variable)在程序运行期间,其内存空间对所有该类的对象实例而言是共享的,因此在某些时候为了节省系统内存开销、共享资源,将类中的一些变量声明为静态变量,通过下面的例子,你可以发现合理应用静态变量带来的好处:
public class WeekA{
static class Data {
private int week;
private String name;
Data(int i, String s) {
month = i;
name = s;
}
}
Data weeks[] = {
new Data(1, "Monday"),
new Data(2, "Tuesay"),
new Data(3, "Wednesday"),
new Data(4, "Thursday"),
new Data(5, "Friday"),
new Data(6, "Saturday")
new Data(7, "Sunday")
};
public static void main(String args[]) {
final int N = 200000;
WeekA weekinstance;
for (int i = 1; i <= N; i++){
weekinstance = new WeekA ();
}
}
}
在上面这段代码中,没有将Data weeks声明为静态变量,因此当创建WeekA对象时将会得到200 000个weeks对象的副本,这些对象被保存在内存中,但是weeks对象中的数据却从来没有被更改过,而且十分稳定。因此,如果能使所有对象共享该数据对象是个不错的解决办法,请看下面的代码:
public class WeekB{
static class Data {
private int week;
private String name;
Data(int i, String s) {
month = i;
name = s;
}
}
static Data weeks[] = {
new Data(1, "Monday"),
new Data(2, "Tuesay"),
new Data(3, "Wednesday"),
new Data(4, "Thursday"),
new Data(5, "Friday"),
new Data(6, "Saturday")
new Data(7, "Sunday")
};
public static void main(String args[]) {
final int N = 200000;
WeekB weekinstance;
for (int i = 1; i <= N; i++){
weekinstance = new WeekB ();
}
}
}
请注意在类WeekB中,在Data weeks[]之前添加了static关键字,将该对象变量声明为静态的,因此当你创建200 000个WeekB对象时系统中只保存着该对象的一份拷贝,而且该类的所有对象实例共享这份拷贝,这无疑节约了大量的不必要的内存开销,同时实现了要完成的系统功能。
那么是不是我们应该尽量地多使用静态变量呢?其实不是这样的,因为静态变量生命周期较长,而且不易被系统回收,因此如果不能合理地使用静态变量,就会适得其反,造成大量的内存浪费,所谓过犹不及。因此,建议在具备下列全部条件的情况下,尽量使用静态变量:
(1)变量所包含的对象体积较大,占用内存较多。
(2)变量所包含的对象生命周期较长。
(3)变量所包含的对象数据稳定。
(4)该类的对象实例有对该变量所包含的对象的共享需求。
如果变量不具备上述特点建议你不要轻易地使用静态变量,以免弄巧成拙。
2.8 不要提前创建对象
为了节省系统内存资源,不提前申请并不急需的内存空间。我们应当尽量在需要的时候创建对象。重复地分配、构造对象可能会因垃圾回收(GC)做额外的工作,降低系统性能,例如下面的代码:
… …
void f() {
int i;
A a = new A();
// 类A 的对象a被创建
// 在判断语句之外没有
// 应用过a对象
...
if (...) {
// 类A 的对象a仅在此处被应用
a.showMessage();
...
}
...
}
… …
正确的书写方式为:
void f() {
int i;
...
if (...) {
A a = new A();
// 类A的对象a被创建
// 在判断语句中
// 使用了a对象
a.showMessage();
}
...
}
上面的代码是在使用a对象的时候才去初始化了a,而不是提前初始化。这样的代码更健壮、高效。
2.9 JVM内存参数调优
我们前面所提到的堆内存(heap)是由Java虚拟机控制管理的,因此,这些参数对JVM而言都有一个默认值,但在某些情况下这些参数的默认值并不是最优的,这就需要我们通过调整这些参数的值来提高JVM的性能,最终提高应用的性能指标。
在实际的应用开发中,如果应用所使用的系统内存较大,经常会引发内存溢出的错误:
…
java.lang.OutOfMemoryError <<no stack trace available>>
java.lang.OutOfMemoryError <<no stack trace available>>
Exception in thread "main"
…
这可能是因为应用要使用的堆内存(heap)超过了JVM所管理内存范围,如果我们适当追加内存值有时就可以避免这种致命错误的出现。
在WINDOWS系统上你可以通过参数-verbosegc查看JVM回收内存的信息,在HP UNIX系统上你可以通过-Xverbosegc:file=/tmp/gc$$.out参数将信息重定向到一个文件中。然后查看相应的信息,例如下面的这个类。
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
}
System.out.println("this is a GC test");
}
}
在类A的main方法中创建了100 000个A对象,然后我们看一下JVM回收内存的情况,编译并执行这个类:
>java -verbosegc A
[GC 512K->91K(1984K), 0.0027537 secs]
this is a GC test
从输出信息中可以看出总共有1984KB的内存被回收,耗时0.002 753 7秒。现在我们将类A添加一行清除对象引用的代码:
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
a = null;
}
System.out.println("this is a GC test");
}
}
编译并执行这个类:
>java -verbosegc A
[GC 512K->91K(1 984K), 0.0 027 450 secs]
this is a GC test
我们看到被回收内存的数量并没有变化,但是回收所需要的时间却变成了0.002 745 0秒,后者比前者节省了0.000 008 7秒,千万不要小看这0.000 008 7秒,当你的应用足够复杂时这个时间就会成指数级增长,看来我们主动清除对象引用的方法,确实可以加速JVM对垃圾内存的回收。
如果再在类A中加入一行强制系统内存回收的代码,结果又会怎样呢?如下所示:
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
a = null;
}
System.gc();
System.out.println("this is a GC test");
}
}
编译并执行这个类:
>java -verbosegc A
[GC 512K->91K(1984K), 0.0 027 272 secs]
[Full GC 487K->91K(1984K), 0.0 070 730 secs]
this is a GC test
系统这次做了两次内存回收,第一次是程序中强制系统内存回收的代码System.gc()导致的内存回收,而后者是系统最终的内存回收操作,我们看到强制内存回收耗时不长,可是却导致了系统最终垃圾回收的时间加长了很多,因此我们在采用强制系统垃圾回收(通过显式调用方法System.gc())的办法来回收系统垃圾内存的办法,还是存在一些弊端的,应尽量少用,或者说只在必要的时候应用。
上面我们提到的内存回收操作就是回收JVM所管理的堆内存(heap)。当系统连续申请内存并且超过JVM所管理的堆内存(heap)的最大值时,就会产生系统内存溢出的致命异常,下面我们来看一下怎样通过设置JVM的内存参数来优化JVM对内存的管理,避免内存溢出异常的发生。表2-1所示的就是与JVM内存相关的参数及其说明。
表2-1 与JVM内存相关的参数及其说明
JVM堆内存(heap)设置选项 参数格式 说 明
设置新对象生产堆内存(Setting the Newgeneration heap size) -XX:NewSize 通过这个选项可以设置Java新对象生产堆内存。在通常情况下这个选项的数值为1 024的整数倍并且大于1MB。这个值的取值规则为,一般情况下这个值-XX:NewSize是最大堆内存(maximum heap size)的四分之一。增加这个选项值的大小是为了增大较大数量的短生命周期对象
增加Java新对象生产堆内存相当于增加了处理器的数目。并且可以并行地分配内存,但是请注意内存的垃圾回收却是不可以并行处理的
续表
JVM堆内存(heap)设置选项 参数格式 说 明
设置最大新对象生产堆内存(Setting the maximum New generation heap size) -XX:MaxNewSize 通过这个选项可以设置最大Java新对象生产堆内存。通常情况下这个选项的数值为1 024的整数倍并且大于1MB
其功用与上面的设置新对象生产堆内存-XX:NewSize相同
设置新对象生产堆内存的比例(Setting New heap size ratios) -XX:SurvivorRatio 新对象生产区域通常情况下被分为3个子区域:伊甸园,与两个残存对象空间,这两个空间的大小是相同的。通过用-XX:SurvivorRatio=X选项配置伊甸园与残存对象空间(Eden/survivor)的大小的比例。你可以试着将这个值设置为8,然后监控、观察垃圾回收的工作情况
设置堆内存池的最小值
(Setting minimum heap size) -Xms 通过这个选项可以要求系统为堆内存池分配内存空间的最小值。通常情况下这个选项的数值为1 024的整数倍并且大于1MB。这个值的取值规则为,一般情况下这个值(-Xms)与最大堆内存相同,以降低垃圾回收的频度
设置堆内存池的最大值(Setting maximum heap size) -Xmx 通过这个选项可以要求系统为堆内存池分配内存空间的最大值。通常情况下这个选项的数值为1 024的整数倍并且大于1 MB
一般情况下这个值(-Xmx)与最小堆内存(minimum heap size –Xms)相同,以降低垃圾回收的频度
取消垃圾回收 -Xnoclassgc 这个选项用来取消系统对特定类的垃圾回收。它可以防止当这个类的所有引用丢失之后,这个类仍被引用时不会再一次被重新装载,因此这个选项将增大系统堆内存的空间
设置栈内存的大小 -Xss 这个选项用来控制本地线程栈的大小,当这个选项被设置的较大(>2MB)时将会在很大程度上降低系统的性能。因此在设置这个值时应该格外小心,调整后要注意观察系统的性能,不断调整以期达到最优
根据表2-1中所描述的参数意义,我们可以在启动应用时为JVM设置相应的参数值以提高系统的性能,例如下面的例子:
java -XX:NewSize=128m -XX:MaxNewSize=128m -XX:SurvivorRatio=8 -Xms512m
-Xmx512m MyApplication
类文件(.class)的大小
由Java源文件.java文件编译成JVM可解释执行的Java字节文件.class。因所采用的编译方式的不同而大小也不同。通常.class文件的大小也存在是否占用较大内存的问题。通过降低.class文件的大小,不但可以降低系统内存的开销,还可以节省网络开销,虽然这部分内容与JVM内存管理联系不大,但是我觉得还是有必要提一下,因为这在你开发Applet应用时会有帮助(注:在本书后续的章节中,将会对如何减小Java类尺寸的技术话题做更为深入的探讨)。因为一般来说,Applet应用都是靠网络分布式传输由客户端浏览器装载运行的,如果类文件较大,无疑将会增大网络开销,降低传输速度无法满足用户的需求,并且如果类文件较大,无疑也会消耗客户端内存资源。我们可以通过在Java编译器javac中添加相应的参数,来缩小类文件的大小,解决上面的问题。
通常有三种编译方式会影响类文件的大小。
(1)默认编译方式: javac A.java。
(2)调试编译方式: javac –g A.java。
(3)代码编译方式: javac –g:none A.java。
例如如下所示的简单的类A:
public class A {
public static void main(String args[]) {
for (int i =0 ;i < 100000;++i) {
A a = new A();
}
}
}
通过上面这三种方式编译后的类文件的大小分别为:
默认编译方式:291字节。
调试编译方式:422字节。
代码编译方式:207字节。
采用三种不同的方式,编译产生的类文件的大小差异非常大,这是什么原因导致的呢?原来在于.class文件中包含多个不同的部分或属性。
代码(Code)属性包含实际的方法字节码。源文件信息(SourceFile Information)包含用于生成.class的源文件名称。代码行序号表(LineNumberTable)用来映射源文件中的代码行序号与字节码文件中的序号偏移。本地变量表(LocalVariableTable)用来映射本地变量与栈桢的偏移。
&注意 如果你想了解字节码文件.class的文件结构详细信息,请参考相关的技术资料,这里就不详细讲解了。
正是由于上面这三种编译方式生成的类文件所包含的信息不同,才导致了类文件的大小差异较大,其包含的信息分别如下所示。
默认编译方式:代码(Code)、源文件信息(SourceFile Information)、代码行序号表(LineNumberTable)。
调试编译方式:代码(Code)、源文件信息(SourceFile Information)、代码行序号表(LineNumberTable)、本地变量表(LocalVariableTable)。
代码编译方式:代码(Code)。
这就是三种编译方式产生类文件大小不同的根本原因。而这三种编译方式在程序开发的不同阶段却都起着非常重要的作用,例如,调试编译方式在程序的调试开发过程中应采用,以获取更为详细的调试信息。因此具体应用上面的三种编译方式中的哪一种,应该适时而定。
2.10 Java程序设计中有关内存管理的其他经验
根据上面讲解的JVM内存管理系统的工作原理,我们可以通过一些技巧和方式,让JVM做GC处理时更加有效率,更加符合应用程序的要求。以下就是程序设计的一些经验。
(1)最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null。我们在使用这种方式时,必须特别注意一些复杂的对象图,例如数组、队列、树、图等,这些对象之间的相互引用关系较为复杂。对于这类对象,GC回收它们的效率一般较低。如果程序允许,尽早将不用的引用对象赋为null。这样可以加速GC的工作。 例如:
… …
A a = new A();
// 应用a对象
a = null; // 当使用对象a之后主动将其设置为空
… …
但要注意,如果a是方法的返回值,千万不要做这样的处理,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现。因此这时很难及时抓住、排除NullPointerException异常。
(2)尽量少用finalize函数。finalize函数是Java给程序员提供一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。
(3)如果需要使用经常用到的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。
(4)注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象,造成内存浪费。
(5)尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其自类的构造器时造成不必要的内存资源浪费。
(6)尽量避免强制系统做垃圾内存的回收(通过显式调用方法System.gc()),增长系统做垃圾回收的最终时间,降低系统性能。
(7)尽量避免显式申请数组空间,当不得不显式地申请数组空间时尽量准确地估计出其合理值,以免造成不必要的系统内存开销。
(8)尽量在做远程方法调用(RMI)类应用开发时使用瞬间值(transient)变量,除非远程调用端需要获取该瞬间值(transient)变量的值。
(9)尽量在合适的场景下使用对象池技术以提高系统性能,缩减系统内存开销,但是要注意对象池的尺寸不易过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行环境所提供内存资源的数量。