看看享元模式给你的程序减少了多少内存

简介: 看看享元模式给你的程序减少了多少内存

举个例子

对象创建是OOP中最基本的操作。即使在最微不足道的用例中,也很难计算我们创建的对象的数量(有意或幕后)。

每个对象都是在堆上创建的,在垃圾收集之前都会占用一些空间。长时间运行的程序会占用堆。类似地,同时运行的线程将成倍增加所使用的内存。

下面举一个简单的例子:

我有一个应用程序,它返回给我大量的数据点来绘制一个图表。数据点包含两个信息——数据和该点在图上的样子:

public class DataPoint {
    private double data;
    private Point point;

    public DataPoint(double data, Point point) {
        this.data = data;
        this.point = point;
    }

    public void setData(double data) {
        this.data = data;
    }

    public void setPoint(Point point) {
        this.point = point;
    }

    public double getData() {
        return data;
    }

    public Point getPoint() {
        return point;
    }
}

每个点含有形状和颜色信息:

public class Point {
    private String color;
    private String shape;

    public Point(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }
    
    public void setColor(String color) {
        this.color = color;
    }

    public void setShape(String shape) {
        this.shape = shape;
    }

    public String getColor() {
        return color;
    }

    public String getShape() {
        return shape;
    }
}

现在来随机产生一些数据点:

public class Main {

    public static void main(String[] args) {
        int N = 10;

        DataPoint[] dp = new DataPoint[N];
        for(int i=0; i<N; i++) {
            double data = Math.random(); // or whatever data source
            Point point = data > 0.5 ? new Point("Green", "Circle") : new Point("Red", "Cross");
            dp[i] = new DataPoint(data, point);
        }
        System.out.println(N);
    }
}

看起来简单,工作很好。让我们看看在创建这个DataPoint数组时使用的内存数量。

idea中在输出部分打断点进行调试:

请添加图片描述

导出进程的内存使用情况
请添加图片描述

使用jhat进行分析,打开localhost:7000

请添加图片描述
请添加图片描述

可以发现每个DataPoint对象占用的内存为32 bytes

请添加图片描述

而DataPoint中的Point同样占用 32bytes

所以说,一个DataPoint对象占用32 bytes,DataPoint中的Point同样占用32 bytes,总内存占用为(32+32)N = 64N bytes,当然,我们上述设置的N为10,如果N为1000那么就占用64KB,如果是100个线程,那么就占用了6.4MB.

有什么问题

实际上只有两种不同的点——绿圆和红十字,但我们创建了N点对象。

使用享元模式解决上述问题

我们可以发现在上述问题中,其实最大的问题就是冗余,我们需要避免冗余值。为此,我们定义如下两个关键名词:

  1. 重复属性——对象的多个实例中可能保持相同的属性。
  2. 唯一属性——随着对象的每个实例而改变的属性。

在我们的场景中,数据点对象的每一半都包含相同的point值(概率地)。享元模式告诉我们对象中可能大量重复的部分,应该使用共享或者重用的模式,而不是重复,特别针对如下场景:

  1. 重复属性特别多,比如本例子中Point对象。
  2. 重复属性可以接受的值数量有限。比如说布尔类,它只能接受true或false两个值。

有很多方法可以实现这一点。让我们看看实现享元模式的几种方法。

使用静态工厂

我们为Point对象的两个可能实例分别公开一个静态工厂方法。我们分别对Point和Main进行如下修改:

class Point {
    private String color;
    private String shape;
    private static Point GREEN_CIRCLE = new Point("Green", "Circle");
    private static Point RED_CROSS = new Point("Red", "Cross");

    private Point(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }

    public static Point getGreenCircle() {
        return GREEN_CIRCLE;
    }
    public static Point getRedCross() {
        return RED_CROSS;
    }
}
public class Main {

    public static void main(String[] args) {
        int N = 10;

        DataPoint[] dp = new DataPoint[N];
        for(int i=0; i<N; i++) {
            double data = Math.random(); // or whatever data source
            Point point = data > 0.5 ? Point.getGreenCircle() : Point.getRedCross();
            dp[i] = new DataPoint(data, point);
        }
        System.out.println(N);
    }
}

同样我们分析一下内存使用情况:
请添加图片描述
请添加图片描述

我们可以发现,DataPoint引用的Point在整个程序中只占用64 bytes,其不会随着DataPoint的增长而增长。那么总的内存占用为:(32N+64)bytes

使用枚举类

我们对程序进行如下修改:

enum Point {
    GREEN_CIRCLE("Green", "Circle"),
    RED_CROSS("Red", "Cross");

    private final String color;
    private final String shape;

    Point(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }
}

同样我们分析一下内存使用情况:

请添加图片描述
请添加图片描述

同样,DataPoint引用的Point在整个程序中只占用120bytes,其不会随着DataPoint的增长而增长。那么总的内存占用为:(32N+120)bytes

很明显,静态工厂和枚举都只会创建2个Point对象的副本,不管DataPoint重复多少次。

缓存

以上两个例子在所有变量都已知的情况下运行得很好。另一种情况是,其中一个字段可以获得比预期更多的值。但是,除非变化的字段发生变化,否则对象的其他值不会发生变化。

让我们举一个不同的例子。假如我们的Point是一个动态变化的值,其有一个id属性用于唯一确定其属性,由于其有限性,我们可以先判断来的数据中是否是已知的Point值,如果没有则缓存,有则使用原来的Point。

class PointCache {
    public static Map<String, Point> pointMap = new HashMap<>();

    public Point getPoint(String pointId) {
        Point point;
        if(pointMap.containsKey(pointId)) {
            point = pointMap.get(pointId);
        } else {
            point = new Point(/*properties*/);
            pointMap.put(pointId, point);
        }
        return point;
    }
}

一旦我们创建了Point对象,我们就会根据它的唯一id将其缓存到映射中,这样我们就不会再初始化相同的Point。

相关文章
|
3月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
195 6
|
2月前
|
NoSQL 测试技术
内存程序崩溃
【10月更文挑战第13天】
140 62
|
23天前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
2月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
121 21
|
3月前
|
C语言 Android开发 C++
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
本文介绍了使用MTuner软件进行Qt MinGW编译程序的内存泄漏检测的方法,提供了MTuner的下载链接和测试代码示例,并通过将Debug程序拖入MTuner来定位内存泄漏问题。
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
|
3月前
|
存储 运维
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
.NET开发必备技巧:使用Visual Studio分析.NET Dump,快速查找程序内存泄漏问题!
|
4月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
4月前
|
监控 Java API
如何从 Java 程序中查找内存使用情况
【8月更文挑战第22天】
221 0
|
5月前
|
缓存 弹性计算 数据库
阿里云2核4G服务器支持多少人在线?程序效率、并发数、内存CPU性能、公网带宽多因素
2核4G云服务器支持的在线人数取决于多种因素:应用效率、并发数、内存、CPU、带宽、数据库性能、缓存策略、CDN和OSS使用,以及用户行为和系统优化。阿里云的ECS u1实例2核4G配置,适合轻量级应用,实际并发量需结合具体业务测试。
83 0
阿里云2核4G服务器支持多少人在线?程序效率、并发数、内存CPU性能、公网带宽多因素
|
5月前
|
算法 Java Serverless
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决