iv.共享内存:
在Java语言里面,没有共享内存的概念,但是在某些引用中,共享内存却很受用,例如Java语言的分布式系统,存着大量的Java分布式共享对象,很多时候需要查询这些对象的状态,以查看系统是否运行正常或者了解这些对象目前的一些统计数据和状态。如果使用的是网络通信的方式,显然会增加应用的额外开销,也增加了不必要的应用编程,如果是共享内存方式,则可以直接通过共享内存查看到所需要的对象的数据和统计数据,从而减少一些不必要的麻烦。
1)共享内存特点:
- 可以被多个进程打开访问
- 读写操作的进程在执行读写操作的时候其他进程不能进行写操作
- 多个进程可以交替对某一个共享内存执行写操作
- 一个进程执行了内存写操作过后,不影响其他进程对该内存的访问,同时其他进程对更新后的内存具有可见性
- 在进程执行写操作时如果异常退出,对其他进程的写操作禁止自动解除
- 相对共享文件,数据访问的方便性和效率
2)出现情况:
- 独占的写操作,相应有独占的写操作等待队列。独占的写操作本身不会发生数据的一致性问题;
- 共享的写操作,相应有共享的写操作等待队列。共享的写操作则要注意防止发生数据的一致性问题;
- 独占的读操作,相应有共享的读操作等待队列;
- 共享的读操作,相应有共享的读操作等待队列;
3)Java中共享内存的实现:
JDK 1.4里面的MappedByteBuffer为开发人员在Java中实现共享内存提供了良好的方法,该缓冲区实际上是一个磁盘文件的内存映象,二者的变化会保持同步,即内存数据发生变化过后会立即反应到磁盘文件中,这样会有效地保证共享内存的实现,将共享文件和磁盘文件简历联系的是文件通道类:FileChannel,该类的加入是JDK为了统一外围设备的访问方法,并且加强了多线程对同一文件进行存取的安全性,这里可以使用它来建立共享内存用,它建立了共享内存和磁盘文件之间的一个通道。打开一个文件可使用RandomAccessFile类的getChannel方法,该方法直接返回一个文件通道,该文件通道由于对应的文件设为随机存取,一方面可以进行读写两种操作,另外一个方面使用它不会破坏映象文件的内容。这里,如果使用FileOutputStream和FileInputStream则不能理想地实现共享内存的要求,因为这两个类同时实现自由读写很困难。
下边代码段实现了上边提及的共享内存功能
// 获得一个只读的随机存取文件对象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 获得相应的文件通道
FileChannel fc = RAFile.getChannel();
// 取得文件的实际大小
int size = (int)fc.size();
// 获得共享内存缓冲区,该共享内存只读
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);
// 获得一个可读写的随机存取文件对象
RAFile = new RandomAccessFile(filename,"rw");
// 获得相应的文件通道
fc = RAFile.getChannel();
// 取得文件的实际大小,以便映像到共享内存
size = (int)fc.size();
// 获得共享内存缓冲区,该共享内存可读写
mapBuf = fc.map(FileChannel.MAP_RW,0,size);
// 获取头部消息:存取权限
mode = mapBuf.getInt();
如果多个应用映象使用同一文件名的共享内存,则意味着这多个应用共享了同一内存数据,这些应用对于文件可以具有同等存取权限,一个应用对数据的刷新会更新到多个应用中。为了防止多个应用同时对共享内存进行写操作,可以在该共享内存的头部信息加入写操作标记,该共享文件的头部基本信息至少有:
共享文件的头部信息是私有信息,多个应用可以对同一个共享内存执行写操作,执行写操作和结束写操作的时候,可以使用如下方法:
public boolean startWrite()
{
if(mode == 0) // 这里mode代表共享内存的存取模式,为0代表可写
{
mode = 1; // 意味着别的应用不可写
mapBuf.flip();
mapBuf.putInt(mode); //写入共享内存的头部信息
return true;
}
else{
return false; //表明已经有应用在写该共享内存了,本应用不能够针对共享内存再做写操作
}
}
public boolean stopWrite()
{
mode = 0; // 释放写权限
mapBuf.flip();
mapBuf.putInt(mode); //写入共享内存头部信息
return true;
}
【*:上边提供了对共享内存执行写操作过程的两个方法,这两个方法其实理解起来很简单,真正需要思考的是一个针对存取模式的设置,其实这种机制和最前面提到的内存的锁模式有点类似,一旦当mode(存取模式)设置称为可写的时候,startWrite才能返回true,不仅仅如此,某个应用程序在向共享内存写入数据的时候还会修改其存取模式,因为如果不修改的话就会导致其他应用同样针对该内存是可写的,这样就使得共享内存的实现变得混乱,而在停止写操作stopWrite的时候,需要将mode设置称为1,也就是上边注释段提到的释放写权限。】
关于锁的知识这里简单做个补充【*:上边代码的这种模式可以理解为一种简单的锁模式】:一般情况下,计算机编程中会经常遇到锁模式,在整个锁模式过程中可以将锁分为两类(这里只是辅助理解,不是严格的锁分类)——共享锁和排他锁(也称为独占锁),锁的定位是定位于针对所有与计算机有关的资源比如内存、文件、存储空间等,针对这些资源都可能出现锁模式。在上边堆和栈一节讲到了Java对象锁,其实不仅仅是对象,只要是计算机中会出现写入和读取共同操作的资源,都有可能出现锁模式。
共享锁——当应用程序获得了资源的共享锁的时候,那么应用程序就可以直接访问该资源,资源的共享锁可以被多个应用程序拿到,在Java里面线程之间有时候也存在对象的共享锁,但是有一个很明显的特征,也就是内存共享锁只能读取数据,不能够写入数据,不论是什么资源,当应用程序仅仅只能拿到该资源的共享锁的时候,是不能够针对该资源进行写操作的。
独占锁——当应用程序获得了资源的独占锁的时候,应用程序访问该资源在共享锁上边多了一个权限就是写权限,针对资源本身而言,一个资源只有一把独占锁,也就是说一个资源只能同时被一个应用或者一个执行代码程序允许写操作,Java线程中的对象写操作也是这个道理,若某个应用拿到了独占锁的时候,不仅仅可以读取资源里面的数据,而且可以向该资源进行数据写操作。
数据一致性——当资源同时被应用进行读写访问的时候,有可能会出现数据一致性问题,比如A应用拿到了资源R1的独占锁,B应用拿到了资源R1的共享锁,A在针对R1进行写操作,而两个应用的操作——A的写操作和B的读操作出现了一个时间差,s1的时候B读取了R1的资源,s2的时候A写入了数据修改了R1的资源,s3的时候B又进行了第二次读,而两次读取相隔时间比较短暂而且初衷没有考虑到A在B的读取过程修改了资源,这种情况下针对锁模式就需要考虑到数据一致性问题。独占锁的排他性在这里的意思是该锁只能被一个应用获取,获取过程只能由这个应用写入数据到资源内部,除非它释放该锁,否则其他拿不到锁的应用是无法对资源进行写入操作的。
按照上边的思路去理解代码里面实现共享内存的过程就更加容易理解了。
如果执行写操作的应用异常中止,那么映像文件的共享内存将不再能执行写操作。为了在应用异常中止后,写操作禁止标志自动消除,必须让运行的应用获知退出的应用。在多线程应用中,可以用同步方法获得这样的效果,但是在多进程中,同步是不起作用的。方法可以采用的多种技巧,这里只是描述一可能的实现:采用文件锁的方式。写共享内存应用在获得对一个共享内存写权限的时候,除了判断头部信息的写权限标志外,还要判断一个临时的锁文件是否可以得到,如果可以得到,则即使头部信息的写权限标志为1(上述),也可以启动写权限,其实这已经表明写权限获得的应用已经异常退出,这段代码如下:
// 打开一个临时文件,注意统一共享内存,该文件名必须相同,可以在共享文件名后边添加“.lock”后缀
RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
// 获取文件通道
FileChannel lockFileChannel = files.getChannel();
// 获取文件的独占锁,该方法不产生任何阻塞直接返回
FileLock fileLock = lockFileChannel.tryLock();
// 如果为空表示已经有应用占有了
if( fileLock == null ){
// ...不可写
}else{
// ...可以执行写操作
}
4)共享内存的应用:
在Java中,共享内存一般有两种应用:
[1]永久对象配置——在java服务器应用中,用户可能会在运行过程中配置一些参数,而这些参数需要永久 有效,当服务器应用重新启动后,这些配置参数仍然可以对应用起作用。这就可以用到该文 中的共享内存。该共享内存中保存了服务器的运行参数和一些对象运行特性。可以在应用启动时读入以启用以前配置的参数。
[2]查询共享数据——一个应用(例 sys.java)是系统的服务进程,其系统的运行状态记录在共享内存中,其中运行状态可能是不断变化的。为了随时了解系统的运行状态,启动另一个应用(例 mon.java),该应用查询该共享内存,汇报系统的运行状态。
v.小节:
提供本机内存以及共享内存的知识,主要是为了让读者能够更顺利地理解JVM内部内存模型的物理原理,包括JVM如何和操作系统在内存这个级别进行交互,理解了这些内容就让读者对Java内存模型的认识会更加深入,而且不容易遗忘。其实Java的内存模型远不及我们想象中那么简单,而且其结构极端复杂,看过《Inside JVM》的朋友应该就知道,结合JVM指令集去写点小代码测试.class文件的里层结构也不失为一种好玩的学习方法。
4.防止内存泄漏
Java中会有内存泄漏,听起来似乎是很不正常的,因为Java提供了垃圾回收器针对内存进行自动回收,但是Java还是会出现内存泄漏的。
i.什么是Java中的内存泄漏:
在Java语言中,内存泄漏就是存在一些被分配的对象,这些对象有两个特点:这些对象可达,即在对象内存的有向图中存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象了。如果对象满足这两个条件,该对象就可以判定为Java中的内存泄漏,这些对象不会被GC回收,然而它却占用内存,这就是Java语言中的内存泄漏。Java中的内存泄漏和C++中的内存泄漏还存在一定的区别,在C++里面,内存泄漏的范围更大一些,有些对象被分配了内存空间,但是却不可达,由于C++中没有GC,这些内存将会永远收不回来,在Java中这些不可达对象则是被GC负责回收的,因此程序员不需要考虑这一部分的内存泄漏。二者的图如下:
因此按照上边的分析,Java语言中也是存在内存泄漏的,但是其内存泄漏范围比C++要小很多,因为Java里面有个特殊程序回收所有的不可达对象:垃圾回收器。对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低,JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。
举个例子:
——[$]内存泄漏的例子——
package org.susan.java.collection;
import java.util.Vector;
public class VectorMemoryLeak {
public static void main(String args[]){
Vector<String> vector = new Vector<String>();
for( int i = 0; i < 1000; i++ ){
String tempString = new String();
vector.add(tempString);
tempString = null;
}
}
}
从上边这个例子可以看到,循环申请了String对象,并且将申请的对象放入了一个Vector中,如果仅仅是释放对象本身,因为Vector仍然引用了该对象,所以这个对象对CG来说是不可回收的,因此如果对象加入到Vector后,还必须从Vector删除才能够回收,最简单的方式是将Vector引用设置成null。实际上这些对象已经没有用了,但是还是被代码里面的引用引用到了,这种情况GC拿它就没有了任何办法,这样就可以导致了内存泄漏。
【*:Java语言因为提供了垃圾回收器,照理说是不会出现内存泄漏的,Java里面导致内存泄漏的主要原因就是,先前申请了内存空间而忘记了释放。如果程序中存在对无用对象的引用,这些对象就会驻留在内存中消耗内存,因为无法让GC判断这些对象是否可达。如果存在对象的引用,这个对象就被定义为“有效的活动状态”,同时不会被释放,要确定对象所占内存被回收,必须要确认该对象不再被使用。典型的做法就是把对象数据成员设置成为null或者中集合中移除,当局部变量不需要的情况则不需要显示声明为null。】
ii.常见的Java内存泄漏
1)全局集合:
在大型应用程序中存在各种各样的全局数据仓库是很普遍的,比如一个JNDI树或者一个Session table(会话表),在这些情况下,必须注意管理存储库的大小,必须有某种机制从存储库中移除不再需要的数据。
[$]解决:
[1]常用的解决方法是周期运作清除作业,该作业会验证仓库中的数据然后清楚一切不需要的数据
[2]另外一种方式是反向链接计数,集合负责统计集合中每个入口的反向链接数据,这要求反向链接告诉集合合适会退出入口,当反向链接数目为零的时候,该元素就可以移除了。
2)缓存:
缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果进行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需要将所使用的内存容量与检索数据的速度加以平衡。
[$]解决:
[1]常用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存,这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。
3)类加载器:
Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与"常规"对象引用有关,同时也和对象内部的引用有关。比如数据变量,方法和各种类。这意味着只要存在对数据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。既然类装载器可以同很多的类关联,同时也可以和静态数据变量关联,那么相当多的内存就可能发生泄漏。
iii.Java引用【摘录自前边的《Java引用总结》】:
Java中的对象引用主要有以下几种类型:
1)强可及对象(strongly reachable):
可以通过强引用访问的对象,一般来说,我们平时写代码的方式都是使用的强引用对象,比如下边的代码段:
StringBuilder builder= new StringBuilder();
上边代码部分引用obj这个引用将引用内存堆中的一个对象,这种情况下,只要obj的引用存在,垃圾回收器就永远不会释放该对象的存储空间。这种对象我们又成为强引用(Strong references),这种强引用方式就是Java语言的原生的Java引用,我们几乎每天编程的时候都用到。上边代码JVM存储了一个StringBuilder类型的对象的强引用在变量builder呢。强引用和GC的交互是这样的,如果一个对象通过强引用可达或者通过强引用链可达的话这种对象就成为强可及对象,这种情况下的对象垃圾回收器不予理睬。如果我们开发过程不需要垃圾回器回收该对象,就直接将该对象赋为强引用,也是普通的编程方法。