TL;DR:
The virtual machine is a concept that represents how one could decouple the actual instructions that your CPU will use to execute your program and your human-readable source code. You, the programmer, are able to write less code that does more.
Longer answer
I'm pretty sure that you're looking for an answer that's much simpler than the classifications that you outlined in your question. Typically, a virtual machine simply stands for the abstraction that is inserted between the source code that you've written and how the physical hardware receives those instructions.
All of those nitty-gritty details that are needed to talk to the processor are handled by a "virtual machine", which itself automates many of the repetitive commands that might be required to do a very simple operation, like manipulating a string UTF-8 encoded characters and printing them to the command line. Python uses one, Java uses one, and the class of "Language Engine" that you outlined above is just a fancier name for the same concept of a virtual machine. Any VM can be as fast and lightweight as the VM programmer designs it to be, which will play into both the usability and reliability of the applications are developed for it.
As is the case when using Java and Python, you're able to write programs using verbose languages that have very little in common with the "language" that the physical processor needs in order to execute commands. Thankfully, people that are far smarter than I am have created programs using that processor-specific language, called the "assembler" or "assembly" languages, which differ between instruction set architectures (e.g. RISC-V, x8/x86-64, ARMv8). These programs are what can translate what you've written into that processor-specific language procedurally, essentially acting as a translation layer.
What you could potentially end up with is a far simpler interface between you, the developer, and the hardware that you're trying to leverage. To boot, the only thing that you need in order to run the program across different OS environments is for the VM to have been implemented in whatever assembly language is used by the physical host processor.
Note: I say can because of the very subjective nature that is the appreciation of any given language's syntax.
To give a high-level example, the Python "interpreter" that we can use both to execute pre-written scripts and interact with in a command-line application relies on a compiler that creates the Python bytecode from your code, which is then fed into the so-called Python Virtual Machine for processor execution.
Also, there's no real difference between a virtual machine and a "language engine". The JavaScript language engines, such as Google's V8, are simply implementations of this virtual machine concept that allows for developers to rely on platform agnosticism and portability when developing dynamic, web-based applications without worrying about breaking the usability of their programs.
If you're interested, look into the various implementations of Python (Jython, IronPython, etc.). These use the conceptual virtual machines implemented by other languages/frameworks, like Java and .NET, to create an implementation of Python that uses the same syntax, but gets cross-compiled into Java bytecode or Common Intermediate Language bytecode instructions to achieve the same results. The "python interpreter" that is almost universally referred to is the C-implemented interpreter that is officially maintained by the Python Software Foundation, colloquially named "CPython".
Just to really drive it home, here's a prime-checking algorithm written in Python, C++ using C libraries, and x86 ASM (x86 assembly language) by xmdi on youtube, and you'll be able to see how the Python VM enables the kind of syntax that allows people to easily get into programming without losing their mind trying to juggle register manipulation using assembly language:
Python:
def isPrime(n):
for i in range(2,n//2+1):
if (not (n%i)):
return 0
return 1
numPrimes = 0
for i in range(2,250001):
numPrimes+=isPrime(i)
print(str(numPrimes))
C++:
#include <stdio.h>
int isPrime(int n){
for (int i = 2; i <= n/2; i++){
if (!(n%2)){
return 0;
}
}
return 1;
}
int main(){
int numPrimes = 0;
for (int i = 2; i <= 250001; i++){
numPrimes += isPrime(i);
}
printf("%d\n", numPrimes);
return 0;
}
x86 ASM:
.section .data
f: .string "%d/n"
.section .text
.globl main
main:
movl $2, %eax
xor %r8d,%r8d
loop:
cmpl $250000,%eax
jg end_loop
movl $2, %r10d
movl %eax,%r11d
shr $1,%r11d
prime_loop:
cmpl %r11d,%r10d
jg prime
push %rax
xor %edx,%edx
div %r10d
test %edx,%edx
pop %rax
je not_prime
inc %r10d
jmp prime_loop
prime:
inc%r8d
not_prime:
inc %eax
jmp loop
end_loop:
lea f(%rip),%rdi
mov %r8d,%esi
xor %eax,%eax
call printf