Let's say we have a block with lots of blockstates. How much memory does it use?
We can use Java Object Layout to get an estimate of how many bytes an instance of an object uses as described here: https://www.baeldung.com/jvm-measuring-object-sizes
So let's say we create a bunch of blocks with different blockstate properties. Java Object Layout has a graph-walking tool that will find every object reachable from a given instance, but most of these objects we find will be shared by other blocks.
However, Java Object Layout does let us get a graph layout for two objects and subtract the references in one from the other, so we can make two unique copies of each block and discard the duplicate references to get a better estimate of the additional RAM that creating a new block would allocate.
[15:37:09] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 27520 bytes
[15:37:09] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 1 boolean properties needs 2000 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 2 boolean properties needs 4504 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 3 boolean properties needs 8896 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 4 boolean properties needs 17256 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 5 boolean properties needs 36608 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 6 boolean properties needs 74952 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 7 boolean properties needs 159776 bytes
[15:37:11] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 8 boolean properties needs 333928 bytes
[15:37:11] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 9 boolean properties needs 716480 bytes
[15:37:11] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 10 boolean properties needs 1566984 bytes
[15:37:12] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 11 boolean properties needs 3475976 bytes
[15:37:14] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 12 boolean properties needs 7105736 bytes
[15:37:22] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 13 boolean properties needs 14935000 bytes
[15:37:42] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 14 boolean properties needs 30745160 bytes
[15:38:45] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 15 boolean properties needs 64890016 bytes
That first block with 0 properties looks like an outlier, so let's create some more blocks with 0 properties
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 24960 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
That makes more sense.
Let's compile that into a nice table as well:
https://docs.google.com/spreadsheets/d/1TYl4gdsHtxcYSXImVDVqswCGzTcjwAzpmn29vNcKhTo/edit?usp=sharing
Why does ram per state increase with the number of states? Minecraft caches a lookup table for state transformations (e.g. calling state.with(someProperty, someValue)), which needs more combinations as the number of blockstate properties increases.