Is there a way to get a complete look at all of the memory a process has allocated?

0

First some backstory. I have a base vew that three of my app's primary views extend. The child views are empty, analog and digital. I place these child view into a gridview (2x3) and the gridview into a slidingdraw. This sliding drawer is the crux of my app. It is absolutely necessary. The sliding drawer must be in every activity, so on activity changes, I just store the state in the aapplication and retrieve it when the new activity loads.

When the app opens, the gridview creates six empty view and adds them to its adapter. Now while all of the views are empty the app works flawlessly. I can wander through the activities and do all of the other function that the app has. And while I stay in the same activity, I can create analog and digital views to my hearts content. They move, delete and do all their functions properly. But as soon as I go to switch to another activity AND I have even one analog or digital view in the gridview, the app crashes via OutOfMemoryError: bitmap size exceeds VM Budget.

Both the analog and digital view create two bitmaps for them selves. One is a the background for the view and the other is the unique look of the view that changes so rarely, it is better suited as a bitmap. Both bitmaps are fairly small (221x221 px on my test Evo). This made me think that I wasn't recycling them properly on activity changes. So I went back and checked that everything was being cleaned up and made a method that completely destroyed each view. Every variable is set to null and all bitmaps are recycled when ever the activity pauses. (Note: Using the logger, I verified that onPause is indeed being called as well as my destroy method.)

Now - days later - I still can't figure out why this memory error is being thrown. I have spent an unaccountable amount of time viewing the DDMS and Memory Tracker and it is quite possibly the most useless thing ever. I am completely fed up with DDMS, I cannot get the stupid thing to tell me anything useful.

So now the question. Is there a way (method / system call or something) that I can get the complete list of allocations of a process (my apps) and print / display / save to a file / etc... it?

Edit 1 : this is in response Falmarri. I may be posting a little to much and I apologize for that. If you want to look at something more specific I am more than happy to help, there is no reason for you to tear through my code.

The clip is from the BaseView:

public abstract class GaugeBase extends View implements BroadcastListener {
    protected static final String TAG = "GaugeBase";
    // =======================================
    // --- Declarations 
    /** Animation dynamics. */
    protected float mTarget = 0, position = 0, velocity = 0.0f, acceleration = 0.0f;
    protected long lastMoveTime = -1L;

    /** Background objects. */
    protected Bitmap mBackground;
    protected Bitmap mFaceTexture;
    protected float borderSize = 0.02f;

    /** Face objects. */
    protected Paint mRimShadowPaint, mTitlePaint, mFacePaint, mRimPaint, mRimBorderPaint;
    protected Path mTitlePath;

    /** Bounding rects. */
    protected static RectF mRimRect, mFaceRect;

    /** Text tools. */
    protected static Typeface mTypeface;

    /** The preferred size of the widget. */
    private static final int mPreferredSize = 300;

    /** The Broadcaster the gauge is registered to. */
    protected SensorBroadcaster mCaster;

    /** Is the view instantiated? */
    private boolean isDestroyed = true;
    // ---
    // =======================================

    public GaugeBase(Context context) {
        super(context);
        mCaster = ((AppionApplication)getContext().getApplicationContext())
            .getSensorBroadcaster(AppionApplication.TEST_SENSOR);
        lastMoveTime = System.currentTimeMillis();
        setTarget(mCaster.getReading());
    }

