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 1 commit
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
145 changes: 131 additions & 14 deletions src/com/sun/jna/Memory.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,9 @@
*/
package com.sun.jna;

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 +48,71 @@
* @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>());

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

/**
* Add the given {@code instance} to the instance tracking.
*
* @param instance the instance to track
*/
static LinkedReference track(Memory instance) {
// 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 LinkedReference HEAD; // the head of the doubly linked list used for instance tracking
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd generally prefer to see the variable definition before it's used in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This could be done easily, LinkedReference would be not defined at that point. I think this is a chicken-and-egg situation, how to solve best?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's a matter of preference/style. I'm not sure there's a right answer.

In this case, LinkedReference is a class, and there's not generally any need to define a class first (I suppose an import statement would normally do that) or in any specific order, compared to a field that's accessed which does have an impact for later fields. When reading code, if I see a class definition, I know "that class is defined somewhere, I can eventually find it either in this file or another one" whereas when I see a variable referenced, I generally scroll up to see where it's defined.

Not a big deal, just expressing my preference! :)


private static final WeakMemoryHolder buffers = new WeakMemoryHolder();

Expand All @@ -66,13 +125,62 @@ 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");
}
}
}
}

/**
* 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 +221,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 +292,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