I’m using the source code for Surface.java
as a reference for this question.
Surface implements the Parcelable interface, and it also holds a handle to an object on the native side.
I’m interested in knowing how garbage collection is handled in this case:
A Surface (A) is created and written into a Parcel. There are no references to it afterwards.
A copy of the original Surface (B) is read from the Parcel; let’s say this happens on another thread used for rendering. This instance now holds onto the same native handle as (A) and there’s a strong reference to this instance somewhere.
A GC occurs and (A) is collected since it’s no longer referenced. finalize()
is run, which calls release()
, which in turn calls nativeRelease(long)
for the native handle.
A cursory look over the source code made me think that now (B) should also kick the bucket and stop working since the native handle is released, but after trying to replicate this it doesn’t seem to be the case. (A) does get collected but (B) lives on and remains usable.
Now I have a feeling that there’s some reference counting going on with the native object, or some other magic on the native side of the parcelling process.
Regardless of whether my assumption is correct, I’m looking for an overview on what causes this behaviour, preferably with some references to the framework source code. I’m also tangentially interested in how Surface locking works in similar cases.
When the JVM doesn't have necessary memory space to run, the garbage collector will run and delete unnecessary objects to free up memory. Unnecessary objects are the objects which have no other references (address) pointing to them.
The garbage collector provides the following benefits: Frees developers from having to manually release memory. Allocates objects on the managed heap efficiently. Reclaims objects that are no longer being used, clears their memory, and keeps the memory available for future allocations.
C does not have automatic garbage collection. If you lose track of an object, you have what is known as a 'memory leak'. The memory will still be allocated to the program as a whole, but nothing will be able to use it if you've lost the last pointer to it. Memory resource management is a key requirement on C programs.
If the application has enough free heap memory, Major GC will not be triggered. With JConsole (comes with your JDK) you can check memory usage. Is triggering Major GCs the actual purpose of your application?
Surfaces are just references into the BufferQueue. They contain a Binder token, used to negotiate sending graphical buffers between producer and receiver. A relevant JNI code:
static jlong nativeReadFromParcel(JNIEnv* env, jclass clazz, jlong nativeObject, jobject parcelObj) {
Parcel* parcel = parcelForJavaObject(env, parcelObj);
if (parcel == NULL) {
doThrowNPE(env);
return 0;
}
android::view::Surface surfaceShim;
// Calling code in Surface.java has already read the name of the Surface
// from the Parcel
surfaceShim.readFromParcel(parcel, /*nameAlreadyRead*/true);
sp<Surface> self(reinterpret_cast<Surface *>(nativeObject));
// update the Surface only if the underlying IGraphicBufferProducer
// has changed.
if (self != nullptr
&& (IInterface::asBinder(self->getIGraphicBufferProducer()) ==
IInterface::asBinder(surfaceShim.graphicBufferProducer))) {
// same IGraphicBufferProducer, return ourselves
return jlong(self.get());
}
sp<Surface> sur;
if (surfaceShim.graphicBufferProducer != nullptr) {
// we have a new IGraphicBufferProducer, create a new Surface for it
sur = new Surface(surfaceShim.graphicBufferProducer, true);
// and keep a reference before passing to java
sur->incStrong(&sRefBaseOwner);
}
if (self != NULL) {
// and loose the java reference to ourselves
self->decStrong(&sRefBaseOwner);
}
return jlong(sur.get());
}
You can clearly see, how a Binder token is read from Parcel and converted to IGraphicBufferProducer IPC interface.
Binder tokens are reference-counted in kernel, destroying one of user space references does nothing as long as more exists.
When you are within the same process, locking semantics do not change, because native Surface
maintains a cache of instances:
sp<Surface> Surface::readFromParcel(const Parcel& data) {
Mutex::Autolock _l(sCachedSurfacesLock);
sp<IBinder> binder(data.readStrongBinder());
sp<Surface> surface = sCachedSurfaces.valueFor(binder).promote();
if (surface == 0) {
surface = new Surface(data, binder);
sCachedSurfaces.add(binder, surface);
} else {
// The Surface was found in the cache, but we still should clear any
// remaining data from the parcel.
data.readStrongBinder(); // ISurfaceTexture
data.readInt32(); // identity
}
if (surface->mSurface == NULL && surface->getISurfaceTexture() == NULL) {
surface = 0;
}
cleanCachedSurfacesLocked();
return surface;
}
Every Java Surface
instance, created by parcelling/unparcelling within the same process, refers to the same native Surface
, which means that locks should still have effect: you will get an exception in case of contention.
Attempting to simultaneously draw to unparcelled Surfaces from multiple processes would fail because IGraphicBufferProducer
contract explicitly forbids that:
// connect attempts to connect a client API to the IGraphicBufferProducer.
// This must be called before any other IGraphicBufferProducer methods are
// called except for getAllocator.
//
// This method will fail if the connect was previously called on the
// IGraphicBufferProducer and no corresponding disconnect call was made.
//
// outWidth, outHeight and outTransform are filled with the default width
// and height of the window and current transform applied to buffers,
// respectively. The token needs to be any binder object that lives in the
// producer process -- it is solely used for obtaining a death notification
// when the producer is killed.
virtual status_t connect(const sp<IBinder>& token,
int api, bool producerControlledByApp, QueueBufferOutput* output) = 0;
You can find more details about lower-level graphical stack architecture on the Android website for device and firmware makers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With