javamemoryprofilingvisualvmflyweight-pattern

visualvm retained size same after using flyweight pattern


I have an odd situation.

I have a simple flyweight factory that allows me to reuse instances that are equal() in a graph of objects.

When I serialize the root object, with and without use flyweight, to measure its benefit, I go from 2,014,169 bytes with a new object for each reference, down to 1,680,865. Okay, that is fine.

BUT, when I look at this object's retained size in a heap dump in jvisualvm, I always see 6,807,832.

How can it be? Surely if in one case I have multiple instances of the same object, they each take up memory. Retained size should be the amount that would be recovered from GC. I would think that would be more without using the flyweight factory to recycle instances. If I wasn't seeing the benefit in serialization, I'd think it was a bug in the flyweight factory, but I can't see how it'd only work for serialization.

Right now I'm a bit perplexed.

Using the flyweight factory, you pass new instances through a check to see if a reference can be reused instead:

map.put(key, flyweightFactory.get(new MyClass()));

If not using the flyweight, store the new object every time:

map.put(key, new MyClass());

And for reference, here is the flyweight factory class:

/**
 * 
 * Provides simple object reuse a la the flyweight pattern. Not thread safe.
 * 
 * @author sigmund.segfeldt
 *
 * @param <A> type to be stored in the flyweight factory
 */
public class FlyweightFactory<A> {

    private final Map<A, A> flyweights = new HashMap<>();

    private int reuses = 0;

    /**
     * 
     * returns an instance of A, which is equal to the instance provided, ensuring
     * that the same reference is always supplied for any such equal objects.
     * 
     * @param instance
     * @return a reference to an equal to instance, possibly instance itself
     */
    public A get(A instance) {
        A flyweight;

        if (flyweights.containsKey(instance)) {
            flyweight = flyweights.get(instance);
            ++reuses;
        } else {
            flyweights.put(instance, instance);
            flyweight = instance;
        }

        return flyweight;
    }

    /**
     * 
     * @return the size of the flyweight factory; i.e. the number of distinct objects held
     */
    public int size() {
        return flyweights.size();
    }

    /**
     * 
     * @return number of times a flyweight has been reused, purely for statistics to see how beneficial flyweight is (without
     * taking into consideration the size of reused objects, of course).
     */
    public int reuses() {
        return reuses;
    }

    @Override
    public String toString() {
        return "FlyweightFactory[size " + size() + ", reuses=" + reuses() + "]";
    }
}

Solution

  • So the problem was that I hadn't released the flyweight factory itself. It is not referenced from the root object, but by holding other references to the flyweight objects, they don't count towards the retained size.

    Once my test case was fixed, and there were no references to the flyweights except through the root object, the retained size went up to 9.2mb with flyweight, 10.3mb without recycling equal instances via flyweights.

    I was fooled by the retained size of the object; that 6.8mb is just overhead to the container objects as well as references (given that the keys were flyweights too). I didn't figure that my flyweights weren't even being counted.

    This actually is very informative. It was a useful mistake to make! I need to look at the containers themselves, see if I can get a benefit from replacing hashmaps with enummaps (10mb might not seem much but I'm aiming for low footprint!)

    By the way, performing a GC didn't make any difference, either with or without flyweights.

    Thanks for your input folks.