    @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); }
    @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); }
    @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { regenerate(); } 
    @Override public void onBroadcastReceived() { setTarget(mCaster.getReading()); }

    @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int chosenWidth = chooseDimension(widthMode, widthSize);
        int chosenHeight = chooseDimension(heightMode, heightSize);
        int chosenDimension = Math.min(chosenWidth, chosenHeight);
        setMeasuredDimension(chosenDimension, chosenDimension);
    }
    @Override protected void onDraw(Canvas canvas) {
        if (isDestroyed) return;
        if (mBackground == null) regenerate(); 
        canvas.drawBitmap(mBackground, 0, 0, null);
        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        canvas.scale((float)getWidth(), (float)getWidth());
        drawForeground(canvas); canvas.restore(); animate();
    }

    public HashMap<String, Object> onSavePersistentState() {
        HashMap<String, Object> mState = new HashMap<String, Object>();
        mState.put("sensor_broadcaster", mCaster.getBroadcasterName());
        mState.put("type", this.getClass().getSimpleName());
        return mState;
    }

    public void onRestorePersistentState(HashMap<String, Object> state) {
        mCaster = ((AppionApplication)getContext().getApplicationContext())
            .getSensorBroadcaster((String)state.get("sensor_broadcaster"));
    }

    private final void setTarget(float target) { mTarget = target; animate(); }

    private static final int chooseDimension(int mode, int size) {
        if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) return size;
        else return mPreferredSize;
    }

    private final void animate() {
        if (! (Math.abs(position - mTarget) > 0.01f)) return;
        if (lastMoveTime != -1L) {
            long currentTime = System.currentTimeMillis();
            float delta = (currentTime - lastMoveTime) / 1000.0f;
            float direction = Math.signum(velocity);
            if (Math.abs(velocity) < 90.0f) acceleration = 10.0f * (mTarget - position);
            else acceleration = 0.0f;
            position += velocity * delta;
            velocity += acceleration * delta;
            if ((mTarget - position) * direction < 0.01f * direction) {
                position = mTarget;
                velocity = 0.0f;
                acceleration = 0.0f;
                lastMoveTime = -1L;
            } else lastMoveTime = System.currentTimeMillis();               
            invalidate();
        } else {
            lastMoveTime = System.currentTimeMillis();
            animate();
        }
    }

    public void preInit() {
        mTypeface = Typeface.createFromAsset(getContext().getAssets(),
                "fonts/SFDigitalReadout-Heavy.ttf");
        mFaceTexture = BitmapFactory.decodeResource(getContext().getResources(),
                R.drawable.gauge_face);
        BitmapShader shader = new BitmapShader(mFaceTexture, 
                Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);

        Matrix matrix = new Matrix();
        mRimRect = new RectF(0.05f, 0.05f, 0.95f, 0.95f);
        mFaceRect = new RectF(mRimRect.left + borderSize, mRimRect.top + borderSize,
                             mRimRect.right - borderSize, mRimRect.bottom - borderSize);


        mFacePaint = new Paint();
        mFacePaint.setFilterBitmap(true);
        matrix.setScale(1.0f / mFaceTexture.getWidth(), 1.0f / mFaceTexture.getHeight());
        shader.setLocalMatrix(matrix);
        mFacePaint.setStyle(Paint.Style.FILL);
        mFacePaint.setShader(shader);

        mRimShadowPaint = new Paint();
        mRimShadowPaint.setShader(new RadialGradient(0.5f, 0.5f, mFaceRect.width() / 2.0f,
                new int[] { 0x00000000, 0x00000500, 0x50000500 },
                new float[] { 0.96f, 0.96f, 0.99f },
                Shader.TileMode.MIRROR));
        mRimShadowPaint.setStyle(Paint.Style.FILL);

        mRimPaint = new Paint();
        mRimPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        mRimPaint.setShader(new LinearGradient(0.4f, 0.6f, 0.6f, 1.0f,
                Color.rgb(0xff0, 0xf5, 0xf0), Color.rgb(0x30, 0x31, 0x30),
                Shader.TileMode.CLAMP));

        mRimBorderPaint = new Paint();
        mRimBorderPaint.setAntiAlias(true);
        mRimBorderPaint.setStyle(Paint.Style.STROKE);
        mRimBorderPaint.setColor(Color.argb(0x4f, 0x33, 0x36, 0x33));
        mRimBorderPaint.setStrokeWidth(0.005f);

        mTitlePaint = new Paint();
        mTitlePaint.setColor(0xff000000);
        mTitlePaint.setAntiAlias(true);
        mTitlePaint.setTypeface(mTypeface);
        mTitlePaint.setTextAlign(Paint.Align.CENTER);
        mTitlePaint.setTextSize(0.2f);
        mTitlePaint.setTextScaleX(0.8f);        

        // Now we prepare the gauge
        init();
        isDestroyed = false;
    }

    /** Update the gauge independent static buffer cache for the background. */
    private void regenerate() {
        if (isDestroyed) return;
        if(mBackground != null) { mBackground.recycle(); mBackground = null; } 
        // Our new drawing area
        mBackground = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas backCanvas = new Canvas(mBackground);
        float scale = (float)getWidth();
        backCanvas.scale(scale, scale);
        drawRim(backCanvas);
        drawFace(backCanvas);
        drawTitle(backCanvas);
        if (!(this instanceof EmptySpace)) { mCaster.getGroup().draw(backCanvas); }
        regenerateBackground(backCanvas);
    }

    /** Prepare the view to be cleaned up. This is called to prevent memory leaks. */
    public void destroy() { 
        isDestroyed = true;
        if (mFaceTexture != null) { mFaceTexture.recycle(); mBackground = null; }
        if (mBackground != null) { mBackground.recycle(); mBackground = null; }
        mRimShadowPaint = null;
        mRimShadowPaint = null;
        mFacePaint = null;
        mRimPaint = null;
        mRimBorderPaint = null;
        mTitlePath = null;
        mRimRect = null; mFaceRect = null;
        mTypeface = null;
        destroyDrawingCache();
    }

    /**
     * Create a bitmap of the gauge. The bitmap is to scale. 
     * @return The bitmap of the gauge.
     */
    int tobitmap = 0;
    public Bitmap toBitmap() {
        Bitmap b = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas();
        canvas.setBitmap(b);
        draw(canvas);
        return b;
    }

    /** Update the gauge dependent static buffer cache for the background. */
    protected abstract void regenerateBackground(Canvas canvas);
    /** Initializes all of the objects the gauge widget will need before use. */
    protected abstract void init();
    /** This is called when drawing the background. Draws the bordered edge of the gauge. */
    protected abstract void drawRim(Canvas canvas);
    /** This is called when drawing the background. Draws the face of the gauge. */
    protected abstract void drawFace(Canvas canvas);
    /** This is called when drawing the background. Draws the title to the gauge. */
    protected abstract void drawTitle(Canvas canvas);
    /**
     *  This is called when drawing the foreground. The foreground includes items like the 
     *  scale of an analog gauge, or the text of a digital gauge. Also any other necessary
     *  items that need drawing go here. Note: drawForeground is called quickly, repeatedly, 
     *  make it run fast and clean.
     */
    protected abstract void drawForeground(Canvas canvas);
}

