Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reworked the way allocated memory is tracked #1239

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 152 additions & 14 deletions src/com/sun/jna/Memory.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@
*/
package com.sun.jna;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.ArrayList;

/**
* A <code>Pointer</code> to memory obtained from the native heap via a
Expand All @@ -51,9 +49,82 @@
* @see Pointer
*/
public class Memory extends Pointer {
/** Keep track of all allocated memory so we can dispose of it before unloading. */
private static final Map<Memory, Boolean> allocatedMemory =
Collections.synchronizedMap(new WeakHashMap<Memory, Boolean>());

private static ReferenceQueue<Memory> QUEUE = new ReferenceQueue<Memory>();
private static LinkedReference HEAD; // the head of the doubly linked list used for instance tracking

/**
* Keep track of all allocated memory so we can dispose of it before
* unloading. This is done using a doubly linked list to enable fast
* removal of tracked instances.
*/
private static class LinkedReference extends WeakReference<Memory> {

private LinkedReference next;
private LinkedReference prev;

private LinkedReference(Memory referent) {
super(referent, QUEUE);
}

/**
* Add the given {@code instance} to the instance tracking.
*
* @param instance the instance to track
*/
static LinkedReference track(Memory instance) {
// use a different lock here to allow the finialzier to unlink elements too
synchronized (QUEUE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this synchronization needed? While the class lacks documentation regarding threading, the OpenJDK implementation protects the reference queue. There are fixed bugs regarding multithreaded enqueue, so I assume, that ReferenceQueue should be save. From my POV it does not matter, which thread unlinks, as the unlink will be protected again by the HEAD lock.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this lock is not needed to avoid concurrency effects with the current OpenJDK implementations. Removing it had a negative effect on performance in my digging around why the WeakHashMap was performing better with low memory.

LinkedReference stale;

// handle stale references here to avoid GC overheating when memory is limited
while ((stale = (LinkedReference) QUEUE.poll()) != null) {
stale.unlink();
}
}

// keep object allocation outside the syncronized block
LinkedReference entry = new LinkedReference(instance);

synchronized (LinkedReference.class) {
matthiasblaesing marked this conversation as resolved.
Show resolved Hide resolved
if (HEAD != null) {
entry.next = HEAD;
HEAD = HEAD.prev = entry;
} else {
HEAD = entry;
}
}

return entry;
}

/**
* Remove the related instance from tracking and update the linked list.
*/
private void unlink() {
synchronized (LinkedReference.class) {
LinkedReference next;

if (HEAD != this) {
if (this.prev == null) {
// this entry was detached before, e.g. disposeAll was called and finalizers are running now
return;
}

next = this.prev.next = this.next;
} else {
next = HEAD = HEAD.next;
}

if (next != null) {
next.prev = this.prev;
}

// set prev to null to detect detached entries
this.prev = null;
}
}
}

private static final WeakMemoryHolder buffers = new WeakMemoryHolder();

Expand All @@ -66,13 +137,71 @@ public static void purge() {

/** Dispose of all allocated memory. */
public static void disposeAll() {
// use a copy since dispose() modifies the map
Collection<Memory> refs = new LinkedList<Memory>(allocatedMemory.keySet());
for (Memory r : refs) {
r.dispose();
synchronized (LinkedReference.class) {
LinkedReference entry;

while ((entry = HEAD) != null) {
Memory memory = HEAD.get();

if (memory != null) {
// dispose does the unlink call internal
memory.dispose();
dbwiddis marked this conversation as resolved.
Show resolved Hide resolved
} else {
HEAD.unlink();
}

if (HEAD == entry) {
throw new IllegalStateException("the HEAD did not change");
}
}
}

synchronized (QUEUE) {
LinkedReference stale;

// try to release as mutch memory as possible
while ((stale = (LinkedReference) QUEUE.poll()) != null) {
stale.unlink();
}
}
}

/**
* Unit-testing only, ensure the doubly linked list is in a good shape.
*
* @return the number of tracked instances
*/
static int integrityCheck() {
synchronized (LinkedReference.class) {
if (HEAD == null) {
return 0;
}

ArrayList<LinkedReference> entries = new ArrayList<LinkedReference>();
LinkedReference entry = HEAD;

while (entry != null) {
entries.add(entry);
entry = entry.next;
}

int index = entries.size() - 1;
entry = entries.get(index);

while (entry != null) {
if (entries.get(index) != entry) {
throw new IllegalStateException(entries.get(index) + " vs. " + entry + " at index " + index);
}

entry = entry.prev;
index--;
}

return entries.size();
}
}

private final LinkedReference reference; // used to track the instance
protected long size; // Size of the malloc'ed space

/** Provide a view into the original memory. Keeps an implicit reference
Expand Down Expand Up @@ -113,11 +242,13 @@ public Memory(long size) {
if (peer == 0)
throw new OutOfMemoryError("Cannot allocate " + size + " bytes");

allocatedMemory.put(this, Boolean.TRUE);
reference = LinkedReference.track(this);
}

protected Memory() {
super();

reference = null;
}

/** Provide a view of this memory using the given offset as the base address. The
Expand Down Expand Up @@ -182,11 +313,18 @@ protected void finalize() {

/** Free the native memory and set peer to zero */
protected synchronized void dispose() {
if (peer == 0) {
// someone called dispose before, the finalizer will call dispose again
return;
}

try {
free(peer);
} finally {
allocatedMemory.remove(this);
peer = 0;
// no null check here, tracking is only null for SharedMemory
// SharedMemory is overriding the dispose method
reference.unlink();
dbwiddis marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
19 changes: 8 additions & 11 deletions test/com/sun/jna/MemoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.Map;

import junit.framework.TestCase;

Expand Down Expand Up @@ -183,23 +181,22 @@ public void testDump() {
"[0c0d0e]" + ls, m.dump());
}

public void testRemoveAllocatedMemoryMap() throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
public void testRemoveAllocatedMemory() {
// Make sure there are no remaining allocations
Memory.disposeAll();

// get a reference to the allocated memory
Field allocatedMemoryField = Memory.class.getDeclaredField("allocatedMemory");
allocatedMemoryField.setAccessible(true);
Map<Memory, Reference<Memory>> allocatedMemory = (Map<Memory, Reference<Memory>>) allocatedMemoryField.get(null);
assertEquals(0, allocatedMemory.size());
assertEquals(0, Memory.integrityCheck());

// Test allocation and ensure it is accounted for
Memory mem = new Memory(1024);
assertEquals(1, allocatedMemory.size());
assertEquals(1, Memory.integrityCheck());

// Test shared memory is not tracked
Pointer shared = mem.share(0, 32);
assertEquals(1, Memory.integrityCheck());

// Dispose memory and ensure allocation is removed from allocatedMemory-Map
mem.dispose();
assertEquals(0, allocatedMemory.size());
assertEquals(0, Memory.integrityCheck());
}

public static void main(String[] args) {
Expand Down