La raison principale qui devrait te pousser à générer du bytecode vers une VM existante, c'est surtout les performances générales et le support d'implémentation déjà créé. Tu pourras jamais égaler, pour un langage générique de programmation, le niveau de détail et d'optimisation qui a été taillé, à coup de millions de $$$ et d'armadas d'ingénieurs, dans des VM comme la JVM, CLR, BEAM ou LLVM.
Ceci étant dit, les avantages annexes sont aussi nombreux ; écrire du bytecode de VM reste du bytecode encore assez haut-niveau ; c'est bien plus facile que de générer de l'asm. Et le bytecode d'une VM donnée est portable, là où tu devras apprendre et porter ton code en ASM pour chaque cible.
Le bytecode custom c'est la moins bonne idée du tas pour un débutant (sauf si tu veux apprendre cette facette-là) parce que tu dois définir un langage assembleur, généralement stack-based, pour ta VM, ainsi que le code permettant de l'interpréter, avec toutes les conventions à respecter et ce que ça implique. C'est très clairement le plus gros travail, pour un bénéfice pas très clair à ton niveau.
Pour le bytecode Java, au contraire, c'est un bytecode très lisible et bien plus haut niveau que de l'assembleur machine traditionnel. Tu peux te renseigner sur les specs officielles (https://docs.oracle.com/javase/specs/), ainsi que des livres écrits sur le sujet, comme http://www.amazon.com/Java-Virtual-Machine-Series/dp/1565921941, qui a notamment donné lieu au développement de Jasmin, un assembleur qui permet de générer du bytecode jvm avec un bytecode encore plus haut niveau.