http://hhvm.com/blog/431/on-garbage-collection
and some other stuff from page 6 forward: http://hhvm.com/blog/page/6
For example, if you've figured out a small increase in reading a value from a table, you have sped up all other data structures, OO code, etc., because the table is used for everything.
1. Boot, setting up system wide resources like DB/memcached connections
2. Start doing work
Then any process forked after step #1 is sharing those sockets and can accidentally corrupt the work that the parent process is doing. With threads, you can share the connections through a pool that can be simply mediated within the process.
Yeah, you can design around it. But with threads, you don't have to.
Here's another: serialisation. If you want to parallelise any processing that builds up any complex data structures, you have to serialise them between your parent/child processes which can make it hard to keep the "richness" of the objects like interdependencies between processing runs. In addition, now your processing bottleneck becomes how fast your single-threaded parent process can deserialise the responses (which in Python in particular can really be quite slow).
And not everything can be serialised. It's not trivial to share sockets with other processes that have already been started. You can't easily accept a connection in one process and pass it to another to do the work and send the response (it's possible, just not as easy as passing an integer around like you can do between threads).