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

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

举个例子

对象创建是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。

相关文章
|
18天前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
1月前
|
缓存 弹性计算 数据库
阿里云2核4G服务器支持多少人在线?程序效率、并发数、内存CPU性能、公网带宽多因素
2核4G云服务器支持的在线人数取决于多种因素:应用效率、并发数、内存、CPU、带宽、数据库性能、缓存策略、CDN和OSS使用,以及用户行为和系统优化。阿里云的ECS u1实例2核4G配置,适合轻量级应用,实际并发量需结合具体业务测试。
27 0
阿里云2核4G服务器支持多少人在线?程序效率、并发数、内存CPU性能、公网带宽多因素
|
2月前
|
设计模式 缓存 Java
Java设计模式:享元模式实现高效对象共享与内存优化(十一)
Java设计模式:享元模式实现高效对象共享与内存优化(十一)
|
29天前
|
算法 Java Serverless
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决
Java演进问题之Java程序占用的内存经常比实际应用运行产生的对象占用要多如何解决
|
2月前
|
存储 Java C++
Java虚拟机(JVM)在执行Java程序时,会将其管理的内存划分为几个不同的区域
【6月更文挑战第24天】Java JVM管理内存分7区:程序计数器记录线程执行位置;虚拟机栈处理方法调用,每个线程有独立栈;本地方法栈服务native方法;Java堆存储所有对象实例,垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息;运行时常量池存储常量;直接内存不属于JVM规范,通过`java.nio`手动管理,不受GC直接影响。
32 5
|
2月前
|
算法 Java
垃圾回收机制(Garbage Collection,GC)是Java语言的一个重要特性,它自动管理程序运行过程中不再使用的内存空间。
【6月更文挑战第24天】Java的GC自动回收不再使用的内存,关注堆中的对象。通过标记-清除、复制、压缩和分代等算法识别无用对象。GC分为Minor、Major和Full类型,针对年轻代、老年代或整个堆进行回收。性能优化涉及算法选择和参数调整。
36 3
|
2月前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
39 2
|
1月前
|
监控
LabVIEW程序内存泄漏分析与解决方案
LabVIEW程序内存泄漏分析与解决方案
57 0
|
2月前
|
Java UED 开发者
JVM逃逸分析原理解析:优化Java程序性能和内存利用效率
JVM逃逸分析原理解析:优化Java程序性能和内存利用效率
|
2月前
|
存储
程序与技术分享:C内存池的实现
程序与技术分享:C内存池的实现

相关实验场景

更多