#
Special case: primitive types
Java determines the size of each primitive type. These sizes don’t change from one machine
architecture to another as they do in most languages. This size invariance is one reason Java
programs are more portable than programs in most other languages.
All numeric types are signed, so don’t look for unsigned types.
The size of the boolean type is not explicitly specified; it is only defined to be able to take the literal values true or false.
The “wrapper” classes for the primitive data types allow you to make a non-primitive object on the heap to represent that primitive type. For example:
char c = 'x';
Character ch = new Character(c);
Or you could also use:
Character ch = new Character('x');
Java SE5 autoboxing will automatically convert from a primitive to a wrapper type:
Character ch = 'x';
and back:
char c = ch;
The reasons for wrapping primitives will be shown in a later chapter.
High-precision numbers
Java includes two classes for performing high-precision arithmetic: BigInteger and
BigDecimal. Although these approximately fit into the same category as the “wrapper” classes, neither one has a primitive analogue.
Both classes have methods that provide analogues for the operations that you perform on
primitive types. That is, you can do anything with a BigInteger or BigDecimal that you can with an int or float, it’s just that you must use method calls instead of operators. Also, since there’s more involved, the operations will be slower. You’re exchanging speed for accuracy.
BigInteger supports arbitrary-precision integers. This means that you can accurately represent integral values of any size without losing any information during operations.
BigDecimal is for arbitrary-precision fixed-point numbers; you can use these for accurate
monetary calculations, for example.
OX123=1×162+2×161+3×160
三种形式的整型常量数据:
1.十进制
2.八进制,以o开头O123
=1×82+1×81+3×80=十进制的83
3.十六进制 ,以ox开头 OX123=1×162+2×161+3×160
1.The hidden implementation
The goal of the class creator is to build a class that exposes only what is necessary to the client programmer and keeps everything else hidden. Why?
(1)Becuase if it is hidden, the client programmer can't access it, which means that the class creator can change the hidden portion at will withou worring about the impact on anyone else.
(2)The hidden portion usually reprsents the tender insides of an object that could easily be corrupted by a careless or uninformed client programmer, so hiding the implementation reduces program bugs.
2.Reusing the implementation
The simplest way to reuse a class is to just use an object of that class directly, but you can also place an object of that class inside a new class. We call this "creating a member object." Your new class can be made up of any mumber and type of other objectss, in any combination that you need to achieve the fuctionality desired in your new class. Because you are composing a new class from existing classes, this conception is called composition. Compositon is often referred to as a "has-a" relationship, as "A car has an engine."
Because inheritance is so important in OOP, it is often highly emphasized, and the new programmer can get the idea that inheritance should be used everywhere. This can result in awkward and overly complicated designs. Instead, you should first look to composition when creating new classes, since it is simpler and more flexible. If you take this approach,your designer will be cleaner. Once you have had some experience, it will be reasonably obvious when you need inheritance.
3.Inheritance
You have two ways to differentiate your new derived class from the original base class.
The first is quite straightforward: You simply add brand new methods to the derived class. This means that the base class simply didn't as much as you wanted it to, so you added more methods. This simple and primitive use for inheritance is, at times, the perfect solution to your problem. However, you should look closely for the posiblilty that your base class might also need these additional methods. This process of discovery and iteration of your design happens regularly in OOP.
The second and more important way to differentiate your new class is to change the behavior of an existing base-class method. This is referred to as overriding that method. To override a method, you simply create a new definition for the method in the derived class. You are saying, "I am using the same interface method here, but I want it to do something different for my new type."
4.Is-a vs. is-like-a relationships
5.Interchangeable objects with polymorphism
6The single rooted hierarchy
All objects have a single rooted hierarchy can be guaranteed to have certain functionality. You know you can perform certain basic operations on every object in your system. All objects can easy be created on the heap, and argument passing is greatly simplified.
A single rooted hierarchy makes it much easier to implement a garbage collector, which is one of the fundamental improvements of Java over C++. And since information about the type of an object is guaranteed to be in all objects, you'll never end up with an object whose type you cannot determine. This is especially important with system-level operations, such as exception handling, and to allow greater flexibility in programming.
7.Containers
8.Parameterized types(generics)
One of the big changes in Java SE5 is the addition of parameterized types, called generics in java. you will recongize the use of generics by angle brackets with types inside.
9.Object creation & lifetime
How can you possibly know when to destroy the objects?
(1).C++ takes the approach that control of efficiency is the most important issue, so it give the programmer a choice.
(2).Java, in heap
10 Exception handling: dealing with errors
所有的整数类型(除了char 类型之外)都是有符号的整数
因为, java的byte是8bit(位),就是8个0/1 来表示。
但是第一位是符号位,表示正数还是负数。所以:
0000 0001表示1, (1×
20)
0000 0000表示0, (0×
20)
计算机中负数的二进制码是是负数的绝对值取反,然后加1.
例如-1的二进制:
-1的绝对值是1(0000 0001);
取反是(1111 1110);
再加 1(0000 0001 );
结果是(1111 1111)
要对一个负数的二进制进行解码,首先对其所有的位取反,然后加1。
例如-1的 二进制 (1111 1111)
取反: 0000 0000 是0
再加1:(0+1=1)
符号位是1,是负数,所以是-1
1000 0000 表示-128, (解码过程:位取反是0111 1111==》127,然后加1==》128,符号位为1,是负数,表示-128)
软件在安装时,到底做了些什么? 大家每天都在用电脑,可能也经常在自己的电脑上安装软件。就算自己没安装过,至少也看到人家安装过软件。在这里,我不是想教你怎么安装软件,而是想向你展示,软件在安装的过程中,到底都做了些什么动作?为什么有些软件要安装,直接拷贝过去却不能用?为什么一些软件安装或卸载之后要重启。下面要讨论的就是这些问题。
首先,我们探讨一下软件安装的共通部分,说共通,就是在不同版本的操作系统上,如WINDOWS98,WIN2K和WINXP等上它们都有共同点的地方。这个文章也试图不针对具体的某个操作系统,而对共同的规律来探讨,不过我自己用的是WINDOWS98,所以有时一些例子可能会用WINDOWS98上的实例来说明,而大多数情况下这些特***在WIN2K和WINXP上也是类似的。
那么,我先来归纳一下,典型的软件安装过程都有可能做哪些事情。由于我们是讨论软件在安装时的行为,所以开始安装前的设置和选项我们就暂不讨论,只说到软件真正开始安装那个时候起的动作:
①文件从安装源位置拷贝到目标位置。
②往系统目录写入一些必要的动态连接库(DLL)。(可选)
③往系统注册表中写入相应的设置项。(可选)
④建立开始菜单里的程序组和桌面快捷方式。(可选)
⑤其他动作。(可选)
下面我们再详细来分析上面归纳出来的这些动作:
1)拷贝软件本身需要的文件。源位置指软件未安装之前的位置,例如光盘,下载的目录等,目标位置指你指定的安装位置。
这是几乎所有的软件安装过程一定会做的一件事。而如果一个软件,在安装时只要这一步,不需要后面的其他几步,我们可以认为这个软件就是绿色软件。或者反过来说绿色软件就是只要拷贝文件,不需要依赖于某个DLL,或者它依赖的DLL在几乎所有的系统中都一定有的,并且它也不依赖于注册表里面的设置项的软件。
2)这一步,可以说至少有一半软件在安装时都会做,一些软件,需要用到某个DLL,特别是那些软件作者开发的DLL,或者系统中不常用的DLL,一般都会随软件的安装拷到系统目录。所谓系统目录,在WIN98下一般是在WINDOWS\SYSTEM这个目录,而WIN2K是在WINNT\SYSTEM32,WINXP是在WINDOWS\SYSTEM32。还有,一些软件如QQ游戏,中游等,它们也用到一些DLL,由于这些DLL只是这个软件自己用到,别的其他软件不会用到,所以它们并不一定存在于系统目录,而是放在软件安装目录里面,这样的DLL已经在上一步中被拷贝,所以和这一步说的情况不一样。
3)这一步同样至少有一半软件会做,一般在安装前用户的设置和一些选项,在安装时就会把这些设置写到注册表里。另外就是有时在上一步把DLL拷贝到系统目录时,一些DLL需要向系统注册,这些DLL的注册信息也会写在注册表里。还有,一些软件有时可能安装时并不写注册表,而是在第一次运行时才把一些设置写到注册表。
4)这个非常简单,大概不需要怎么解释。建立这些快捷方式一方面是便于用户执行,另外在时也会把卸载的快捷方式放在程序组里。关于卸载后面我们再来讨论。
5)这个就是除了上面说的以外的其他情况。例如有些软件安装时会先把所有文件(或一部分文件)先解压到临时目录,那么安装完之后就要把这些文件删除掉。
那么我们再总结一下:
一、一个典型的软件在安装过程一般都会执行上面的1-4项。这样可以认为是一个完整的安装过程。
二、除了第1项之外,其他的都不是必要的。只需要第一项的软件,我们可以把它叫做绿色软件。
三、有些软件安装时是执行了1、2、4,有些软件是执行了1、3、4,有些软件是执行了1、4。
四、一个特殊的情况,一般的驱动程序,只会执行2和3,没有1和4。
五、理论上,任何软件,如果你非常确切地知道了它在上面的那几步都具体做了些什么,特别是2和3,那么,理论上你可以把这个软件的安装文件拷贝到另一台机子,把必要的DLL从系统目录拷贝到那一台机子的系统目录,再把注册表里软件写入的项目导出来(必要时还要修改一下)再导入到那台机子的注册表中,那么,就算不是绿色软件,你也能这样把它移植给另一台机。但有时特别是一些共享软件,一般都会有注册表中设置比较隐蔽的项目,不容易查找,所以除非你对系统非常熟悉,否则不是绿色软件的软件要移植还是有一定的难度的。
那么,下面我们再来看看,为什么一些软件安装后要重启。
在WINDOWS操作系统上,一般一个正在运行中的程序,操作系统是不让你修改它的,修改包括替换,改动和删除。那么有时,一些软件需要向系统目录中写入一个DLL,而系统目录中原来已经有同名的DLL并且这个DLL目前正在被系统使用,因此不能用新版本去替换它,这个时候就需要重启,在重启的过程中,在这个DLL旧的版本被使用之前用新版本替换它。这就是为什么要重启的原因。
你能看到这里,说明你很有耐心,并且对技术的探讨很有兴趣,那么我就再说得更详细些。在WIN98中,上面说的这个替换是由系统的一个工具来实现的,这个工具叫WININIT.EXE。安装程序在检测到需要写入的DLL或其他程序文件正在使用时,会把要写入的DLL文件先定一个临时的文件名,然后在WINDOWS目录中往WININIT.INI写入一个改写项,比如,一个叫ABCD.DLL的动态连接库现在正在使用中,而安装程序要往系统中写入新版本的ABCD.DLL,这时安装程序会把新版本ABCD.DLL先定一个临时文件名,例如AAAA.LLL,然后在WININIT.INI中的[rename]一节中写入这一项: 篩l罉枓犮
C:\windows\system\abcd.dll=C:\windows\system\aaaa.lll CX=B)
这样,在重启时,进入WINDOWS图形界面之前,WININIT.EXE在检测到WINDOWS目录中有WININIT.INI存在时,就执行里面的操作,在上面的例子中,是用C:\windows\system\aaaa.lll去替换掉C:\windows\system\abcd.dll这个文件,并且把WININIT.INI改名为WININIT.BAK。
另外,有些软件,在安装时,是把所有文件包括SETUP.EXE解压到临时文件里面再执行SETUP.EXE进行安装的,按理来说安装完要把所有的临时文件删除掉,这个操作当然也是由安装程序SETUP.EXE来完成,但它自己正在运行,也删不了它自己,所以也要重启来删除,做法和上面差不多,只是改成类似这样子的: 怦S?vH燁?
NUL=C:\WINDOWS\TEMP\SETUP.EXE
在WIN2K和WINXP中,存在类似的机制,不过并不是用WININIT.EXE和WININIT.INI来实现,具体的做法我也不是很清楚,长期以来我大多数时候都是在用WIN98,所以没认真研究过,但软件安装过程要重启的现象在2K和XP上是仍然存在的,原理也是在重启时替换或修改正在使用的文件,只是实现的方式不同。
最后,我们再来看看有关卸载方面的内容。一般卸载有好几种方式:
1)早期的安装程序,一般会在安装过程记录了上面说的安装过程的1234四个步骤中具体拷贝的文件和DLL以及注册表项,把它保存在INSTALL.LOG之类的文件中,再在软件的安装目录(或WINDOWS目录中)放一个UNINST.EXE之类的卸载程序。然后要么在程序组里为这个UNINST.EXE建一个快捷方式,要么在注册表中为这个UNINST.EXE建一个快捷方式(这诳刂泼姘宓奶砑由境绦蚓湍芸吹饺砑男对叵?,并把INSTALL.LOG做为它的参数,这样就实现卸载了。
2)现在比较多的安装程序是用新版的INSTALLSHIELD生成的,安装时的记录和卸载程序一般是会放在C:\Program Files\InstallShield Installation Information这个文件夹(隐藏属***)里,同样也会在程序组和注册表中建立卸载项。
另外,在卸载时,也会遇到文件(一般是DLL文件)正在使用的情况。所以有时卸载的时候也要重启,就是要在重启过程中删掉这些正在使用的DLL文件。
关于软件的安装过程,大概就想到这里,以后再有想到什么的,我再补充,大家有什么看不懂的也可以把问题提出来。
安装新
软件前,打开注册表编辑器,选择“注册表→导出注册表文件”,利用“全部”选项,将结果文件保存为Before.txt(不要使用REG扩展名)。安装新
软件或进行用户想跟踪的其他任何更改后,打开注册表编辑器,再导出整个注册表,这一次将导出的文件命名为After.txt文件。接着打开MS-DOS命令窗口,转换到有那两个文本文件的目录中,然后执行以下命令:
FC Before.txt After.txt > Diff.txt
关闭DOS窗口,在“记事本”中打开Diff.txt文件,这里会显示在注册表所发现的所有不同之处。
我们在解析配置文件的时候,常常会为路径发愁,我就遇到过这样的情况
如上图所示:
ParseProperties.
java是配置文件database.properties的解析类,那么我们怎样去取得它的路径并解析起配置呢?看解析类ParseProperties的源代码如下:
package zy.pro.sc.db;
import java.util.*;
import java.io.*;
public class ParseProperties {
Properties properties = new Properties();
public ParseProperties() {
try{
this.parseProp();
}catch(Exception e){
e.printStackTrace();
}
}
public Properties parseProp()throws IOException {
InputStream is=this.getClass().getResourceAsStream("database.properties");
/*
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream in = classLoader.getSystemResourceAsStream(fileName);
*/
properties.load(is);
is.close();
return null;
}
public String getProperties(String propStr){
return properties.getProperty(propStr);
}
public static void main(String[] args) {
ParseProperties pp=new ParseProperties();
String driver=pp.getProperties("jdbc.driver");
System.out.println(driver);
}
}
看粗体部分,this.getClass()方法可以得到了当前类的Class对象,也可以用ParseProperties.class.getClass()方法来实现同样的效果。之后调用其getResourceAsStream("database.properties")方法来解析配置文件。getResourceAsStream()方法解析文件时候的相对路径是当前类的包路径。
就当前的包来说,zy.pro.sc.db对应的路径是src/zy/pro/sc/db。由于我们要解析的文件和解析类在同一目录下,所以我们的路径是"database.properties"。
如果我们的解析文件和解析类不在同一目录下呢,如以下目录结构:
那么先看一下我们解析类的代码:
InputStream is=this.getClass().getResourceAsStream("/database.properties");
解析路径变成了"/database.properties", “/“表示取当前类所在的包的根路径下的database.properties文件,也就是相对于ParsePropertie.class的包的根路径下的 database.properties文件。
用这种方法更有灵活性。此地要认真体会。不用这种方法,你的解析类只能在目录结构不发生改变的情况下使用。否则将发生异常。例如:你的解析类在servlet中调用的时候就会抛出找不到文件的异常。
此路径的定位方法也可以使用于解析XML的文件。详细情况同上。
JProfiler是一款Java的性能监控工具。可以查看当前应用的
对象、对象引用、内存、CPU使用情况、线程、线程运行情况(阻塞、等待等),同时可以查找应用内存使用得热点,即:哪个对象占用的内存比较多;或者CPU热点,即:哪儿方法占用的较大得CPU资源。我使用的是4.3.2版本,以前试用过3**版本,不过那个bug比较多,容易死,4**版本稳定多了。
有了上面那些信息对于系统的调优会有很大帮助。这里提供有几篇文章供参考:获取、介绍,简单入门,使用JProfiler解决实际问题。这几篇文章基本介绍了常见东西了,下面说点心得。
- JProfiler监控是要消耗系统资源的,所以一般情况下不要用于性能测试时候的监控。
- 如果要用于相对大压力情况下,可以有选择的打开监控项,不用所有都打开。主要有两个,一个是内存监控,打开的情况下可以查找内存分配热点。一个是CPU监控,打开的情况下可以查看CPU使用热点。
如图所示,红笔标注部分。如果两个都关闭的话,还是可以跑一定压力的,同时还可以监控对象数量。
- 个人认为最好用的(也是用的最多的)是查询当前的对象的数量。数量监控很重要,如果你使用了单例,那么你只会看到有一个对象存在,如果多了就说明程序有问题了。同样,如果应用进行一系列操作,检查一下该销毁的对象是否还继续存在,如果没有释放,就得考虑是否存在内存溢出了。
- JProfiler还提供了一个比较好的检查内存溢出得工具。他可以查找某个对象的引用情况,即:当你发现某个该释放掉的对象没有释放,就可以看一下哪个实例在引用它,找到了根即找到了溢出点。
具体操作如下:在 “Memory Views”界面中右键选择你要监控的对象,选择第一项“Take Heap Snapshot for Selection”,选择完成后会进入“Heap Walker”界面,界面下面提供几个功能,选择“References”即可 。如图:
- JProfiler提供不同的观察粒度,提供对类的监控、对包的监控、对J2EE组件的监控,同时过滤器也比较好用,直接定位你关注的包或类即可。
- JProfiler的监控可能与应用之间存在一定时间差,所以有些时候需要等待刷新,才能显示正确系统情况。
在中间件应用服务器的整体调优中,有关于等待队列、执行线程,EJB池以及数据库连接池和Statement Cache方面的调优,这些都属于系统参数方面的调优,本文主要从另外一个角度,也就是从应用的角度来解决中间件应用服务器的内存泄露问题,从这个角度来提高系统的稳定性和性能。
项目背景
问题描述
某个大型项目(Use Case用例超过300个),在项目上线后,其Web应用服务器经常宕机。表现为:
1. 应用服务器内存长期不合理占用,内存经常处于高位占用,很难回收到低位;
2. 应用服务器极为不稳定,几乎每两天重新启动一次,有时甚至每天重新启动一次;
3. 应用服务器经常做Full GC(Garbage Collection),而且时间很长,大约需要30-40秒,应用服务器在做Full GC的时候是不响应客户的交易请求的,非常影响系统性能。
Web应用服务器的物理部署
一台Unix服务器(4CPU,8G Memory)来部署本Web应用程序;Web应用程序部署在中间件应用服务器上;部署了一个节点(Node),只配置一个应用服务器实例(Instance),没有做Cluster部署。
Web应用服务器启动脚本中的内存参数
MEM_ARGS="-XX:MaxPermSize=128m -XX:MaxNewSize=512m -Xms3096m
-Xmx3096m -XX:+Printetails -Xloggc:./inwebapp1/gc.$$" |
可以看出目前生产系统中Web应用服务器的内存分配为3G Memory。
Web应用服务器的重要部署参数
参数名称 |
参数值 |
参数解释 |
kernel.default(Thread Count) |
120 |
执行线程数目,是并发处理能力的重要参数 |
Session Timeout |
240分钟(4小时) |
HttpSession会话超时 |
分析
分析方法
内存长期占用并导致系统不稳定一般有两种可能:
1. 对象被大量创建而且被缓存,在旧的对象释放前又有大量新的对象被创建使得内存长期高位占用。
- 表现为:内存不断被消耗、在高位时也很难回归到低位,有大量的对象在不断的创建,经过很长时间后又被回收。例如:在HttpSession中保存了大量的分页查询数据,而HttpSession的会话超时时间设置过长(例如:1天),那么在旧的对象释放前又有大量新的对象在第二天产生。
- 解决办法:对共享的对象可以采用池机制进行缓存,避免各自创建;缓存的临时对象应该及时释放;另一种办法是扩大系统的内存容量。
2. 另一种情况就是内存泄漏问题
- 表现为:内存回收低位点不断升高(以每次内存回收的最低点连成一条直线,那么它是一条上升线);内存回收的频率也越来越高,内存占用也越来越高,最终出现"Out of Memory Exception"的系统异常。
- 解决办法:定位那些有内存泄漏的类或对象并修改完善这些类以避免内存泄漏。方法是:经过一段时间的测试、监控,如果某个类的对象数目屡创新高,即使在JVM Full GC后仍然数目降不下来,这些对象基本上是属于内存泄漏的对象了。
问题定位
这里请看5月份 Web应用服务器的内存回收图形:
《注意:5月18日早上10点重新启动了Web服务器,5月20日早上又重新启动了Web服务器。》
- 在Web应用重要部署参数中,我们知道:Session的超时时间为4个小时,我们在监控平台也观测到:在18日晚上10点左右所有的会话都过期了,从图形一中也能看出18日晚上确实系统的内存有回收到40%(就象股票的高位跳水);
- 从图形一(5月18日)中我们也能看到Full GC回收后的内存占用率走势(红色曲线),上午基本平滑上升到20%(内存占用率),中午开始上升到30%,下午上升到40%
- 从图形二(5月19日)中我们也能看到Full GC回收后的内存占用率走势(红色曲线),上午又上升到了60%,到下午上升到了70%。
- 从黄色曲线(GC花费的时间,以秒为单位),Full GC的频率也在增快,时间耗费也越来越长,在图形一中基本高位在20秒左右,到19日基本都是30-40秒之间了。
图形一 5月18日
图二
通过上述分析,我们基本定位到了Web应用服务器的内存在高位长期占用的原因了:是内存泄露!并且正是由于这个原因导致系统不稳定、响应客户请求越来越慢的。
解决方法
方法如下:
- 我们从图形二中发现,在8.95(将近9点钟)到9.66(将近9点40)期间有几次Full GC,但是有内存泄漏,从占用率40%上升到50%左右,泄漏了大约10%的内存,约300M;
- 我们在自己搭建的Web应用服务器平台(应用软件版本和生产版本一致)做这一阶段相同的查询交易;表明对同一个黑盒(Web应用)施加同样的刺激(相同的操作过程和查询交易)以期重现现象;
- 我们使用Jprofiler工具对Web应用服务器的内存进行实时监控;
- 做完这些交易后,用户退出系统,并等待Web应用服务器的HttpSession超时(我们这里设置为15分钟);
- 我们对Web应用服务器做了两次强制性的内存回收操作。
发现如下:
图三
如图三所示,内存经过HttpSession超时后,并强制gc后,仍然有大量的对象没有释放。例如:gov.gdlt.taxcore.comm.security.MenuNode,仍然有807个实例没有释放。
我们继续追溯发现,这些MenuNode首先存放在一个ArrayList对象中,然后发现这个ArrayList对象又是存放在WHsessionAttrVO对象的Map中,WHsessionAttrVO 对象又是存放在ExternalSessionManager的staic Map中(名称为sessionMap),如图四所示。
图四
我们发现gov.gdlt.taxcore.taxevent.xtgl.comm.WHsessionAttrVO中保存了EJBSessionId信息(登录用户的唯一标志,由用户id+登录时间戳组成,每天都不同)和一个HashMap,这个HashMap中的内容有:
- ArrayList: 内有MenuTreeNodes(菜单树节点)
- HashMap: 内有操作人员代码信息
- CurrentVersion:当前版本号
- CurrentTime:当前系统时间
WHsessionAttrVO这个对象的最终存放在ExternalSessionManager的static Map sessionMap中,由于ExternalSessionManager是一个全局的单实例,不会释放,所以它的成员变量sessionMap中的数据也不会释放,而Map中的Key值为EJBSessionId,每天登录的用户EJBSessionId都不同,就造成了每天的登录信息(包括菜单信息)都保存在sessionMap中不会被释放,最终造成了内存的泄漏。
图五
如上图所示:WHsessionAttrsVO对象中除了有一个String对象(内容是EJBSessionId),还有一个HashMap对象。
图六
如上图所示,这个HashMap中的内容主要有menuTreeNodes为key,value为ArrayList的对象和以czrydminfo为key,value为HashMap对象的数据。
图七
如上图所示:menuTreeNodes为key,value为ArrayList对象中包含的对象有许多的MenuNode对象,封装的都是用户的菜单节点。
图八
如上图所示,最顶层(Root)的初始对象为一个ExternalSessionManager对象,其中的一个成员变量为static (静态的),名称为:sessionMap,这个对象是singleton方式的,全局只有一个。
初步估量
我们从图形一和图形二中可以看出,每天应用服务器损失大约40%的内存,大约1G左右。
从图形四可以看出,当前用户(Id=24400001129)有807个菜单项(每个菜单项为一个MenuNode 对象实例,图形四中的这个实例的size为592 Byte),这些菜单数据和用户基本登录信息(czrydmInfo HashMap)也都存放在WHsessionAttrVO对象中,当前这个WHsessionAttrVO对象的size为457K。
我们做如下估算:
假设平均每天有4千人(估计值,这个数值仅仅是5月19日峰值的1/2左右)登录系统(有重复登录的现象,例如:上午登录一次,中午退出系统,下午登录一次),以平均每人占用200K(估计值,是用户id=24400001129 的Size的1/2左右)来计算,一天泄漏的内存约800M,比较符合目前内存泄漏的情况。当然,这种估计仍然需要经过实践的检验,方法是:当这次发现的内存泄漏问题解决后看系统是否还有其它内存泄漏问题。
方案
ExternalSessionManager类是当初某某软件商设计的用来解决Web服务器负载均衡的模块,这个类主要用来保存客户的基本登录信息(包括会话的EJBSessionId),以维护多个Web服务器之间的会话信息一致。
改进方案有两种:
-
从架构设计方面改进
实现Web层的负载均衡有很多标准的实现方式。例如:采用负载均衡设备(硬件或软件)来实现。
如果采用新的Web层的负载均衡方式,那么就可以去掉ExternalSessionManager这个类了。
-
从应用实现方面改进
保留当前的Web层的负载均衡设计机制,仅仅从应用实现方面解决内存泄漏问题,首先菜单信息不应该保存在ExternalSessionManager中。其次,增加对ExternalSessionManager类中用户会话登录信息的清除,有几种方式可以选择:
- 被动方式,当HttpSession会话超时(或过期)被Web应用服务器回收时清除相应的ExternalSessionManager中的过期会话登录信息。
- 主动方式,可以采用任务定时清理每天的过期会话登录信息或线程轮询清理。
- 采用新的会话登录信息存储方式,ExternalSessionManager的sessionMap中的key值不再以EJBSessionId作为键值,而是以用户id(EJBSessionId的前11位)代替。由于用户id每天都是一样的,所以不会造成内存泄漏。保存得登录信息也不再包含菜单节点信息,而只是登录基本信息。最多也只是保存整个系统所有的用户id及其基本登录信息(大约每个用户的登录信息只有1.5K左右,而目前这个系统的营业网点用户为1万左右,所以大约只占用Web服务器15M内存)。
实施情况
采用的方案:某某软件商采用了新的会话登录信息存贮方案,即:ExternalSessionManager的成员变量sessionMap中不再保存用户菜单信息,只保存基本的登录信息;存储方式采用用户id(11位)作为键值(key)来保留用户基本登录信息。
基本分析:由于基本登录信息只有1K左右,而目前内网登录的用户总数也只有8887个,所以只保存了大约10M-15M的信息在内存,占用量很小,并且不会有内存泄漏。用户菜单信息保存在session中,如果用户退出时点击logout页面,那么应用服务器可以很快地释放这部分内存;如果用户直接关闭窗口,那么保存在session中的菜单信息只有等会话超时后才会由系统清除并回收内存。
监控状况:
图九
如图九所示,ExternalSessionManager中只保留了简单的登录信息(Map中保存了WHsessionAttrVO对象),包括:当前版本(currentversion),操作人员代码基本信息(czrydmInfo),当前时间(currenttime)。
图十
如图十所示,这个登录用户的基本信息只有1368 bytes,大约1.3K
图十一
如图十一所示,一共有两个用户(相同的用户id)登录系统,当一个用户使用logout页面退出时,保留在session中的菜单信息(MenuNode)立刻释放了,所以Difference一栏减少了806个菜单项。
图十二
如图十二所示,当另外一个会话超时后,应用服务器回收了整个会话的菜单信息(MenuNode),图上已经没有MenuNode对象了。并且由于是同一个用户登录,所以保留在ExternalSessionManager成员变量sessionMap中的对象WHsessionAttrVO只有一个(id=24400001129),而没有产生多个,没有因为多次登录而产生多个对象的后果,避免了内存泄漏问题的出现,解决了前期定位的内存泄漏问题。
图十三
如图十三所示,经过gc内存回收后,发现内存回收比较稳定,基本都回收到了最低点,也证明了内存没有泄露。
结论与建议:从测试情况看,解决了前期定位的内存泄漏问题。
生产系统实施后的监控与分析
经过调优后,我们发现:在2005年6月2日晚9点40左右重新部署、启动了Web应用服务器(采用了新的调优方案)。经过几天的监控运行,发现Web应用服务器目前运行基本稳定,目前没有出现新的内存泄漏问题,下列图示说明了这一点
图十四 2005年6月2日
如图十四所示,6月2日晚21.7(21点42分)重新启动应用服务器,内存占用很少,大约为15%(请看红色曲线),每次GC消耗的时间也很短,大约在5秒以内(请看黄色曲线)。
图十五 2005年6月3日周五
如图十五所示,在6月3日周五的整个工作日内,内存的回收基本到位,回收位置控制在20%-30%之间,也就是在600M-900M之间(请看红色曲线的最低点),始终可以回收2G的内存供应用程序使用,每次GC的时间最高不超过20秒,Full GC平均在10秒左右,时间消耗比较短(请看黄色曲线)。
图十六2005年6月5日周日
如图十六所示,在周日休息日期间,Web应用服务器全天只做了大约4次Full GC(黄色曲线中的小山峰),时间都在10秒以内;大的Full GC后,内存只占用10%,内存回收很彻底。
图十七 2005年6月6日周一
如图十七所示,在周一工作日期间,内存回收还是不错的,基本可以回收到30%(见红色曲线的最低点),即:占用900M内存空间,剩余2G的内存空间;Full GC的时间大部分控制在20秒以内,平均15秒(见黄色曲线)。
图十八 2005年6月7日周二
如图十八所示,在6月7日周二早上,大约8:30左右,Web应用服务器作了一次Full GC,用了10秒的时间,把内存回收到了10%的位置,为后续的使用腾出了90%的内存空间。内存回收仍然比较彻底,说明基本没有内存泄漏问题。
经过这几天的监控分析,我们可以看出:
- Web应用服务器的内存使用已经比较合理,内存在工作日的占用在20%至30%之间,约1G的内存占用,有2G的内存空间富裕;而在空闲时间(周日,每天的凌晨等)内存可以回收到10%,有90%的内存空间富裕;
- Web应用服务器的Full GC的次数明显减少了并且每次Full GC占用的时间也很少,基本控制在10-20秒之间,有的甚至在10秒以内,明显改善了内网应用服务器内存的使用;
- 从6月2日重新部署之后,Web应用服务器没有出现宕机重启的现象。
总结
通过本文,我们可以看到,内存的泄露将会导致服务器的宕机,系统性能就更别说了。对于系统内存泄露问题应该从服务器GC日志方面进行早诊断,使用工具早确认并提出解决方案,排除内存泄露问题,提高系统性能,以规避项目风险。
Sample1,利用Menifest文件读取jar中的文件
/*
1.文件目录
test--
--a.text
--b.gif
2. Menifest文件内容:
Manifest-Version: 1.0
abc: test/a.txt
iconname: test/Anya.jpg
注意:manifest.mf文件最后一行要打一回车
Another Notification:
如果manifest文件内容是:
Manifest-Version: 1.0
Main-Class: com.DesignToolApp
Class-path: lib/client.jar lib/j2ee.jar
在MANIFEST.MF文件的最后,要留两个空行(也就是回车),才可以识别到Class-Path这一行,如果只有一个空行,那么只识别到Main-Class这一行。Class-Path中的库名用空格格开,使用和jar包相对的路径,发布时把jar包和其他用到的类库一起交给用户就可以了。
3.打jar包
test.jar
*/
String iconpath = jar.getManifest().getMainAttributes().getValue("abc");
InputStream in = jar.getInputStream(jar.getJarEntry(iconpath));
//Image img = ImageIO.read(in);
InputStreamReader isr = new InputStreamReader(in);
BufferedReader reader = new BufferedReader(isr);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
Sample2,读取JAR 文件列表及各项的名称、大小和压缩后的大小
public class JarFileInfoRead {
public static void main (String args[])
throws IOException {
String jarpath="d://temp//test.jar";
JarFile jarFile = new JarFile(jarpath);
Enumeration enu = jarFile.entries();
while (enu.hasMoreElements()) {
process(enu.nextElement());
}
}
private static void process(Object obj) {
JarEntry entry = (JarEntry)obj;
String name = entry.getName();
long size = entry.getSize();
long compressedSize = entry.getCompressedSize();
System.out.println(name + "\t" + size + "\t" + compressedSize);
}
}
Sample3,读取JAR中 文件的内容
public class JarFileRead {
public static void main (String args[])
throws IOException {
String jarpath="d://temp//test.jar";
JarFile jarFile = new JarFile(jarpath);
Enumeration enu = jarFile.entries();
while (enu.hasMoreElements()) {
JarEntry entry = (JarEntry)enu.nextElement();
String name = entry.getName();
//System.out.println(name);
if(name.equals("test/a.txt")){
InputStream input = jarFile.getInputStream(entry);
process(input);
}
}
jarFile.close();
}
private static void process(InputStream input)
throws IOException {
InputStreamReader isr =
new InputStreamReader(input);
BufferedReader reader = new BufferedReader(isr);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
}
}