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

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

举个例子

对象创建是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月前
|
存储 程序员 C语言
【动态内存管理助力程序优化与性能飞升】(下)
【动态内存管理助力程序优化与性能飞升】
|
3月前
|
C语言
【动态内存管理助力程序优化与性能飞升】(中)
【动态内存管理助力程序优化与性能飞升】
|
17天前
|
存储 编译器 C语言
深入探索C语言动态内存分配:释放你的程序潜力
深入探索C语言动态内存分配:释放你的程序潜力
28 0
|
3月前
|
存储 缓存 Java
Java性能优化: 如何减少Java程序的内存占用?
Java性能优化: 如何减少Java程序的内存占用?
244 2
|
1月前
|
存储 设计模式 缓存
C++享元模式探索:轻松优化内存使用和性能提升之道
C++享元模式探索:轻松优化内存使用和性能提升之道
41 0
|
1月前
|
缓存 算法 编译器
C/C++编译器内存优化技术:内存优化关注程序对内存的访问和使用,以提高内存访问速度和减少内存占用。
C/C++编译器内存优化技术:内存优化关注程序对内存的访问和使用,以提高内存访问速度和减少内存占用。
40 0
|
2月前
|
监控 Java 编译器
优化Go语言程序中的内存使用与垃圾回收性能
【2月更文挑战第5天】本文旨在探讨如何优化Go语言程序中的内存使用和垃圾回收性能。我们将深入了解内存分配策略、垃圾回收机制,并提供一系列实用的优化技巧和建议,帮助开发者更有效地管理内存,减少垃圾回收的开销,从而提升Go程序的性能。
|
3月前
|
存储 程序员 编译器
C/C++程序内存区域划分以及各区域的介绍
C/C++程序内存区域划分以及各区域的介绍
|
3月前
|
Web App开发 前端开发 JavaScript
JavaScript 内存泄漏的检测与防范:让你的程序更稳定
JavaScript 内存泄漏的检测与防范:让你的程序更稳定
JavaScript 内存泄漏的检测与防范:让你的程序更稳定
|
3月前
|
编译器 C语言
【动态内存管理助力程序优化与性能飞升】(上)
【动态内存管理助力程序优化与性能飞升】