This is from the digital view: (cause it's smaller and still causes the error)

public class DigitalGauge extends GaugeBase {
    // ================================
    // --- Drawing tools
    private RectF lcdRect;
    private Paint lcdPaint, detailPaint;
    private Path facePath, borderPath;
    // ---
    // ================================
    public DigitalGauge(Context context) {
        super(context);
    }

    @Override protected void regenerateBackground(Canvas canvas) { }
    @Override protected void init() {
        lcdPaint = new Paint();
        lcdPaint.setColor(0xff000000);
        lcdPaint.setAntiAlias(true);
        lcdPaint.setStrokeWidth(0.005f);
        lcdPaint.setTextSize(0.4f);
        lcdPaint.setTypeface(mTypeface);
        lcdPaint.setTextAlign(Paint.Align.CENTER);

        detailPaint = new Paint();
        detailPaint.setColor(0xff000000);
        detailPaint.setTextSize(0.2f);
        detailPaint.setStrokeWidth(0.005f);
        detailPaint.setAntiAlias(true);
        detailPaint.setTypeface(mTypeface);
        detailPaint.setTextScaleX(0.8f);
        detailPaint.setTextAlign(Paint.Align.CENTER);

        facePath = new Path();
        facePath.moveTo(0.12f, 0.0f);
        facePath.lineTo(0.88f, 0.0f);
        facePath.arcTo(new RectF(), 0, 90);
        // TODO Make the trapazoidal look of the digital gauge


        lcdRect = new RectF(mFaceRect.left + borderSize, mFaceRect.top + borderSize,
                mFaceRect.right - borderSize, mFaceRect.top - borderSize - lcdPaint.getTextSize());
    }

    @Override protected void drawRim(Canvas canvas) {
        canvas.drawRect(mRimRect, mRimPaint);
        canvas.drawRect(mRimRect, mRimBorderPaint);
    }

    @Override protected void drawFace(Canvas canvas) {
        canvas.drawRect(mFaceRect, mFacePaint);
        canvas.drawRect(mFaceRect, mRimBorderPaint);
    }

    @Override protected void drawTitle(Canvas canvas) {
        canvas.drawText(mCaster.getBroadcasterSerial(), mFaceRect.left - 0.1f,
                mFaceRect.top + 0.1f, mTitlePaint);
    }
    @Override protected void drawForeground(Canvas canvas) {
        String display = "000000" + String.valueOf(Math.ceil(position));
        String read = display.substring(display.length()-8, display.length() - 2);
        canvas.drawText(read, 0.5f, lcdRect.top + lcdPaint.getTextSize() / 2, lcdPaint);
        /**canvas.drawText(mContext.getResources().getStringArray(R.array.pressureTypes)[measurement],
            0.5f, lcdRect.top + lcdPaint.getTextSize() , detailPaint);*/
    }
}

As for the state passing through the application, I put the type of view and the string name of caster the view is representing into a hashmap. I pass that hashmap to the gridview that will then put all six maps into an array that will represent the locations of the views in the gridview. That array is then held in the application and retrieved on necessity.

This is the gridview. The more I think about, this class is were I think may issues lie.

public class Workbench extends GridView {
    /** Our debugging tag */
    private static final String TAG = "Workbench";
    /** Name of the Workbench. */
    private String mId = "-1";
    /** The title of the Workbench. */
    private String mTitle = "Workbench";
    /** The list of Widgets that will be handled by the bench */
    private GaugeBase[] mContent = new GaugeBase[6];
    /** The current selection from the bench */
    private int mSelection = -1;

    /** When a GaugeBase is moves we want to remove from the adapter. Now we won't lose it.*/
    private GaugeBase mHeldGaugeBase = null;
    private Bitmap mHold = null;
    private boolean mIsHolding = false;
    private float x = -1000f, y = -1000f; // Where the held bitmap should be
    private Bitmap trash;
    private RectF trashBox;

    // The touch listener we will use if we need to move a widget around
    private OnTouchListener mWidgetExchanger = new OnTouchListener() {
        @Override public boolean onTouch(View v, MotionEvent e) {
            int w = getWidth(); int h = getHeight(); 
            float xx = e.getX(); float yy = e.getY();
            switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN: // Fall through
            case MotionEvent.ACTION_MOVE: 
                if (mIsHolding) {
                    x = e.getX() - mHold.getWidth()/2; y = e.getY() - mHold.getHeight()/2;
                    postInvalidate(); break;
                }
            case MotionEvent.ACTION_UP: 
                if (mIsHolding) {
                    if (trashBox.contains(xx, yy)) removeGaugeBase(mSelection);
                    else {
                        if ((xx < w / 2) && (yy < h /3)) makeSwitch(0);
                        else if ((xx > w / 2) && (yy < h /3)) makeSwitch(1);
                        else if ((xx < w / 2) && (yy > h /3) && (yy < h * .666)) makeSwitch(2);
                        else if ((xx > w / 2) && (yy > h /3) && (yy < h * .666)) makeSwitch(3);
                        else if ((xx < w / 2) && (yy > h *.666)) makeSwitch(4);
                        else if ((xx > w / 2) && (yy > h *.666)) makeSwitch(5);
                    }
                    mSelection = -1;
                    //mHeldGaugeBase.destroy(); mHeldGaugeBase = null;
                    mHold.recycle(); mHold = null;
                    trash.recycle(); trash = null;
                    mIsHolding = false;
                    setOnTouchListener(null);
                    x = -1000f; y = -1000f;
                    ((AppionApplication)getContext().getApplicationContext()).vibrate(200); update();
                }
                break;
            }
            return true;
        }
    };

    public Workbench(Context context) { this(context, null); }
    public Workbench(Context context, AttributeSet attrs) { this(context, attrs, 0); }
    public Workbench(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        for (int i = 0; i < mContent.length; i++) {
            mContent[i] = new EmptySpace(getContext());
        }
        setAdapter(new BenchAdapter());
        this.setOnItemClickListener(new OnItemClickListener() {
            @Override public void onItemClick(AdapterView<?> arg0, View view, final int pos, long arg3) {
                if (mContent[pos] instanceof EmptySpace) {
                    CharSequence[] items = {"Analog", "Digital"};
                    AlertDialog.Builder adb = new AlertDialog.Builder(getContext());
                        adb.setTitle("Add a widget?")
                            .setItems(items, new DialogInterface.OnClickListener () {
                                @Override public void onClick(DialogInterface arg0, int position) {
                                    mContent[pos].destroy();
                                    mContent[pos] = null;
                                    SensorBroadcaster s = ((AppionApplication)getContext().getApplicationContext()).
                                        getSensorBroadcaster(AppionApplication.TEST_SENSOR);
                                    switch (position) {
                                    case 0: // Add an Analog GaugeBase to the Workbench
                                        mContent[pos] = new AnalogGauge(getContext());
                                        // TODO: Option to link to a manager
                                        break;
                                    case 1: // Add a digital GaugeBase to the Workbench
                                        mContent[pos] = new DigitalGauge(getContext());
                                        // TODO: Option to link to a manager
                                        break;
                                    } mContent[pos].preInit();
                                    update();
                                }
                            });
                        adb.show();
                } //else new GaugeBaseDialog(getContext(), Workbench.this, (GaugeBase)view, pos).show();
            }
        });
        setOnItemLongClickListener(new OnItemLongClickListener() {
            @Override public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
                    int pos, long arg3) {
                mSelection = pos;
                mHold = mContent[pos].toBitmap();
                mHeldGaugeBase = mContent[pos];
                mHeldGaugeBase.destroy();
                trash = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getContext().getResources(),
                        R.drawable.trash), getWidth() / 10, getHeight() / 10, true);
                trashBox = new RectF(getWidth() / 2 - trash.getWidth()/2, getHeight() - trash.getHeight(),
                        getWidth() /2 + trash.getWidth() /2, getHeight());
                mContent[pos] = new EmptySpace(getContext());
                update();
                mIsHolding = true;
                setOnTouchListener(mWidgetExchanger);
                ((AppionApplication)getContext().getApplicationContext()).vibrate(300);
                return false;
            }
        });
    }

    /**
     * Perform a switch in within the bench. Exchange on slot with another.
     * @param slot The slot of the widgets list that we are switching to.
     */
    public void makeSwitch(int slot) {
        if (mSelection == -1) return;
        Log.i(TAG, "Performing a Widget switch");
        mContent[mSelection].destroy();
        mContent[mSelection] = mContent[slot];
        mContent[slot] = mHeldGaugeBase;
        mContent[slot].preInit();
        mContent[slot].invalidate();
        Log.d(TAG, " mSelection = " + mContent[mSelection] + " slot = " +mContent[slot]);
        update();                                                               
    }
