【Java】网络编程(Socket)

网络编程

Socket

我们开发的网络应用程序位于应用层,TCP和UDP属于传输层协议,在应用层如何使用传输层的服务呢?在应用层和传输层之间,则使用套接字Socket来进行分离

套接字就像是传输层为应用层开的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,是不知道也不需要知道的,也不会关心它如何传输,这属于网络其他层次工作

Socket实际是传输层供给应用层的编程接口。Socket就是应用层与传输层之间的桥梁。使用Socket变成可以开发客户机和服务器的应用程序,可以在本地网络上通信,也可以通过Internet在全球范围内通信

Java网络编程中的常用类

Java为了跨平台,在网络应用通信时是不允许直接调用操作系统接口的,而是由java.net包来提供网络功能。

InetAddress的使用

作用:封装计算机的IP地址和DNS(没有端口信息)

特点:

这个类没有构造方法,如果想要得到对象,只能通过静态方法:getLocalHost() 、 getByName() 、 getALLByName() 、 getAddress() 、 getHostName()

获取本机信息

获取本机信息需要使用getLocalHost()方法创建InetAddress对象,这个对象包含了本机的IP地址,计算机名等信息

java 复制代码
public class InetTest {
    public static void main(String[] args) {
        try {
            InetAddress localHost = InetAddress.getLocalHost();
            String hostAddress = localHost.getHostAddress();
            String hostName = localHost.getHostName();
            System.out.println(hostAddress);
            System.out.println(hostName);
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }
}

根据域名获取计算机的信息

根据域名获取计算机信息时需要使用getByName("域名")方法创建InetAddress对象

java 复制代码
public class InetTest2 {
    public static void main(String[] args) {
        try {
            InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
            System.out.println(inetAddress.getHostAddress());
            System.out.println(inetAddress.getHostName());

        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }
}

根据IP地址获取计算机的信息

根据IP地址获取计算机的信息时需要使用getByName("IP")方法创建InetAddress对象

java 复制代码
public class InetTest3 {
    public static void main(String[] args) {
        try {
            InetAddress inetAddress = InetAddress.getByName("110.242.68.4");
            System.out.println(inetAddress.getHostName());
            System.out.println(inetAddress.getHostAddress());
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }
}
InetSocketAddress的使用

作用:包含IP和端口信息,常用于Socket通信。此类实现IP套接字地址(IP地址+端口号),不依赖任何协议

InetSocketAddress相比较InetAddress多了一个端口号,端口的作用:一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现

java 复制代码
public class InetSocketTest {
    public static void main(String[] args) {
        InetSocketAddress inetSocketAddress = new InetSocketAddress("www.baidu.com",80);
        System.out.println(inetSocketAddress.getAddress().getHostAddress());
        System.out.println(inetSocketAddress.getHostName());
    }
}
URL的使用

IP地址标识了Internet上唯一的计算机,而URL则标识了这些计算机上的资源。URL代表一个资源定位符,它是指向互联网"资源"的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,例如对数据库或者搜索引擎的查询

为了方便程序员编程,JDK中提供了URL类,该类的全名是java.net.URL,有了这样一个类,就可以使用它的各种方法来对URL对象进行分割、合并等处理

java 复制代码
public class UrlTest {
    public static void main(String[] args) {
        try {
            URL url = new URL("http://www.edu2act.cn/task/list/finished/");
            System.out.println("获取当前协议的默认端口:" + url.getDefaultPort());
            System.out.println("访问资源:" + url.getFile());
            System.out.println("主机名" + url.getHost());
            System.out.println("访问资源的路径:" + url.getPath());
            System.out.println("协议" + url.getProtocol());
            System.out.println("参数部分" + url.getQuery());
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
}

通过URL实现最简单的网络爬虫

java 复制代码
public class UrlTest2{
    public static void main(String[] args)throws Exception {
        URL url = new URL("http://dbms.wangding.co/design/ch04-database-design-1/");
        try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) {
            StringBuilder sb = new StringBuilder();
            String temp;
            while ((temp = br.readLine()) != null) {
                sb.append(temp);
            }
            System.out.println(sb);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
TCP通信的实现和项目案例
TCP通信实现原理

前边我们提到TCP协议是面向的连接的,在通信时客户端与服务器端必须建立连接。在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。

请求-相应 模式

  • Socket类:发送TCP信息
  • ServerSocket类:创建服务器

套接字Socket是一种进程间的数据交换机制。这些进程既可以在同一机器上,也可以在通过网络连接的不同机器上。换句话说,套接字起到通信端点的作用。单个套接字是一个端点,而一对套接字则构成一个双向通信信道,使非关联进程可以在本地或通过网络进行数据交换。一旦建立套接字连接,数据即可在相同或不同的系统中双向或单向发送,直到其中一个端点关闭连接。套接字与主机地址和端口地址相关联。主机地址就是客户端或服务器程序所在的主机的IP地址。端口地址是指客户端或服务器程序使用的主机的通信端口。

在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样,客户端和服务器通过套接字所建立的连接使用输入输出流进行通信。

TCP/IP套接字是最可靠的双向流协议,使用TCP/IP可以发送任意数量的数据。

实际上,套接字只是计算机上已编号的端口。如果发送方和接收方计算机确定好端口,他们就可以通信了。

客户端与服务器端的通信关系图:

TCP/IP通信连接的简单过程

位于A计算机上的TCP/IP软件向B计算机发送包含端口号的消息,B计算机的TCP/IP软件接收该消息,并进行检查,查看是否有它知道的程序正在该端口上接收消息。如果有,他就将该消息交给这个程序。

通过Socket的编程顺序

1、创建服务器ServerSocket,在创建时,定义ServerSocket的监听端口(在这个端口接收客户端发来的消息)

2、ServerSocket调用accept()方法,使之处于阻塞状态。

3、创建客户端Socket,并设置服务器的IP及端口。

4、客户端发出连接请求,建立连接。

5、分别取得服务器和客户端Socket的InputStream和OutputStream。

6、利用Socket和ServerSocket进行数据传输。

7、 关闭流及Socket。

TCP通信入门案例

创建服务端

java 复制代码
public class BasicSocketServer {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器已启动,等待监听....");
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(8888);
            Socket socket = serverSocket.accept();
            //连接成功后会得到与客户端对应的Socket对象,并解除线程阻塞
            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            System.out.println(br.readLine());
            OutputStream out = socket.getOutputStream();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            serverSocket.close();
        }
    }
}

创建客户端

java 复制代码
public class BasicSocketClient {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        PrintWriter pw = null;
        try {
            socket = new Socket("127.0.0.1",8888);
            OutputStream out = socket.getOutputStream();
            pw = new PrintWriter(out);
            pw.write("服务端,你好!");
            pw.flush();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            pw.close();
            socket.close();
        }
    }
}
TCP单向通信

单向通信是指通信双方中,一方固定为发送端,一方固定为接收端

创建服务端

java 复制代码
public class OneWaySocketServer {
    public static void main(String[] args) {
        System.out.println("服务器启动,开始监听");
        try(ServerSocket serverSocket = new ServerSocket(8888);) {
            Socket socket = serverSocket.accept();
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            System.out.println("连接成功");
            while (true){
                String str = br.readLine();
                System.out.println("客户端说" + str);
                if ("exit".equals(str)){
                    break;
                }
                pw.println(str);
                pw.flush();
            }
        } catch (IOException e) {
            System.out.println("服务器启动失败");
            throw new RuntimeException(e);
        }
    }
}

创建客户端

java 复制代码
public class OneWaySocketClient {
    public static void main(String[] args) {
        try(Socket socket = new Socket("127.0.0.1",8888)) {
            Scanner scanner = new Scanner(System.in);
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (true){
                String s = scanner.nextLine();
                pw.println(s);
                pw.flush();
                if ("exit".equals(s)){
                    break;
                }
                String serverInput = br.readLine();
                System.out.println("服务器返回的" + serverInput);

            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
TCP双向通信

双向通信是指通信双方中,任何一方都可为发送端,任何一方都可为接收端

服务端

java 复制代码
public class TwoWaySocketServer {
    public static void main(String[] args) {
        System.out.println("服务器启动,监听8888端口");
        try(ServerSocket serverSocket = new ServerSocket(8888);) {
            Socket socket = serverSocket.accept();
            //创建键盘输入对象
            Scanner scanner = new Scanner(System.in);
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            while (true){
                String str = br.readLine();
                System.out.println("客户端说:" + str);
                String keyInput = scanner.nextLine();
                pw.println(keyInput);
                pw.flush();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

客户端

java 复制代码
public class TwoWaySocketClient {
    public static void main(String[] args) {
        try(Socket socket = new Socket("127.0.0.1",8888)) {
            Scanner scanner = new Scanner(System.in);
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
            while (true){
                String keyInput = scanner.nextLine();
                pw.println(keyInput);
                pw.flush();
                String str = br.readLine();
                System.out.println("服务端说:" + str);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
创建点对点的聊天应用

创建服务端

主线程

java 复制代码
public class ChatSocketServer {
    public static void main(String[] args) {
        try(ServerSocket serverSocket = new ServerSocket(8888)) {
            System.out.println("服务端启动,等待连接");
            Socket socket = serverSocket.accept();
            new Thread(new SendThread(socket)).start();
            new Thread(new ReceiveThread(socket)).start();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

接收消息线程

java 复制代码
public class ReceiveThread implements Runnable{
    private Socket socket;
    public ReceiveThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        this.receiveMsg();
    }
    private void receiveMsg(){
        try(BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));){
            while (true){
                String msg = br.readLine();
                System.out.println("客户端说:" + msg);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

发送消息线程

java 复制代码
public class SendThread implements Runnable{
    private Socket socket;
    public SendThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        this.sendMsg();
    }
    private void sendMsg(){
        try(Scanner scanner = new Scanner(System.in);
            PrintWriter pw = new PrintWriter(socket.getOutputStream());
        ){
            while (true){
                String msg = scanner.nextLine();
                pw.println(msg);
                pw.flush();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

创建客户端

主线程

java 复制代码
public class ChatSocketClient {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1",8888);
            System.out.println("连接成功");
            new Thread(new ClientSendThread(socket)).start();
            new Thread(new ClientReceiveThread(socket)).start();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

接收消息线程被

java 复制代码
public class ClientReceiveThread implements Runnable{
    private Socket socket;
    public ClientReceiveThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        receiveMsg();
    }

    private void receiveMsg() {
        try(BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
            while (true){
                String msg = br.readLine();
                System.out.println("服务端说:" + msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

发送消息线程

java 复制代码
public class ClientSendThread implements Runnable{
    private Socket socket;
    public ClientSendThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        this.sendMsg();
    }

    private void sendMsg() {
        try(Scanner scanner = new Scanner(System.in);
            PrintWriter pw = new PrintWriter(socket.getOutputStream())){
            while (true){
                String msg = scanner.nextLine();
                pw.println(msg);
                pw.flush();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
优化点对点的聊天应用
java 复制代码
public class GoodTCP {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        Socket socket = null;
        try {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入:server,<port> 获取 <ip>,<port>");
            String str = scanner.nextLine();
            String[] arr = str.split(",");
            if("server".equals(arr[0])){
                System.out.println("TCP Server Listen at" + arr[1] + "......");
                serverSocket = new ServerSocket(Integer.parseInt(arr[1]));
                socket = serverSocket.accept();
                new Receive(socket);
            }else {
                socket = new Socket(arr[0],Integer.parseInt(arr[1]));
                System.out.println("连接成功");
            }
            new Thread(new Send(socket, scanner)).start();
            new Thread(new Receive(socket)).start();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(serverSocket != null){
                serverSocket.close();
            }
        }
    }
}
java 复制代码
public class Send implements Runnable{
    private Socket socket;
    private Scanner scanner;

    public Send(Socket socket, Scanner scanner) {
        this.socket = socket;
        this.scanner = scanner;
    }

    @Override
    public void run() {
        this.sendMsg();
    }

    private void sendMsg(){
        try(PrintWriter pw = new PrintWriter(socket.getOutputStream());
        ){
            while (true){
                String msg = scanner.nextLine();
                pw.println(msg);
                pw.flush();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
java 复制代码
public class Receive implements Runnable{
    private Socket socket;
    public Receive(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        sendMsg();
    }
    private void sendMsg(){
        try(BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));){
            while (true){
                String msg = br.readLine();
                System.out.println("客户端说:" + msg);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
一对多应用

服务端应该将serverSocket.accept()放入while(true)循环中

一对多聊天服务器设计

难点在于解决线程同步

当没有消息发送时,发送线程处于等待状态,当接收线程接收到消息后,唤醒所有等待的发送线程

java 复制代码
public class ChatRoomServer {
    public static String buf;
    public static void main(String[] args) {
        System.out.println("Chat Server Version 1.0");
        System.out.println("Listen at 8888......");
        try(ServerSocket serverSocket = new ServerSocket(8888)){
            while (true){
                Socket socket = serverSocket.accept();
                System.out.println("连接到" + socket.getInetAddress());
                new Thread(new ChatReceiveThread(socket)).start();
                new Thread(new ChatSendThread(socket)).start();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
java 复制代码
public class ChatReceiveThread implements Runnable {
    private Socket socket;

    public ChatReceiveThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        receiveMsg();
    }

    private void receiveMsg() {
        try(BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));){
            while (true){
                String msg = br.readLine();
                synchronized ("abc"){
                    ChatRoomServer.buf = "[" + this.socket.getInetAddress() + "]" + msg;
                    "abc".notifyAll();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
java 复制代码
public class ChatSendThread implements Runnable{
    Socket socket;

    public ChatSendThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        sendMsg();
    }

    private void sendMsg() {
        try(PrintWriter pw = new PrintWriter(socket.getOutputStream());){
            while (true){
                synchronized ("abc"){
                    //先让发送消息的线程处于等待状态
                    "abc".wait();
                    //将公共数据区的数据发送给客户端
                    pw.println(ChatRoomServer.buf);
                    pw.flush();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
UDP通信实现原理

UDP协议与之前讲到的TCP协议不同,是面向无连接的,双方不需要建立连接便可通信。UDP通信所发送的数据需要进行封包操作(使用DatagramPacket类),然后才能接收或发送(使用DatagramSocket类)。

DatagramPacket:数据容器(封包)的作用

此类表示数据报包。 数据报包用来实现封包的功能。

常用方法

方法名 使用说明
DatagramPacket(byte[] buf, int length) 构造数据报包,用来接收长度为 length 的数据包
DatagramPacket(byte[] buf, int length, InetAddress address, int port) 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号
getAddress() 获取发送或接收方计算机的IP地址,此数据报将要发往该机器或者是从该机器接收到的
getData() 获取发送或接收的数据
setData(byte[] buf) 设置发送的数据

DatagramSocket:用于发送或接收数据报包

当服务器要向客户端发送数据时,需要在服务器端产生一个DatagramSocket对象,在客户端产生一个DatagramSocket对象。服务器端的DatagramSocket将DatagramPacket发送到网络上,然后被客户端的DatagramSocket接收。

DatagramSocket有两种常用的构造函数。一种是无需任何参数的,常用于客户端;另一种需要指定端口,常用于服务器端。如下所示:

  • DatagramSocket() :构造数据报套接字并将其绑定到本地主机上任何可用的端口。
  • DatagramSocket(int port) :创建数据报套接字并将其绑定到本地主机上的指定端口。

常用方法

方法名 使用说明
send(DatagramPacket p) 从此套接字发送数据报包
receive(DatagramPacket p) 从此套接字接收数据报包
close() 关闭此数据报套接字

UDP通信编程基本步骤:

1、创建客户端的DatagramSocket,创建时,定义客户端的监听端口。

2、创建服务器端的DatagramSocket,创建时,定义服务器端的监听端口。

3、在服务器端定义DatagramPacket对象,封装待发送的数据包。

4、客户端将数据报包发送出去。

5、服务器端接收数据报包。

UDP通信入门案例

服务端

java 复制代码
public class UDPServer {
    public static void main(String[] args) {
        //创建服务端接收数据的DatagramSocket对象
        try(DatagramSocket datagramSocket = new DatagramSocket(9999)){
            //创建数据缓冲区
            byte[] b = new byte[1024];
            //创建数据报包对象
            DatagramPacket datagramPacket = new DatagramPacket(b,b.length);
            //等待接收客户端所发送的数据
            datagramSocket.receive(datagramPacket);
            //取出数据
            String str = new String(datagramPacket.getData(),0,datagramPacket.getLength());
            System.out.println(str);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

客户端

java 复制代码
public class UDPClient {
    public static void main(String[] args) {
        try(DatagramSocket datagramSocket = new DatagramSocket(8888);) {
            byte[] bytes = "Triticale".getBytes();
            DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length,new InetSocketAddress("127.0.0.1",9999));
            datagramSocket.send(datagramPacket);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
基本数据类型通信

服务端

java 复制代码
try(DataInputStream dis = new DataInputStream(new ByteArrayInputStream(datagramPacket.getData()));){
	System.out.println(dis.readLong());
}

客户端

java 复制代码
long n = 2000l;
try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
	DataOutputStream dos = new DataOutputStream(bos);){
	dos.writeLong(n);
	byte[] arr = bos.toByteArray();
	DatagramPacket datagramPacket1 = new DatagramPacket(arr,arr.length,new InetSocketAddress("127.0.0.1",9999));
}
传递自定义数据类型

创建服务端

java 复制代码
public class ObjectTypeUDPServer {
    public static void main(String[] args) {
        try(DatagramSocket datagramSocket = new DatagramSocket(9999)){
            byte[] bytes = new byte[1024];
            DatagramPacket datagramPacket = new DatagramPacket(bytes,bytes.length);
            datagramSocket.receive(datagramPacket);
            try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(datagramPacket.getData()))){
                Person person = (Person) ois.readObject();
                System.out.println(person.toString());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

创建客户端

java 复制代码
public class ObjectTypeUDPClient {
    public static void main(String[] args) {
        try(DatagramSocket datagramSocket = new DatagramSocket(8888);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos)){
            Person person = new Person();
            person.setName("张三");
            person.setAge(18);
            oos.writeObject(person);
            byte[] byteArray = bos.toByteArray();
            DatagramPacket datagramPacket = new DatagramPacket(byteArray,byteArray.length,new InetSocketAddress("127.0.0.1",9999));
            datagramSocket.send(datagramPacket);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
相关推荐
思科小白白2 分钟前
【无标题】
网络·智能路由器
卑微的Coder8 分钟前
JMeter同步定时器 模拟多用户并发访问场景
java·jmeter·压力测试
pjx98718 分钟前
微服务的“导航系统”:使用Spring Cloud Eureka实现服务注册与发现
java·spring cloud·微服务·eureka
yayaer244 分钟前
GOOSE 协议中MAC配置
服务器·网络·goose
嵌入式在学无敌大神1 小时前
IP协议、以太网包头及UNIX域套接字
网络·tcp/ip·unix
多多*1 小时前
算法竞赛相关 Java 二分模版
java·开发语言·数据结构·数据库·sql·算法·oracle
爱喝酸奶的桃酥1 小时前
MYSQL数据库集群高可用和数据监控平台
java·数据库·mysql
唐僧洗头爱飘柔95272 小时前
【SSM-SSM整合】将Spring、SpringMVC、Mybatis三者进行整合;本文阐述了几个核心原理知识点,附带对应的源码以及描述解析
java·spring·mybatis·springmvc·动态代理·ioc容器·视图控制器
骑牛小道士2 小时前
Java基础 集合框架 Collection接口和抽象类AbstractCollection
java
alden_ygq2 小时前
当java进程内存使用超过jvm设置大小会发生什么?
java·开发语言·jvm