The trick I've used in the past is to start by tackling a single, small, isolated feature change. After you finish it, start with another one that's related to this one. Keep doing it until you've covered the whole codebase, making sure to only do small ones! This trick doesn't work.
In a nutshell, find the main/entry method, then walk the tree from there. That takes you from most general/big picture to most specific/granular and helps in mapping out the structure.<p>Obviously for big code bases you can't do it all in a day, but just having a big picture model in my head when I resume on subsequent days helps fit it all together.
If you are lucky enough that the large and complex codebase has extensive system-level and unit-tests, then that is where I would start.<p>Hopefully it is easy to get the codebase up and running so that you can explore it hands-on and try both happy-path cases and what happens when things go awry.<p>Walk through a feature of the code manually or step through it using a debugger while drawing boxes on how stuff fits together, and what the main building blocks are.<p>It will always take time to really understand a new large and complex codebase. If it is high-quality code it is easier, if you have experience with similar products it is easier. But to really feel that I grasp the really good parts and the dark and dangerous corners of a new codebase always takes me months.