android
memory
bitmap
ddms
asked on Stack Overflow May 27, 2011 by ahodder • edited May 27, 2011 by ahodder

3 Answers

1

I suggest you to take a look at this Video. It helped me in many ways, and I´m also experiencing this problems with Bitmap sizes and vm memory budgets.

And from my experience: I developed the bad habit to call System.gc() everywhere after .recycle(). I know this isn´t good, but it helped me to prevent this force-closes, after hours of debugging why my bitmaps weren´t recycled properly.

answered on Stack Overflow May 27, 2011 by stk • edited Jan 2, 2013 by onkar
1

This is sort of in response to @mah, but it's too long for a comment.

it is always performed in multiples of the system page size

This is not necessarily true. Especially with an android app. There are many different memory allocators with different semantics. However, assuming you're talking about JAVA, as opposed to the NDK (C++), there's even less transparency. Java's virtual machine (or rather, dalvik) will almost certainly over-allocate memory on machine start up and then when the app requests small bits of memory, it will give it to you from that pool.

If it runs out of memory from its pool, it will allocate another block from the OS and add that to its pool and return a block from there.

However, if you request LARGE memory blocks, such as you want for a bitmap for example, the JVM (or dalvik machine) will most likely use the system's mmap method, which maps a portion of memory. Depending on the exact circumstances, it could do a private anonymous map and then give you access to sections of that. Or, it can map a file into memory which is basically a memory view of what's on the disk. This is probably how dalvic handles large bitmap allocations.

