See how much memory the meta mode reduces for your program

for instance

Object creation is the most basic operation in OOP. Even in the most trivial use cases, it is difficult to count the number of objects we create (intentionally or behind the scenes).

Each object is created on the heap and takes up some space before garbage collection. Long running programs occupy the heap. Similarly, threads running at the same time will multiply the memory used.

Here is a simple example:

I have an application that returns me a large number of data points to draw a chart. A data point contains two pieces of information - the data and what the point looks like on the graph:

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;
    }
}

Each point contains shape and color information:

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;
    }
}

Now let's randomly generate some data points:

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);
    }
}


It looks simple and works well. Let's look at the amount of memory used when creating this DataPoint array.

Debug at the break point of the output part in idea:

Memory usage of the export process

Use jhat for analysis and open localhost:7000


It can be found that the memory occupied by each DataPoint object is 32 bytes

The Point in the DataPoint also occupies 32 bytes

Therefore, a DataPoint object occupies 32 bytes, and the points in the DataPoint also occupy 32 bytes. The total memory occupation is (32 + 32) N = 64N bytes. Of course, the N set above is 10. If N is 1000, it occupies 64KB, and if it is 100 threads, it occupies 6.4MB

What's the problem?

There are actually only two different points - the green circle and the Red Cross, but we created an N-point object.

Use the meta mode to solve the above problems

We can find that among the above problems, the biggest problem is redundancy. We need to avoid redundancy. To this end, we define the following two key nouns:

  1. Duplicate attributes - the same attributes may remain in multiple instances of an object.

  2. Unique attributes - attributes that change with each instance of an object.

In our scenario, each half of the data point object contains the same point value (probability). The shared meta pattern tells us that a large number of repetitive parts in the object should use the shared or reused pattern instead of repetition, especially for the following scenarios:

  1. There are many duplicate attributes, such as the Point object in this example.

  2. A limited number of values can be accepted for duplicate attributes. For example, a Boolean class can only accept true or false values.

There are many ways to do this. Let's look at several ways to implement the meta pattern.

Use static factory

We expose a static factory method for two possible instances of a Point object. We modify Point and Main as follows:

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);
    }
}

Similarly, let's analyze the memory usage:

We can find that the Point referenced by DataPoint only occupies 64 bytes in the whole program, and it will not grow with the growth of DataPoint. Then the total memory occupation is: (32N+64) bytes

Using enumeration classes

We modify the procedure as follows:

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;
    }
}

Similarly, let's analyze the memory usage:


Similarly, the Point referenced by DataPoint only occupies 120bytes in the whole program, and it will not grow with the growth of DataPoint. Then the total memory occupation is: (32N+120) bytes

Obviously, both static factories and enumerations will only create copies of 2 Point objects, no matter how many times DataPoint is repeated.

cache

The above two examples work well when all variables are known. Alternatively, one of the fields can get more values than expected. However, other values of the object do not change unless the changed field changes.

Let's take a different example. If our Point is a dynamically changing value, it has an id attribute to uniquely determine its attribute. Due to its limitation, we can first judge whether the data is a known Point value. If not, cache it and use the original 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;
    }
}

Once we create a Point object, we cache it into the map according to its unique id, so that we won't initialize the same Point again.

Tags: Java Design Pattern

Posted on Mon, 29 Nov 2021 14:36:09 -0500 by n5tkn