内存溢出对java应用来说实在是太平常了,有以下两种可能。
对象太多
代码写的很烂,产生的对象太多,内存被耗完
业务需求,一个考试系统,登陆后需要填写一下信息:
考试科目,选择框
package com.example.xpeng.myapplication;
/**
* Created by xpeng on 2018/7/8.
*/
public class SignInfo {
//报名人员的ID
private String id;
//考试地点
private String location;
//考试科目
private String subject;
//邮寄地址
private String postAddress;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getPostAddress() {
return postAddress;
}
public void setPostAddress(String postAddress) {
this.postAddress = postAddress;
}
}
它是一个很简单的POJO对象(Plain Ordinary Java Object,简单Java对象)。我们再来看工厂类,代码如下:
public class SignInfoFactory {
//报名信息的对象工厂
public static SignInfo getSignInfo(){
return new SignInfo();
}
}
工厂类就这么简单?非也,这是我们的教学代码,真实的ObjectFactory要复杂得多,主要是注入了部分Handler的管理。表现层是如何创建对象的,代码如下:
public class Client {
public static void main(String[] args) {
//从工厂中获得一个对象
SignInfo signInfo = SignInfoFactory.getSignInfo();
//进行其他业务处理
}
}
就这么简单,但是简单为什么会出现问题呢?而且这样写也没有问题啊,很标准的工厂方法模式,应该不会有大问题。定位分析是内存对象太多,那应该想到使用一种共享的技术减少对象数量,那怎么共享呢?
对象池的实现有很多开源工具,我们自己来设计一个共享对象池,需要实现如下两个功能。
package com.example.xpeng.myapplication;
/**
* Created by xpeng on 2018/7/8.
*/
public class SignInfo4Pool extends SignInfo {
//定义一个对象池提取的KEY的值
private String key;
//构造函数获取相同标志
public SignInfo4Pool(String key){
this.key = key;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
很简单,就是增加了一个key值,为什么要增加key值?为什么要使用子类,而不再SignInfo类上做修改?我们刚刚分析了所有的SignInfo对象都有一些共同的属性:考试科目和考试地点,我们把这些共性提取出来作为所有对象的外部状态,这个对象池中一个具体的外部状态只有一个对象。按照这个设计,我们定义key值的标准为:考试科目+考试地点的符合字符串作为唯一的池对象标准,也就是说在对象池中,一个key值唯一对应一个对象。
注意:
在对象池中,对象一旦产生,必然有一个唯一的、可访问的状态标志该对象,而且池中的对象声明周期是由池容器决定,而不是由使用者决定的。
你可能马上就要提出了,为什么不建立一个新的类,包含subject和location两个属性作为外部状态呢?嗯,这是一个办法,但不是最好的办法,有两个原因:
package com.example.xpeng.myapplication;
import android.util.Log;
import java.util.HashMap;
/**
* Created by xpeng on 2018/7/8.
*/
public class SignInfoFactory {
//池容器
private static HashMap pool = new HashMap<>();
//报名信息的对象工厂
@Deprecated
public static SignInfo getSignInfo(){
return new SignInfo();
}
//报名信息的对象工厂
public static SignInfo getStignInfo(String key){
//设置返回对象
SignInfo result = null;
//池中没有该对象,则建立,并放入池中
if (!pool.containsKey(key)){
Log.e("xyz","---建立对象,并放置到池中");
result = new SignInfo4Pool(key);
pool.put(key,result);
}else{
result = pool.get(key);
Log.e("xyz","--直接从池中取的");
}
return result;
}
}
方法都很简单,不多解释。读者需要注意一点的是@Deprecated注解,不要有删除投产中代码的念头,如果方法或类确实不再使用了,增加该注解,表示该方法或类已经过时,尽量不要在使用了,我们应该保持历史原貌,同事也有助于版本向下兼容,特别是在产品研发中。
我们再来看看客户端是如何调用的,如下代码:
private void Client() {
//初始化对象池
for (int i = 0; i < 4; i++) {
String subject = "科目" + i;
//初始化地址
for (int j = 0; j < 30; j++) {
String key = subject + "考试地点" + j;
SignInfoFactory.getStignInfo(key);
}
}
SignInfo signInfo = SignInfoFactory.getStignInfo("科目1考试地点1");
}
这就是享元模式
享元模式的定义
享元模式是池技术的重要实现方式,其定义如下:使用共享对象可有效的支持大量的细粒度的对象
享元模式的定义为我们提供了两个要求:细粒度的对象和共享对象。我们知道分配太多的对象到应用程序中将有损程序的性能,同时还容易造成内存溢出,那怎么避免呢?就是享元模式提到的共享技术。我们先来了解一下对象都内部状态和外部状态
要求细粒度对象,那么不可避免的使得对象数量多且性质相近,我们就将这些对象的信息分为两个部分:内部状态和外部状态
内部状态
内部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态,如我们例子中的考试科目+考试地点复合字符串,它是一批对象的统一标识,是唯一的一个索引值。
有了对象的两个状态,我们就可以来看享元模式的通用类图,如下:
类图也很简单,我们先来看我们享元模式角色名称
Flyweight——抽象享元角色
它简单的说就是一个产品的抽象类,同事定义出对象的外部状态和内部状态的接口或实现。
ConcreateFlyweight——具体享元角色
具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。
unsharedConcreateFlyweight——不可共享的享元角色
不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。
package com.example.xpeng.myapplication;
public abstract class Flyweight {
//内部状态
private String intrinsic;
//外部状态
protected final String Extrinsic;
//要求享元角色必须接受外部状态
public Flyweight(String extrinsic) {
Extrinsic = extrinsic;
}
//定义业务操作
public abstract void operate();
//内部状态的getter/setter
public String getIntrinsic() {
return intrinsic;
}
public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}
}
抽象享元角色一般为抽象类,在实际项目中,一般是一个实现类,它是描述一类事物的方法。在抽象角色中,一般需要把外部状态和内部状态(当然了,可以没有内部状态,只有行为也是可以的)定义出来,避免子类的随意扩展。我们再来看看具体的享元角色,代码如下:
package com.example.xpeng.myapplication;
public class ConcreateFlyweight1 extends Flyweight {
//接受外部状态
public ConcreateFlyweight1(String extrinsic) {
super(extrinsic);
}
//根据外部状态进行逻辑处理
@Override
public void operate() {
}
}
package com.example.xpeng.myapplication;
public class ConcreateFlyweight2 extends Flyweight {
//接受外部状态
public ConcreateFlyweight2(String extrinsic) {
super(extrinsic);
}
//根据外部状态进行逻辑处理
@Override
public void operate() {
}
}
这很简单,实现自己的业务逻辑,然后接收外部状态,以便内部业务逻辑对外部状态的依赖。注意,我们在抽象享元中对外部状态加上了final关键字,防止意外发生,什么意外?获得了一个外部状态,然后无意修改了一下,池就混乱了!
注意 在程序开发中,确认只需要一次赋值的属性则设置为final类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量。
我们继续看享元工厂,代码如下:
package com.example.xpeng.myapplication;
import java.util.HashMap;
public class FlyweightFactory {
//定义一个容器池
private static HashMap pool = new HashMap<>();
//享元工厂
public static Flyweight getFlyweight(String Extrinsic){
//需要返回的对象
Flyweight flyweight = null;
//在池中没有该对象
if (pool.containsKey(Extrinsic)){
flyweight = pool.get(Extrinsic);
}else {
//根据外部状态创建享元对象
flyweight = new ConcreateFlyweight1(Extrinsic);
//放置在池中
pool.put(Extrinsic,flyweight);
}
return flyweight;
}
}
享元模式的应用
(1)享元模式的优缺点
享元模式是一个非常简单的模式,它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但是同时也提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱
(2)享元模式的使用场景
在如下场景中可以选择使用享元模式
需要缓冲池的场景
享元模式的扩展
(1)线程安全的问题
线程安全是一个老生常谈的话题,只要使用JAVA开发否会遇到这个问题,我们之所以要在今天的享元模式中提到该问题,是因为该模式有太大的几率发生线程不安全,为什么呢?
设置的享元对象数量太少,导致每个线程都到对象池中获取对象,然后都去修改其属性,于是就出现一些不和谐数据。我们在使用享元模式时,对象池中的享元对象尽量多,多到足够满足业务为止。
(2)性能平衡
尽量使用Java基本类型作为外部状态。
享元模式DEMO下载地址:
demo下载
设计模式之禅demo