Java-Gaming.org Hi !
Featured games (83)
games approved by the League of Dukes
Games in Showcase (522)
Games in Android Showcase (127)
games submitted by our members
Games in WIP (590)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  Abusing WeakReference for fun and profit  (Read 2554 times)
0 Members and 1 Guest are viewing this topic.
Offline Orangy Tang

JGO Kernel


Medals: 56
Projects: 11


Monkey for a head


« Posted 2009-06-30 16:51:20 »

I was randomly poking around some of the lesser visited areas of the java api in the hope that java.lang.reflect.Proxy could solve a problem of mine (it can't) when I restumbled upon WeakReference.

Now I've been thinking more and more about resource management recently, in particular since most of the time I take the easy option and just load everything at game boot. That works but is sloppy and as I start adding more animation and landscape tiles into RescueSquad it starts getting impractical.

Assuming two levels:
 - Stage 1, needs 'grass.png' and 'ice.png'
 - Stage 2, needs 'grass.png' and 'lava.png'

Normal flow goes something like:
 - Load stage 1
 - Play stage 1
 - Destroy stage 1
 - Load stage 2
 - Play stage 2
 - Destroy stage 2

Which is nice and easy, but you get something like:
 - load(grass.png)
 - load(ice.png)
 - destroy(grass.png)
 - destroy(ice.png)
 - load(grass.png)
 - load(lava.png)

Obviously this is less than ideal, since we've loaded grass.png twice when really we could have just not destroyed it in the first place. Enter weak reference abuse:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  
91  
92  
93  
94  
95  
96  
97  
98  
99  
100  
101  
102  
103  
104  
105  
106  
107  
108  
109  
110  
111  
112  
113  
114  
115  
116  
117  
118  
119  
120  
121  
class Texture
{

  public Texture(String path)
  {
    // load from disk, create native resource
    // ..
  }

  public void destroy()
  {
    // destroy native resource
    // ..
  }
}

class ResourcePool
{
  Map<String, ResourceEntry> loadedResources;

  ResourceHandle load(String resName)
  {
    if (loadedResources.contains(resName))
    {
      ResourceEntry re = loadedResources.get(resName);
      return new ResourceHandle(re.texture, re.sentinal);
    }
    else
    {
      Texture texture = new Texture(resName);
      Object sentinal = new Object();
      ResourceHandle h = new ResourceHandle(texture, sentinal );
      loadedRes.add( new ResourceEntry(texture, sentinal) );
      return h;
    }
  }

  public void tidy()
  {
    System.gc();
    System.gc();
    for (Iterator it=loadedResources.iterator(); it.hasNext(); )
    {
      ResourceEntry e = it.next();
      if (!e.isInUse())
         e.destroy();
      it.remove();
    }
  }
}

class ResourceHandle
{
  private Object sentinal;
  private Texture texture;

  public ResourceHandle(Texture t, Object s)
  {
    this.texture = t;
    this.sentinal = s;
  }

  public Texture get() { return texture; } // Game code should only hold on to this temporarily
}

class ResourceEntry
{
  Texture texture;
  WeakReference sentinal;

  public ResourceEntry(Texture t, Object s)
  {
    this.texture = t;
    this.sentinal = new WeakReference(s);
  }

  public void destroy()
  {
    texture.destroy();
    texture = null;
  }
 
  public boolean isInUse()
  {
    return sentinal.get() != null;
  }
}



// Game code:

class Stage
{
  ResourceHandle tex1, tex2;

  public Stage(ResourcePool pool, String s1, String s2)
  {
    tex1 = pool.load(s1);
    tex2 = pool.load(s2);
  }
}


{
  ResourcePool pool = new ResourcePool();

  Stage currentStage;

  // Load stage 1
  currentStage = new Stage(pool, "ice.png", "grass.png");

  // Do gameplay
  // ..

  // Stage 1 complete, load next level
  currentStage = new Stage(pool, "grass.png", "lava.png");

  // Sentinal for 'ice' resource now vanishes, pool tidies and leaves just 'grass' and 'lava' in memory
  pool.tidy();
}


(obviously for a proper version 'Texture' could become a generic Resource interface for any native resource that needs explicit cleanup).

The 'trick' is to dish out handles to our Texture object, which have both an accessable Texture *plus* a hard reference to a Sentinal object. Internally we keep a hard reference to the same Texture, but a weak reference to the Sentinal. Once a new level has finished loading we scan through and check which sentinals have now vanished (ie. there are no more handles pointing at them). Since we still kept a proper hard ref to the (now unused) Texture object, we can happily destroy it.

This seems like a neat idea to me, since it means Stage/etc. classes don't have to remember to return/release Textures back to the resource pool, as we let the gc do the heavy lifting for us. Does anyone see any big gaping holes in this trick that I've missed?

[ TriangularPixels.com - Play Growth Spurt, Rescue Squad and Snowman Village ] [ Rebirth - game resource library ]
Offline Eli Delventhal

JGO Kernel


Medals: 42
Projects: 11
Exp: 10 years


Game Engineer


« Reply #1 - Posted 2009-06-30 17:09:28 »

Hey, that's a pretty cool idea! I've been using text level files to show all the resources I will need, then I do manual comparison to see what I should unload or keep. It works well enough, but you've got a very elegant solution.

Also having used Objective-C a lot lately the way they handle garbage collection gave me an idea for this. Basically, you call "retain" on something you want to keep in memory and "release" on something you want to remove from memory. This isn't the same as C++, however, because retain and release don't allocation or deallocate anything, they just increment or decrement a retain count variable. Then when the autorelease pool (basically the garbage collector) fires at the end of the timestep it removes anything with a retain count <= 0. So when using Objective-C it is quite easy to first iterate through all the stuff getting loaded in for the next level, add a retain to anything that already exists, then calling a release on everything from the last level. You'll end up with a retain count of 2 for items that are in both levels, then it will be decremented back to 1 correctly.

You could probably take this idea to Java, and simply have a list of integers corresponding with your loaded resources, then add 1 to it whenever you load a new resource. Wouldn't be as useful as your method, but could do the trick and might be easier to understand.

See my work:
OTC Software
Offline Orangy Tang

JGO Kernel


Medals: 56
Projects: 11


Monkey for a head


« Reply #2 - Posted 2009-06-30 17:24:41 »

Yeah, the retain/release reference counting works well in languages like C++ and Objective-C where you can put things on the stack and not worry about manually having to call destroy() on them. Java's finalisers fill that void for a gc environment but in practice they don't really cut it IMHO. C# has it's IDisposable which is an improvement - you still have to manually call destroy() but at least the compiler complains if you forget.

The only thing I worry about with this is the unpredictability of the gc - calling System.gc() may not actually remove the sentinels by the end of loading, so tidy() might not do anything. The workaround to that would be calling tidy() in the game loop every frame, which might not be too bad - the majority of resources get removed when expected, and any stragglers probably won't last more than a few frames.

[ TriangularPixels.com - Play Growth Spurt, Rescue Squad and Snowman Village ] [ Rebirth - game resource library ]
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline Riven
« League of Dukes »

« JGO Overlord »


Medals: 836
Projects: 4
Exp: 16 years


Hand over your head.


« Reply #3 - Posted 2009-06-30 17:28:22 »

1  
2  
    ResourceHandle h = new ResourceHandle( new Texture(resName), sentinal );
    loadedRes.add( new ResourceEntry(h, sentinal) );


1  
  public ResourceEntry(Texture t, Object s)



This combo won't even compile. It should be:
1  
  public ResourceEntry(ResourceHandle h, Object s)



Ontopic:
It should work, but ti's not very nice.

Let say you call pool.load("tree.png"); 3x during the entire game, you end up loading 3 Texture objects, that are all going to be destroyed.

So ArrayList loadedRes; should be a Map<String, ResourceEntry> from resName to an entry, so that you can first check whether you already have loaded the Texture

Hi, appreciate more people! Σ ♥ = ¾
Learn how to award medals... and work your way up the social rankings
Offline Orangy Tang

JGO Kernel


Medals: 56
Projects: 11


Monkey for a head


« Reply #4 - Posted 2009-06-30 17:45:14 »

Oops you're right. Brain thinking too fast for fingers to keep up - 'tis not really supposed to be working code more of a general idea, but I've updated it now with something a little nicer.

Similarly I've ignored exception handling and the possibility of textures failing to load for clarity.

[ TriangularPixels.com - Play Growth Spurt, Rescue Squad and Snowman Village ] [ Rebirth - game resource library ]
Offline Riven
« League of Dukes »

« JGO Overlord »


Medals: 836
Projects: 4
Exp: 16 years


Hand over your head.


« Reply #5 - Posted 2009-06-30 17:53:43 »

Why are you doing this:
1  
2  
      ResourceEntry re = loadedResources.get(resName);
      return new ResourceHandle(re.texture, re.sentinal);


Just returning the original re will be effectively the same.


Also, did you know that it requires the GC two passes (2x System.gc()) to cleanup a WeakReference..?

Hi, appreciate more people! Σ ♥ = ¾
Learn how to award medals... and work your way up the social rankings
Offline Orangy Tang

JGO Kernel


Medals: 56
Projects: 11


Monkey for a head


« Reply #6 - Posted 2009-06-30 18:03:30 »

Because there's a difference between a ResourceHandle (which is dished out to game code) and a ResourceEntry (which is an internal structure to ResourcePool). The handles have a strong reference to the sentinel, and so keep it alive while it's being used. The entries have the weak reference, so tell us when all of the handles have vanished.

I didn't know that about x2 gc() though, I shall amend accordingly.

[ TriangularPixels.com - Play Growth Spurt, Rescue Squad and Snowman Village ] [ Rebirth - game resource library ]
Offline Riven
« League of Dukes »

« JGO Overlord »


Medals: 836
Projects: 4
Exp: 16 years


Hand over your head.


« Reply #7 - Posted 2009-06-30 18:05:08 »

Blegh, I was reading your code incorrectly. Even after the copy/paste... nevermind.

Hi, appreciate more people! Σ ♥ = ¾
Learn how to award medals... and work your way up the social rankings
Offline Roquen
« Reply #8 - Posted 2009-07-01 07:28:25 »

To do this kind of stuff I use a custom hashtable, where the entries extend SoftReference.  Inside the table is a ReferenceQueue to nuke entries when removed.  The last 'n' references are held in a circular array to prevent collection of recently added items.  Also has a touch method to allow user to place an entry into the array.

I've been vaguely thinking about goofing with playing with "java.lang.management.MemoryNotificationInfo" to build something more clever. Sun explicitly states not to use it in this manner, but I don't know of any other option.
Offline pjt33
« Reply #9 - Posted 2009-07-01 08:30:30 »

ResourcePool.tidy could be a bit cleaner and ResourceEntry.isInUse() could be eliminated if you used a ReferenceQueue. That would also make calling it every loop virtually free if there's nothing to remove.

Also I'm not sure why you're using WeakReference rather than SoftReference - my experience has been that in practice they behave in the same way, but in theory SoftReference is more appropriate for resource management.
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline Orangy Tang

JGO Kernel


Medals: 56
Projects: 11


Monkey for a head


« Reply #10 - Posted 2009-07-01 08:58:06 »

I don't see why you'd want to use a Soft rather than Weak reference - i'm explicitly after the case when an object is no longer reachable, which is predictable and consistent. Using a soft reference means that it's going to suck up an unknown amount of memory based on the whim of when the VM decides it's low on memory. Users don't like to see apps taking up huge chunks of memory even if they would otherwise be not using it.

Using ReferenceQueue is a good idea though.

[ TriangularPixels.com - Play Growth Spurt, Rescue Squad and Snowman Village ] [ Rebirth - game resource library ]
Offline princec

« JGO Spiffy Duke »


Medals: 422
Projects: 3
Exp: 16 years


Eh? Who? What? ... Me?


« Reply #11 - Posted 2009-07-01 09:34:16 »

Orangy - what you're doing really is abuse and probably massively unreliable. I too had the wizbang idea of munging all that Resource stuff in SPGL to use WeakReferences to automatically call destroy() on unreferenced resources - but actually that's not what I really wanted to do when I thought really hard about it.

The problem actually stemmed from precisely the lazy initialisation code that you're describing - eg. create some prerequisite resource, then use it several times, then destroying it when it's no longer referenced.

First of all what you effectively need to do is decide on the set of resources you need to have created at some moment in time - say, the start of a level - and look at the set of resources which are "in use" right now, and then work out the "difference". In SPGL terms this would be like calling create() which lazily loads all dependent resources, but you'd actually set a flag or something to prevent the actual creation of the resources - instead you'd just get a graph of all the required resources. Then you'd destroy() all the resources not in that graph and call create() once more with the flag unset.

That's half the story Smiley

The other half is, sometimes you create a resource solely to use as a source for other resources. Imagine a BufferedImage resource, where create() simply loads the BI from an URL. You then have a whole bunch of sprite resources you create from the BI. The problem is that after creating all your sprites you don't need that BI any more in the heap at all.

It'd be nice to solve that problem using the WeakReference hack except for the fact that GC might not actually do anything about it. In fact I've turned off GC using a VM option before to solve glitching problems at inopportune moments (live TV graphics).

What I think the process should be to solve this is to have resources marked as "transient". That is, you specifically set some resources as being used temporarily. At any point you can then simply destroy() all transient resources in one go. You could also detect transient resources and make them into SoftReferences so that the GC would at least throw them out when memory got tight (in which case you'd need to track them in a ReferenceQueue and destroy() them when they wound up in there). One thing you specifically can't do though is mark stuff as "transient" that has been sent outside the VM. That's stuff like textures and buffers etc.

I realise this isn't quite as automatic as you were hoping for but it isn't really an automatic problem like garbage collection. Indeed you can see the rather strange solution to similar issues in garbage collection with the various different sorts of Reference object and complete lack of guarantees about when or if things end up in any particular state Smiley

For the most part though the 2-pass graph/sweep method is entirely adequate and you'll only need to go as far as marking stuff as transient when you're into heavy memory optimisation mode.

Cas Smiley

Offline Orangy Tang

JGO Kernel


Medals: 56
Projects: 11


Monkey for a head


« Reply #12 - Posted 2009-07-01 10:50:54 »

Yeah, while I quite like this trick I'm not sure if it's robust enough to be used as a basis for resource management in a game. Ideally Java would have something like C#'s IDisposable but since that requires proper language and/or VM support that's not going to happen really.

At work when we've needed *strict* resource management it's always been done manually with loading "sections" - usually defined in a text file you'd have sections like "PreGameMenus", "InGame", "PostGame" etc. that listed all the resources that would be loaded when that section started. That makes it reasonably easy to diff two sections and decide what's new and needs to be loaded in, and what's unneeded and can be ditched. The problem is that it's very manual so if you use an extra resource in code you've got to remember to add it into all the relevant loading sections or you get a crash at runtime. Sad

The WeakReference approach is at the other end of the scale really, as it tries to figure out the sections and unions of resources automatically depending on what the code is *actually* doing.

A two-pass solution is something I've thought about but not really looked into - it certainly seems to be a useful compromise in that the resources to load come from the code rather than an external file that needs to be kept in sync, but also doesn't rely on the gc behaving nicely. However it means that your actual game code for creating a new map and populating it with objects becomes more complicated, which is a big loss IMHO. But I've not looked into it properly yet, so maybe there's ways to make the two-pass method work while still keeping the game code suitably simple.

[ TriangularPixels.com - Play Growth Spurt, Rescue Squad and Snowman Village ] [ Rebirth - game resource library ]
Offline princec

« JGO Spiffy Duke »


Medals: 422
Projects: 3
Exp: 16 years


Eh? Who? What? ... Me?


« Reply #13 - Posted 2009-07-01 11:31:41 »

As far as I can see, some experimental tweaks to Resource/Resources in SPGL will be able to manage what we want without breaking too much existing code. I suspect I'll have to go through all the doCreate() implementations to put in a check to see whether it's actually being asked to create things or just make a graph. I think it should solve 99% of the problem, the remaining marking with "transient" being something you'd only do when you start performance tuning at the end of the development cycle.

Cas Smiley

Offline pjt33
« Reply #14 - Posted 2009-07-02 09:25:31 »

I don't see why you'd want to use a Soft rather than Weak reference - i'm explicitly after the case when an object is no longer reachable, which is predictable and consistent. Using a soft reference means that it's going to suck up an unknown amount of memory based on the whim of when the VM decides it's low on memory. Users don't like to see apps taking up huge chunks of memory even if they would otherwise be not using it.

Using ReferenceQueue is a good idea though.
I may be out of date with respect to recent changes in the way the VM handles memory, but the way I'm used to it working is that the VM never shrinks its heap and clears soft references to softly reachable objects before expanding its heap. Therefore soft refs don't make it take up any more memory and do improve loading time if the player keeps dying at the start of level 2 and restarting at level 1.
Offline Riven
« League of Dukes »

« JGO Overlord »


Medals: 836
Projects: 4
Exp: 16 years


Hand over your head.


« Reply #15 - Posted 2009-07-02 16:39:40 »

I may be out of date with respect to recent changes in the way the VM handles memory, but the way I'm used to it working is that the VM never shrinks its heap and clears soft references to softly reachable objects before expanding its heap. Therefore soft refs don't make it take up any more memory and do improve loading time if the player keeps dying at the start of level 2 and restarting at level 1.
These days the JVM does shrink the heap - HURRAY!



I don't see why you'd want to use a Soft rather than Weak reference - i'm explicitly after the case when an object is no longer reachable, which is predictable and consistent.
Unfortunately not the case. The JVM will determine whether a reference is reachable upon garbage collection.

The difference between a WeakReference and a SoftReference is this:
  • Weak: will be collected on any GC
  • Soft: will be collected on Full GC

Neither are predictable / consistent.

Hi, appreciate more people! Σ ♥ = ¾
Learn how to award medals... and work your way up the social rankings
Offline pjt33
« Reply #16 - Posted 2009-07-03 08:39:14 »

So if it shrinks the heap does that mean that the difference between Weak and Soft is increasingly academic as it will always want to free the memory?
Offline Roquen
« Reply #17 - Posted 2009-07-03 08:54:30 »

WeakReferences have a stronger contract which may cause the GC to take more time (and memory depending on the implementation).
Pages: [1]
  ignore  |  Print  
 
 
You cannot reply to this message, because it is very, very old.

 

Add your game by posting it in the WIP section,
or publish it in Showcase.

The first screenshot will be displayed as a thumbnail.

trollwarrior1 (34 views)
2014-11-22 12:13:56

xFryIx (73 views)
2014-11-13 12:34:49

digdugdiggy (52 views)
2014-11-12 21:11:50

digdugdiggy (46 views)
2014-11-12 21:10:15

digdugdiggy (40 views)
2014-11-12 21:09:33

kovacsa (66 views)
2014-11-07 19:57:14

TehJavaDev (70 views)
2014-11-03 22:04:50

BurntPizza (68 views)
2014-11-03 18:54:52

moogie (83 views)
2014-11-03 06:22:04

CopyableCougar4 (82 views)
2014-11-01 23:36:41
Understanding relations between setOrigin, setScale and setPosition in libGdx
by mbabuskov
2014-10-09 22:35:00

Definite guide to supporting multiple device resolutions on Android (2014)
by mbabuskov
2014-10-02 22:36:02

List of Learning Resources
by Longor1996
2014-08-16 10:40:00

List of Learning Resources
by SilverTiger
2014-08-05 19:33:27

Resources for WIP games
by CogWheelz
2014-08-01 16:20:17

Resources for WIP games
by CogWheelz
2014-08-01 16:19:50

List of Learning Resources
by SilverTiger
2014-07-31 16:29:50

List of Learning Resources
by SilverTiger
2014-07-31 16:26:06
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!