Objective-C

Objective-C Object Instance Memory Structure: An In-Depth Analysis

By Seren  |  18 Sep, 2026  |  Leave a comment


The first time I tried to budget memory for a large Objective-C import, I made a neat spreadsheet and got the wrong answer. Each parsed record had an integer, a flag, and a title, so I counted those fields, multiplied by the number of records, and expected Instruments to agree. The process used considerably more memory. My calculation was not useless, but it mixed together three different measurements: pointer size, logical instance size, and allocator block size.

I later made the opposite mistake. I saw a larger allocator block for one object and assumed that its strong NSString property had been copied into the object’s bytes. It had not. The instance stored a pointer; the string had its own storage and lifetime. This article rebuilds that investigation with a small program that you can run on macOS. The goal is not to memorize one set of byte counts. It is to know what each number actually measures.

The Batch Import That Broke My Estimate

The project imported a large text export into a local database. I represented each row as a temporary ParsedRecord. It contained an NSInteger identifier, a BOOL indicating whether validation passed, and an NSString title. During a 100,000-row run, peak memory grew much faster than my field-size calculation predicted.

The missing costs came from several places. Every instance includes inherited object storage. Alignment can leave gaps between ivars. The allocator may provide a block larger than the logical class layout. The title is a separate object, and parsing creates temporary strings and collections. The import array also reserves capacity for references. Looking only at the three declared ivars ignored most of the ownership graph.

Once I processed records in bounded batches and released intermediate parsing results, the peak dropped. Changing ivar order was not the important fix. Measurement led to an ownership and lifetime fix, which is a useful pattern for most memory investigations.

Start With Three Different Size Questions

sizeof(record) answers a C language question: how large is the local pointer variable? On a 64-bit process it is normally 8 bytes. It does not inspect the referenced object. The same result applies whether the pointer refers to an empty object, a large model, or nil.

class_getInstanceSize([ParsedRecord class]) asks the Objective-C runtime for the size of instances of that class. This includes inherited instance storage and the class’s ivar layout. It does not recursively add the bytes owned by the title string, an array, an image, or anything else reached through a pointer.

malloc_size reports the size of the allocator block associated with a live allocation on Darwin. That makes it useful as a local diagnostic, but it is allocator-specific rather than a portable Objective-C rule. The block may be larger than the logical instance because allocators use alignment and size classes. A value observed on my Mac is not a promise for every iPhone, architecture, OS release, or future allocator.

What Is Actually Inside an Instance?

An Objective-C instance contains runtime-required inherited storage and its instance variables. Conceptually, the runtime can determine the object’s class and use that class metadata for method lookup and introspection. It is common to explain this using an isa field, and Clang-generated teaching output can make the layout easier to visualize.

However, application code should not hard-code a private isa_t bitfield copied from one objc4 release. The bit allocation has changed across architectures and runtime versions. Details such as inline reference-count bits, masks, and flags are valuable when debugging a particular system, but they are not stable application contracts. The supported API object_getClass expresses the question without requiring the program to decode private storage.

Methods are also not copied into every instance. Method metadata belongs to the class structures used by the runtime. Adding methods can affect executable and metadata size, but it does not append function bodies to each object. Properties usually imply accessor methods and, when synthesized with backing storage, an ivar; it is the ivar that contributes to the instance layout.

Ivar Offsets Explain Padding Better Than Guesswork

The runnable sample uses class_getInstanceVariable and ivar_getOffset to print the offsets selected for _identifier, _accepted, and _title. On my current arm64 Mac, they appear at offsets 8, 16, and 24, and the logical instance size is 32 bytes. Those are measured outputs from this environment, not constants used by the tests.

Alignment means that the next field may not begin immediately after the previous field’s last meaningful byte. A pointer generally needs stricter alignment than a one-byte Boolean, so padding can appear before it. The compiler and runtime cooperate on a valid layout for the target ABI.

I do not rely on a broad claim that Objective-C always reorders every ivar into the smallest possible arrangement. Layout behavior is constrained by inheritance, ABI compatibility, runtime rules, visibility, and the compiler. If a byte-level layout genuinely matters, measure the actual target and keep assumptions behind tests. For normal application models, clearer declarations are usually worth more than speculative micro-optimization.

Referenced Objects Are Not Inline Payloads

The _title ivar stores an object reference. The characters of an arbitrary string do not become extra trailing bytes in every ParsedRecord. The string object may use its own allocation strategy, may share storage, or may even use an optimized representation. Those are separate implementation questions.

This corrects a tempting but inaccurate explanation for an unexpectedly large model. Several strong string properties add several pointer-sized ivars to the host object’s logical layout. The strings they reference contribute to retained memory elsewhere. Padding can change the host instance size, but it is wrong to add the strings’ character buffers directly to that instance and call the sum its malloc_size.

In Instruments, I therefore separate the “how many model objects exist?” question from “what graph do they retain?” The Allocations instrument helps with counts and allocation stacks. Memory Graph can reveal ownership relationships. Leaks and Address Sanitizer answer different questions again. One number should not be stretched into every diagnosis.

Why Minimum Object Size Is Not a Universal Rule

On common Apple 64-bit systems, a small object may show an 8-byte logical size and a 16-byte allocator block. That observation is useful. Turning it into “all Objective-C objects always occupy at least 16 bytes” is too broad. Tagged pointers, different allocators, runtime changes, object classes with custom allocation behavior, and platform differences complicate the statement.

The demo deliberately checks only a relationship that should hold for its ordinary heap object in the tested process: the observed allocator block must be large enough for the runtime-reported instance layout. It prints the exact values so readers can compare environments. It does not fail merely because a future machine chooses different valid sizes.

This distinction is important for E-E-A-T as well as engineering. A trustworthy technical article labels observations, public contracts, and private implementation notes separately. A precise limit on a claim is more useful than an impressive-sounding universal number.

Run the Complete Memory Layout Project

The repository contains a Foundation command-line program, a Makefile, a README, and a .gitignore. It requires macOS and the Xcode Command Line Tools. Run it with:

git clone https://github.com/2252408699/objc-object-memory-layout-demo.git
cd objc-object-memory-layout-demo
make run

The program builds with ARC and treats compiler warnings as errors. It verifies that all three ivars are discoverable, that inherited storage is included, that the reported offsets are ordered as expected for this class, and that the observed allocator block can contain the logical instance. A successful run prints eight PASS lines and ends with All memory-layout checks passed.

The sample assigns a real title but reports its length separately. This makes the ownership boundary visible: the host instance has a title pointer at a reported offset, while the referenced string has its own representation. Change the title text and rerun the program; the logical size of ParsedRecord should not grow with the character count.

The Checklist I Use Before Optimizing Models

  • Confirm whether the number is pointer size, runtime instance size, allocator block size, or total retained memory.
  • Measure object counts and allocation call stacks in a repeatable workload.
  • Inspect ivar offsets instead of guessing where padding appears.
  • Count referenced objects separately from pointer fields in the host instance.
  • Treat private objc4 layouts as version-specific research, not stable API.
  • Optimize lifetimes and ownership before rearranging a few small fields.

For my importer, this checklist replaced a tidy but misleading spreadsheet with a useful experiment. Batching reduced the peak far more than layout speculation ever could. The size APIs remained valuable because each answered a narrowly defined question.

Complete runnable example: objc-object-memory-layout-demo on GitHub

Seren
Seren

A developer exploring iOS, Objective-C, Swift, and other tech stacks. Here to document my learning process, pitfalls, and growing journey across new technologies.

Your email address will not be published. Required fields are marked *