0

I am working on an in-memory storage solution that stores data samples. This is for a multithreaded trending application that has items constantly being written into the storage array and items being removed from it periodically. It will store the latest 24 hours of samples. I need to be able to grab the data in full, or partially. Because of these requirements I chose to use a CopyOnWriteArrayList.

The storage solution is stored in a

CopyOnWriteArrayList<Point>.

I have two classes that I wrote to house the data: Point and Samples.

Point consists of:

private int ioId;
private int machineId;
private jPointType pointType; //(int)
private String subfield;
private long startTimestamp;
private long endTimestamp;
private int pruneLock;
private jTrendReqType predefType; //(int)

CopyOnWriteArrayList<Sample> dataList;

Sample consists of:

Long timestamp;
double data;

I'm currently testing with 2/sec data and 30 points (7200 Samples per each point). I run the test for 1 hour and see an increase of about 10MB of usage via Task Manager. This ends up being about 45 bytes per sample. This seems to be quite high for the type of data that I am storing. Is there that much Java overhead or am I doing something inefficiently?

Tacitus86
  • 1,314
  • 2
  • 14
  • 36
  • Have a look at the [Eclipse Memory Analyzer](http://www.eclipse.org/mat/), maybe it will help you understand the memory usage better. – Sidias-Korrado Sep 21 '17 at 18:29
  • In theory, you could get rid of the Point class, and write everything to a `ByteBuffer`, but that is harder and probably slower to read from. The `CopyOnWriteArrayList` also seems a bit heavy to me, as it will copy the list on every write. You stated that the list is constantly written to, so the CopyOnWrite list will perform terrible. Every thread writing to the list will create and at least temporarily allocate a new ArrayList instance. – NickL Sep 21 '17 at 23:25
  • @NickL I agree it's heavy but I don't know another good data structure that has good accessibility as well as being thread safe since the reads/writes are occurring from multiple threads. – Tacitus86 Sep 22 '17 at 12:13

1 Answers1

3

Well, let's look at your Sample class:

Long timestamp;
double data;

From this other answer, a Long takes about 16 bytes (8 bytes for the long and 8 bytes overhead.

The double has a memory footprint of 8 bytes in Java.

The Sample object reference adds at least 8 bytes (it needs to store a long to reference the memory address).

So just by itself, the Sample object is 32 bytes long.

However, you calculated an average storage size of 45 bytes per Sample.

So possible other causes:

  • Point objects contain String which are about 8 bytes + 2 bytes * length
  • Overhead from CopyOnWriteArrayList's implementation
  • Unfreed memory - memory that is not used, but not yet released by the JVM.

The most likely cause however is probably unfreed memory. Due to how Java operates, memory is only freed when garbage collection(GC) is run (and there's no guaranteed way of forcing it to run). Because you're using CopyOnWriteArrayList, you're constantly creating new lists behind the scenes as you're adding objects and the JVM just haven't released them just yet because GC hasn't run.

Here's a link to some Oracle documentation on Java's Garbage Collection mechanism.

ACVM
  • 1,497
  • 8
  • 14
  • Thanks. I did change the Long to a long as it didn't seem to be needed. – Tacitus86 Sep 21 '17 at 18:53
  • @Tacitus86 Yeah that'd help a bit, something else you can consider is to call `System.gc()` which lets Java know that this is a good time to run GC. However, that call is completely advisory, so the JVM may still not run GC. Your best bet is to run it for longer so that the JVM will want to run GC and then checking then. – ACVM Sep 21 '17 at 19:35
  • 1
    *Every* object has an object header or more generally, a JVM specific overhead, not just the `Long` object. So if you assume 8 bytes overhead for the `Long` instance, you would have to assume 8 bytes overhead for the `Sample` instance too. Then, since these object are referenced by lists, there is space for a reference needed for each instance, if we assume “compressed oops”, 4 bytes each, so we are already at 44 bytes per `Sample`… – Holger Sep 25 '17 at 09:19
  • I did already account for the Sample overhead. That's a good point about the list themselves requiring bits to store the reference. – ACVM Sep 25 '17 at 17:30
  • @Eugene: afaik, the header consist of whole words only, e.g. 8 bytes with compressed klass pointers, so the subsequent data is already aligned when the start of the header is aligned. With compressed oops, we end up at 16 bytes for the `Long` and 20 bytes for the `Sample` instance (8 header + 8 `double` + 4 reference), padded to 24, which makes a total of 40 bytes for `Sample`+`Long` (assuming no sharing), then add four bytes for the compressed oops in an array based list (does not need further alignment) and you are at 44 bytes, which is very close to the reported average of 45 bytes. – Holger Sep 26 '17 at 06:35
  • @Holger I will remove that comment, I can't tell why and *where* did I yesterday saw headers being aligned separately - they are not; and it's really easy to test :( totally my bad – Eugene Sep 26 '17 at 07:04
  • @Holger So what I'm seeing after switching to Sample using a long and a double for 86400 samples for each of 60 points according to Task Manager uses roughly 225MB of RAM. And I also switched to a plain ArrayList using synchronized access. – Tacitus86 Sep 26 '17 at 19:17
  • @Holger - Can you revise your explanation using the 8 byte long instead of the 16 byte long? I just want to make sure I'm not missing and extra padding in my calculation. – Tacitus86 Sep 26 '17 at 19:28
  • 1
    @Tacitus86: depending on the JVM configuration, you’ll have either, 24 bytes or 32 bytes per sample instance now, since your previous results indicated 8 bytes overhead (either 32 bit or 64 bit with compressed oops and compressed klass pointers), you’ll have 8 bytes header + 8 bytes `long` + 8 bytes `double` and no further padding, as 3×8 == 24 is perfectly divisible by eight. Then, reference within the list is needed, 4 bytes with compressed oops, no further padding, so you end up with 28 bytes per sample. – Holger Sep 27 '17 at 07:02
  • 1
    Well, `ArrayList`s may have a higher capacity than needed when you don’t specify a precalculated initial capacity (and never trim them), but it should never result in more than two bytes per sample, given its standard strategy of increasing the capacity by 50%. – Holger Sep 27 '17 at 07:19
  • Side note: It's trimmed once an hour if the timestamp is greater that 24hours old (except in certain cases where the prune is delayed), just to address that note in your comment. – Tacitus86 Sep 27 '17 at 12:59