and made a method that completely destroyed each view

First, you really have no direct control over that in java. Setting an object to null doesn't delete it. Even if you assume that you only have a single reference to that object, you have to wait until the garbage collector cleans up the object's data.


It's really impossible to tell you what your problem is, or really even a hint. All I can really say is that you're probably allocating space for your bitmaps in more places than you thing. Or you're holding references to them somewhere that they aren't being cleaned.

I just store the state in the application and retrieve it when the new activity loads.

This is pretty vague, but I would first take a look at what you're doing in this. If, say, you're passing bitmaps as parsable objects, you're probably allocating 3-4x the space as necessary. Sending large custom objects and bitmaps through parsable interfaces is EXTREMELY expensive.

I would suggest one of a few things. Either you can lazily load your bitmaps. That is, don't store them anywhere ever. Only pull them up when you need them. This MIGHT be a solution because it could be a case where you think you're outsmarting the compiler. But I guarantee that the compiler is smarter than you at efficient memory usage.

--

Or, you could try the opposite and ONLY load the bitmap on application load, and make sure that each time you display it, you're not creating a new bitmap or anything. That way it's only ever in memory once, and if it truly is too big, you crash early and in a known place.

--

Finally, if you really can't find a solution, and you really think your java process is truly running out of memory, you could re-write the portion that handles the bitmap views in the NDK. That way they won't be garbage collected and recreated without you explicitly doing it.

--

So now the question. Is there a way (method / system call or something) that I can get the complete list of allocations of a process (my apps) and print / display / save to a file / etc... it?

I'm sure there is. But, and this is a big but, it will NOT be by ANY means easy. What you'll probably have to do is replace your system's glibc (that is, the malloc functions specifically) with versions that track who requests memory. However, even this will be obfuscated by the java virtual machine.


Long story short, post some of your code, especially the portions where you're manipulating and saving your views and bitmaps.

Update:

Just from glancing at your code, I would check how often regenerate() is being called, specifically because of this:

mBackground = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas backCanvas = new Canvas(mBackground);

I'm not an expert on java bitmap memory management, but my guess is that that would be expensive.

answered on Stack Overflow May 27, 2011 by Falmarri • edited May 27, 2011 by Falmarri
0

You can look at /proc/self/maps (within the process) or /proc/[process id]/maps, but that's not likely to tell you what you want, and no system call will. When you allocate memory within your process, it is always performed in multiples of the system page size (4kb, perhaps more), even if you allocate only 1 byte -- but it then becomes internally managed. Future allocations come from that block before more memory is received from the system.

answered on Stack Overflow May 27, 2011 by mah

User contributions licensed under CC BY-SA 3.0