Ci sono due principali differenze tra i due.
Primo: la JVM è astratta, PyPy è concreta. La JVM è una specifica, un pezzo di carta. PyPy è un'implementazione, una parte di codice.
Ci sono molte diverse implementazioni della JVM che funzionano in modo molto diverso. Alcuni interpretano solo il codice byte JVM, alcuni lo compilano solo in anticipo, alcuni lo compilano solo in modo dinamico, alcuni hanno sia un interprete che un compilatore JIT, alcuni hanno un interprete e più compilatori JIT, alcuni non hanno interprete e solo un compilatore JIT, alcuni non hanno interprete e più compilatori JIT. Alcuni hanno JIT di tracciamento, alcuni hanno JIT method-at-time, alcuni hanno entrambi. Alcuni hanno thread nativi, alcuni hanno fili verdi. Alcuni hanno i netturbini di rintracciamento, alcuni hanno un conteggio dei rifiuti. E così via e così via.
Secondo: PyPy è più generale. Non è un'implementazione di un linguaggio specifico, è un framework per creare facilmente implementazioni linguistiche efficienti. Ci sono molte implementazioni linguistiche differenti create usando il framework PyPy, c'è Topaz (un'implementazione di Ruby), HippyVM (un'implementazione di PHP), Pyrolog (Prolog), RSqueak (a Squeak VM), PyGirl (un emulatore GameBoy), langjs (JavaScript) e anche implementazioni di Io e Scheme. E ovviamente anche un'implementazione di Python.
Dato che hai chiesto informazioni specifiche sui compilatori, c'è una distinzione molto importante tra il JIT di PyPy e i compilatori JIT di altri motori in modalità mista. In un tipico motore in modalità mista (ad esempio, Oracle HotSpot JVM, IBM J9 JVM, Rubinius, Apple Squirrelfish FX, ...), l'interprete e il compilatore vengono eseguiti parallelamente e elaborano lo stesso programma. L'interprete parte, interpreta il programma e, una volta stabilito che sarebbe utile compilare (parti di) il programma, il programma viene trasferito al compilatore e compilato.
In PyPy, tuttavia, il compilatore non compila il programma interpretato dall'interprete. Compila il interprete stesso in quanto interpreta il programma!
Ora, perché dovresti fare qualcosa del genere? Pensa a cosa significa: se JIT compila l'interprete mentre interpreta il programma, ciò che si finisce con, è una versione specializzata dell'interprete che può solo interpretare quell'unico programma, il tutto compilato in codice nativo. Ma un interprete che può interpretare solo un singolo programma è indistinguibile da quel programma. Quindi, in altre parole, hai appena compilato quel programma senza nemmeno sapere nulla di quel programma!
Questo ha a che fare con PyPy inteso come un framework: in questo modo, hai solo bisogno di un one compilatore JIT e funziona per le tutte lingue! L'unica cosa che devi scrivere se vuoi implementare una nuova lingua nel framework PyPy, è l'interprete. Ottieni il compilatore JIT "gratuitamente". E l'interprete può essere molto semplice, non deve eseguire ottimizzazioni aggressive o così, perché il compilatore JIT è abbastanza buono. (Ad esempio, HippyVM, l'implementazione PHP che utilizza PyPy, è quasi 8 volte più veloce di Zend Engine (l'implementazione standard di PHP) e due volte più veloce di HHVM di implementazione PHP ad alte prestazioni ottimizzata di Facebook.