One of the biggest problem with working with large, nested object trees is implementing a way for a child
node to see its parent. The easiest way to do this is to add a reference to the child back to its parent.
This results in a "circular" reference, where A refers to B refers to A. Unfortunately, the garbage
collector perl uses during runtime is not capable of knowing whether or not something ELSE is referring
to these circular references.
In practical terms, this means that object trees in lexically scoped variable ( e.g. "my $Object =
Tree->new" ) will not be cleaned up when they fall out of scope, like normal variables. This results in a
memory leak for the life of the process, which is a bad thing when using mod_perl or other processes that
live for a long time.
Object::Destroyer allows for the creation of "Destroy" handles. The handle is "attached" to the circular
relationship, but is not a part of it. When the destroy handle falls out of scope, it will be cleaned up
correctly, and while being cleaned up, it will also force the data structure it is attached to to be
destroyed as well. Object::Destroyer can call a specified release method on an object (or method
"DESTROY" by default). Alternatively, it can execute an arbitrary user code passed to constructor as a
code reference.
UseasaStandaloneHandle
The simplest way to use the class is to create a standalone destroyer, preferably in the same lexical
content. ( i.e. immediately after creating the object to be destroyed)
sub plagiarise {
# Parse in a large nested document
my $filename = shift;
my $document = My::XML::Tree->open($filename);
# Create the Object::Destroyer to clean it up as needed
my $sentry = Object::Destroyer->new( $document, 'release' );
# Continue with the Document as normal
if ($document->author == $me) {
# Normally this would have leaked the document
return new Error("You already own the Document");
}
$document->change_author($me);
$document->save;
# We don't have to $Document->DESTROY here
return 1;
}
When the $sentry falls out of scope at the end of the sub, it will force the cirularly linked $Document
to be cleaned up at the same time, rather than being forced to manually call "$Document-<gt"release> at
each and every location that the sub could possible return.
Using the Object::Destroyer object to force garbage collection to work properly allows you to neatly
sidestep the inadequecies of the perl garbage collector and work the way you normally would, even with
big objects.
Usetoclean-updatastructures
If a data structure with circular refereces has no method to release memory, you can create an
"Object::Destroyer" object that will do the job. Pass a code reference (most probably created by an
anonymous subrotine block) to the constructor of the sentry object, and this code will be called upon
leaving the scope.
{
$params{other} = \%other_params;
$other_params{params} = \%params;
my $sentry = Object::Destroyer->new( sub {undef $params{other}} );
##
## From now on, memory of %params will be
## safely released when block is exited.
##
... code with return, next or last ...
}
UseasaTransparentWrapper
For situations where a class is always going to produce circular references, you may wish to build this
improved clean up directly into the class itself, and with a few exceptions everything will just work the
same.
Take the following example class
package My::Tree;
use strict;
use Object::Destroyer;
sub new {
my $self = bless {}, shift;
$self->init; ## assume that circular references are made
## Return the Object::Destroyer, with ourself inside it
my $wrapper = Object::Destroyer->new( $self, 'release' );
return $wrapper;
}
sub release {
my $self = shift;
foreach (values %$self) {
$_->DESTROY if ref $_ eq 'My::Tree::Node';
}
%$self = ();
}
We might use the class in something like this
sub process_file {
# Create a new tree
my $tree = My::Tree->new( source => shift );
# Process the Tree
if ($tree->comments) {
$tree->remove_comments or return;
}
else {
return 1; # Nothing to do
}
my $filename = $tree->param('target') or return;
$tree->write($filename) or return;
return 1;
}
We were able to work with the data, and at no point did we know that we were working with a
Object::Destroyer object, rather than the My::Tree object itself.
ResourceUsage
To implement the transparency, there is a slight CPU penalty when a method is called on the wrapper to
allow it to pass the method through to the encased object correctly, and without appearing in the
"caller()" information. Once the method is called on the underlying object, you can make further method
calls with no penalty and access the internals of the object normally.
ProblemswithWrappersandreforUNIVERSAL::isa
Although it may ACT exactly like what's inside it, is isn't really it. Calling "ref $wrapper" or "blessed
$wrapper" will return 'Object::Destroyer', and not the class of the object inside it.
Likewise, calling "UNIVERSAL::isa( $wrapper, 'My::Tree' )" or "UNIVERSAL::can( $wrapper, 'param' )"
directly as functions will also not work. The two alternatives to this are to either use "$Wrapper->isa"
or "$wrapper->can", which will be caught and treated normally, or simple don't use a wrapper and just use
the standalone cleaners.