JinhaoPlus

JinhaoPlus 查看完整档案

杭州编辑北京理工大学  |  计算机科学与技术 编辑曹操专车  |  Developer 编辑 segmentfault.com/u/jinhaoplus 编辑
编辑

扎瓦程序员

个人动态

JinhaoPlus 发布了文章 · 2019-09-05

close和shutdown关闭TCP连接

内核态用户态读写流程

clipboard.png

write调用的过程

  • 用户态的用户程序对socket进行write调用
  • 内核会搬运用户程序缓冲区的数据到内核写缓冲区(发送缓冲区),搬运完毕write调用就会返回(即使缓冲区上的数据还没发送出去)
  • 内核TCP协议栈会搬运数据从内核写缓冲区(发送缓冲区)到网卡
  • 网卡在物理层把数据发送到目标网卡上,中间的网络过程略过

read调用的过程:

  • 用户态的用户程序对socket进行read调用
  • 内核TCP协议栈会搬运网卡上来自源的数据到内核读缓冲区(接收缓冲区)
  • 内核会搬运内核读缓冲区(接收缓冲区)的数据到用户程序缓冲区
  • 用户程序就可以在用户程序缓冲区访问到这些数据了

shutdown与close

int close(int sockfd)

close函数会对套接字引用计数(引用了这个套接字描述符的进程数)减一,一旦发现套接字引用计数到0,就会对套接字进行彻底释放,并且会关闭TCP两个方向的数据流并回收连接和相关资源,是所谓的粗暴式关闭:

  • 在read方向,内核会将该套接字设置为不可读,对套接字的read都会返回异常
  • 在write方向,内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个FIN报文,接下来如果再对该套接字进行write会返回异常
int shutdown(int sockfd, int howto)

shutdown函数可以单向或者双向的关闭连接,是所谓的优雅式关闭,howto来设置:

  • SHUT_RD(0):关闭连接的read方向,对该套接字进行read直接返回EOF。从数据角度来看,套接字上接收缓冲区已有的数据将被丢弃,如果再有新的数据流到达,会对数据进行ACK,然后悄悄地丢弃。也就是说,对端还是会接收到ACK,在这种情况下根本不知道数据已经被丢弃了

    clipboard.png

  • SHUT_WR(1):关闭连接的write方向,这就是常被称为半关闭的连接。此时,不管套接字引用计数的值是多少,都会直接关闭连接的write方向。套接字上发送缓冲区已有的数据将被立即发送出去,并发送一个FIN报文给对端,之后应用程序如果对该套接字进行write会报错

    clipboard.png

  • SHUT_RDWR(2):相当于SHUT_RD和SHUT_WR操作各一次,关闭套接字的read和write两个方向

    clipboard.png

写程序来看一下close和shutdown的区别

client:

int main(int argc, char **argv) {
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    socklen_t server_len = sizeof(server_addr);
    int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
    if (connect_rt < 0) {
        error(1, errno, "connect failed ");
    }

    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    int n;

    fd_set readmask;
    fd_set allreads;

    FD_ZERO(&allreads);
    FD_SET(0, &allreads);
    FD_SET(socket_fd, &allreads);
    for (;;) {
        readmask = allreads;
        // IO多路复用select函数,可以同时监听socket_fd和标准输入
        int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);
        if (rc <= 0)
            error(1, errno, "select failed");
        if (FD_ISSET(socket_fd, &readmask)) {
            n = read(socket_fd, recv_line, MAXLINE);
            if (n < 0) {
                error(1, errno, "read error");
            } else if (n == 0) {
                error(1, 0, "server terminated \n");
            }
            recv_line[n] = 0;
            fputs(recv_line, stdout);
            fputs("\n", stdout);
        }
        if (FD_ISSET(0, &readmask)) {
            if (fgets(send_line, MAXLINE, stdin) != NULL) {
                if (strncmp(send_line, "shutdown", 8) == 0) {
                    FD_CLR(0, &allreads);
                    if (shutdown(socket_fd, 1)) {
                        error(1, errno, "shutdown failed");
                    }
                } else if (strncmp(send_line, "close", 5) == 0) {
                    FD_CLR(0, &allreads);
                    if (close(socket_fd)) {
                        error(1, errno, "close failed");
                    }
                    sleep(6);
                    exit(0);
                } else {
                    int i = strlen(send_line);
                    if (send_line[i - 1] == '\n') {
                        send_line[i - 1] = 0;
                    }

                    printf("now sending %s\n", send_line);
                    size_t rt = write(socket_fd, send_line, strlen(send_line));
                    if (rt < 0) {
                        error(1, errno, "write failed ");
                    }
                    printf("send bytes: %zu \n", rt);
                }
            }
        }
    }
}
static void sig_int(int signo) {
    printf("\nreceived %d datagrams\n", count);
    exit(0);
}


int main(int argc, char **argv) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        error(1, errno, "bind failed ");
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        error(1, errno, "listen failed ");
    }

    signal(SIGINT, sig_int);
    signal(SIGPIPE, SIG_IGN);

    int connfd;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
        error(1, errno, "bind failed ");
    }

    char message[MAXLINE];
    count = 0;

    for (;;) {
        int n = read(connfd, message, MAXLINE);
        if (n < 0) {
            error(1, errno, "error read");
        } else if (n == 0) {
            error(1, 0, "client closed \n");
        }
        message[n] = 0;
        printf("received %d bytes: %s\n", n, message);
        count++;

        char send_line[MAXLINE];
        sprintf(send_line, "Hi, %s", message);
        // 休眠几秒模拟服务器工作一段时间
        sleep(5);

        int write_nc = send(connfd, send_line, strlen(send_line), 0);
        printf("send bytes: %zu \n", write_nc);
        if (write_nc < 0) {
            error(1, errno, "error write");
        }
    }
}

close的效果

client:

aaa
now sending aaa
send bytes: 3 
close

server:

received 3 bytes: aaa
send bytes: 7 
error read: Connection reset by peer (54)

可以看到client发送完aaa的数据后随即调用close,会导致client的TCP连接断开且资源回收,server处理完数据发回来的时候发现TCP连接已经没有了,所以就connection reset了,下面用tcpdump追踪一下:

> sudo tcpdump 'tcp and port 9527' -i lo0 -S
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
11:06:23.013648 IP localhost.55463 > localhost.9527: Flags [S], seq 3739428838, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904150004 ecr 0,sackOK,eol], length 0
11:06:23.013755 IP localhost.9527 > localhost.55463: Flags [S.], seq 2449498522, ack 3739428839, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904150004 ecr 904150004,sackOK,eol], length 0
11:06:23.013771 IP localhost.55463 > localhost.9527: Flags [.], ack 2449498523, win 6379, options [nop,nop,TS val 904150004 ecr 904150004], length 0
11:06:23.013783 IP localhost.9527 > localhost.55463: Flags [.], ack 3739428839, win 6379, options [nop,nop,TS val 904150004 ecr 904150004], length 0
11:06:30.327692 IP localhost.55463 > localhost.9527: Flags [P.], seq 3739428839:3739428842, ack 2449498523, win 6379, options [nop,nop,TS val 904157265 ecr 904150004], length 3
11:06:30.327740 IP localhost.9527 > localhost.55463: Flags [.], ack 3739428842, win 6379, options [nop,nop,TS val 904157265 ecr 904157265], length 0
11:06:31.826987 IP localhost.55463 > localhost.9527: Flags [F.], seq 3739428842, ack 2449498523, win 6379, options [nop,nop,TS val 904158750 ecr 904157265], length 0
11:06:31.827034 IP localhost.9527 > localhost.55463: Flags [.], ack 3739428843, win 6379, options [nop,nop,TS val 904158750 ecr 904158750], length 0
11:06:35.328859 IP localhost.9527 > localhost.55463: Flags [P.], seq 2449498523:2449498530, ack 3739428843, win 6379, options [nop,nop,TS val 904162236 ecr 904158750], length 7
11:06:35.328946 IP localhost.55463 > localhost.9527: Flags [R], seq 3739428843, win 0, length 0

分析一下上面的抓包结果:

C -> S [S]
S -> C [S.]
C -> S [.]
S -> C [.]
C -> S [P.] aaa
S -> C [.]
C -> S [F.]
S -> C [.]
S -> C [P.] Hi, aaa
C -> S [R]

client发完数据aaa后server响应了ack,然后client主动close,client会发送了FIN包给server,server响应了ack后client回收了连接和资源,server处理完数据发了结果Hi, aaa给client,这时client连接已经断了所以无法识别这个连接响应了RST包。

shutdown的效果

client:

aaa
now sending aaa
send bytes: 3 
shutdown
Hi, aaa
server terminated 

server:

received 3 bytes: aaa
send bytes: 7 
client closed

可以看到client发送完aaa的数据后随即调用shutdown,会导致client的TCP连接处于半关闭状态,这时read方向还是正常的但是write方向已经断开了,server处理完数据发回来的时候client还可以读到,等一段时间client exit退出连接就全部断开了,服务端read到EOF也就关闭了,同样的用tcpdump追踪一下:

> sudo tcpdump 'tcp and port 9527' -i lo0 -S
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
11:06:53.692427 IP localhost.55594 > localhost.9527: Flags [S], seq 2938836011, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904180495 ecr 0,sackOK,eol], length 0
11:06:53.692546 IP localhost.9527 > localhost.55594: Flags [S.], seq 2801533649, ack 2938836012, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 904180495 ecr 904180495,sackOK,eol], length 0
11:06:53.692562 IP localhost.55594 > localhost.9527: Flags [.], ack 2801533650, win 6379, options [nop,nop,TS val 904180495 ecr 904180495], length 0
11:06:53.692577 IP localhost.9527 > localhost.55594: Flags [.], ack 2938836012, win 6379, options [nop,nop,TS val 904180495 ecr 904180495], length 0
11:06:58.429387 IP localhost.55594 > localhost.9527: Flags [P.], seq 2938836012:2938836015, ack 2801533650, win 6379, options [nop,nop,TS val 904185206 ecr 904180495], length 3
11:06:58.429435 IP localhost.9527 > localhost.55594: Flags [.], ack 2938836015, win 6379, options [nop,nop,TS val 904185206 ecr 904185206], length 0
11:07:00.789790 IP localhost.55594 > localhost.9527: Flags [F.], seq 2938836015, ack 2801533650, win 6379, options [nop,nop,TS val 904187548 ecr 904185206], length 0
11:07:00.789847 IP localhost.9527 > localhost.55594: Flags [.], ack 2938836016, win 6379, options [nop,nop,TS val 904187548 ecr 904187548], length 0
11:07:03.431085 IP localhost.9527 > localhost.55594: Flags [P.], seq 2801533650:2801533657, ack 2938836016, win 6379, options [nop,nop,TS val 904190180 ecr 904187548], length 7
11:07:03.431161 IP localhost.55594 > localhost.9527: Flags [.], ack 2801533657, win 6379, options [nop,nop,TS val 904190180 ecr 904190180], length 0
11:07:03.431663 IP localhost.9527 > localhost.55594: Flags [F.], seq 2801533657, ack 2938836016, win 6379, options [nop,nop,TS val 904190180 ecr 904190180], length 0
11:07:03.431728 IP localhost.55594 > localhost.9527: Flags [.], ack 2801533658, win 6379, options [nop,nop,TS val 904190180 ecr 904190180], length 0

分析一下上面的抓包结果:

C -> S [S]
S -> C [S.]
C -> S [.]
S -> C [.]
C -> S [P.] aaa
S -> C [.]
C -> S [F.]
S -> C [.]
S -> C [P.] Hi, aaa
C -> S [.]
S -> C [F.]
C -> S [.]

client发完数据aaa后server响应了ack,然后client主动shutdown,client会发送了FIN包给server,server响应了ack后client半关闭只能读不能再写了,server处理完数据发了结果Hi, aaa给client,这时client读了最后的结果全关闭读写,注意这时只是关闭了读写没有回收资源,server读到了EOF发松了最后的FIN,client回复了ACK,最后是完整的四次挥手。

注意关闭的是socket不是连接

之前分析问题的时候我有一个疑问:既然client处于半关闭了,也就是只能读不能写了,那为什么还可以发送ack给server呢,其实这里就是没彻底理解关闭的意义,半关闭是说socket这个套接字描述符半关闭了,不是连接本身半关闭了,连接在内核态还存在,所以还是可以通过内核TCP协议栈正常通信,但是用户态的程序对socket的write调用不行了。再明白的来看其实还是下面这张图:

clipboard.png

用户态里红色的write虽然关闭了,但是内核态里面写缓冲到网卡之间还是通的。

查看原文

赞 1 收藏 1 评论 0

JinhaoPlus 发布了文章 · 2019-08-31

tcpdump实验:分析TCP连接建立、传输、断开

TCP包的结构:TCP协议规定的数据报文的构造

TCP协议规定的数据报文的构造

TCP报文中的序列号seq和确认号到底指什么?

序列号seq(sequence numer)

序列号seq,其实TCP发送payload数据的每一个字节都是有编号的,每个报文的编号其实就是这个报文payload数据中的首个字节的编号,通俗的例子:
一共发了3个报文:P1(payload=aaa), P2(payload=bb), P3(payload=c),那么

P1.seq=0
P2.seq=3(P1.seq+p1.payload.length) 
P3.seq=5(P2.seq+p2.payload.length)

确认号ack(acknowledge numer)

确认号ack,其实是对前一个对端发送来的报文的回复,ack=x+1说明之前对端发送来的x个字节的payload数据都已经收到了,请对端下次发送seq=x+1的报文吧,或者说叫期望从对端收到的下一字节的序号。通俗的例子:
A主机发来了P1(payload=aaa), B主机接收到P1报文后需要发回确认报文R1:

P1.seq=0
R1.ack=3(P1.seq+p1.payload.length) 

tcpdump抓包分析

抓在80端口上的TCP包:

> tcpdump -S 'tcp and port 80'
21:33:01.704998 IP 192.168.31.72.61146 > 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http: Flags [S], seq 396038856, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 744881175 ecr 0,sackOK,eol], length 0

其中传输的Flags释义:

  • [S]:SYN,表示开始连接
  • [.]:没有标记,一般是确认
  • [P]:PSH,表示数据推送
  • [F]:FIN,表示结束连接
  • [R] :RST,表示重启连接

抓包的一组TCP连接传输断开的数据报文:

> tcpdump -S 'tcp and port 80'
21:33:01.704998 IP 192.168.31.72.61146 > 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http: Flags [S], seq 396038856, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 744881175 ecr 0,sackOK,eol], length 0
21:33:01.717502 IP 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http > 192.168.31.72.61146: Flags [S.], seq 2345237966, ack 396038857, win 14400, options [mss 1412,nop,nop,sackOK,nop,wscale 7], length 0
21:33:01.717542 IP 192.168.31.72.61146 > 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http: Flags [.], ack 2345237967, win 4096, length 0
21:33:01.718011 IP 192.168.31.72.61146 > 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http: Flags [P.], seq 396038857:396039622, ack 2345237967, win 4096, length 765: HTTP: POST /mmtls/22207be4 HTTP/1.1
21:33:01.727958 IP 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http > 192.168.31.72.61146: Flags [.], ack 396039622, win 125, length 0
21:33:01.758550 IP 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http > 192.168.31.72.61146: Flags [P.], seq 2345237967:2345238289, ack 396039622, win 125, length 322: HTTP: HTTP/1.1 200 OK
21:33:01.758735 IP 192.168.31.72.61146 > 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http: Flags [.], ack 2345238289, win 4090, length 0
21:33:01.759551 IP 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http > 192.168.31.72.61146: Flags [F.], seq 2345238289, ack 396039622, win 125, length 0
21:33:01.759693 IP 192.168.31.72.61146 > 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http: Flags [.], ack 2345238290, win 4090, length 0
21:33:01.773281 IP 192.168.31.72.61146 > 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http: Flags [F.], seq 396039622, ack 2345238290, win 4096, length 0
21:33:01.786917 IP 101.165.151.61.dial.xw.sh.dynamic.163data.com.cn.http > 192.168.31.72.61146: Flags [.], ack 396039623, win 0, length 0

梳理一下:

报文传输方向Flagsseqackpayload length
C -> S[S]3960388560
S -> C[S.]23452379663960388570
C -> S[.](396038857)23452379670
C -> S[P.]396038857(:396039622)2345237967765
S -> C[.](2345237967)3960396220
S -> C[P.]2345237967(:2345238289)396039622322
C -> S[.](396039622)23452382890
S -> C[F.]23452382893960396220
C -> S[.](396039622)23452382900
C -> S[F.]39603962223452382900
S -> C[.](2345238290)3960396230

下面图示了整个连接、传输和断开过程的TCP数据报:
抓包分析图

其中需要注意的点有:

  • 为了避免过长的seq和ack,图中seq和ack只展示了后3位;
  • tcpdump开启-S命令后,所有的seq和ack都将保留绝对值而不是相对值,易于清楚呈现出连接报文的详情;
  • 报文4seq 396038857:396039622 length 765,这个意思是seq=396038857,其报文的payload length=765,所以对端ack的时候应该ack=396039622(即表示396039622前的字节我都收到了,下一次请给我编号为396039622及以后的字节),显示这个就不需要人读的时候再手工计算了,简单明了的展示清楚,不过第一次用的话会很懵。报文5ack 396039622印证了这个观点;
  • 为什么syn报文明明payload length=0,对端ack还要加1呢?因为syn是建立连接的关键报文,而为了确保对方接收到,使用超时重传机制,TCP规定,只为有数据的TCP报文重传,SYN占据一个序号(可以认为只有一个字节数据的报文,即使其payload length=0),这样做TCP也会重传SYN报文;
  • ack的payload length=0,可以看到ack报文发送后之后的报文seq并没有变;

可以对照此经典图进行分析(不过要注意这个图里数据传输过程中的数据报文假定payload length=1):
经典图

查看原文

赞 2 收藏 1 评论 0

JinhaoPlus 发布了文章 · 2018-09-17

Java语法糖的编译结果分析(二)

语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可不会被虚拟机接受,因此编译器为了让这些含有语法糖的代码正常工作其实需要对这些代码进行加工,经过编译器在生成class字节码的阶段完成解语法糖(desugar)的过程,那么这些语法糖最终究竟被编译成了什么呢,在这里列举了如下的一些Java典型的语法糖,结合实例和它们的编译结果分析一下。本文为本系列第二篇。

枚举类

枚举在编译后会变成一个特殊的final类,因此枚举类型是名副其实的不可变类,我们通过下面最简单的例子来仔细分析一下:

源码:

enum COLOR {
    RED,
    BLUE,
    GREEN
}

使用这个枚举的时候我们可以发现有valueOf(String)values()这样的方法可以用,因此不难猜测编译器会添加一些未在源码中出现的其他增强二进制字节码,可以看一下具体的字节码:

final class COLOR extends java.lang.Enum<COLOR>
  minor version: 0
  major version: 52
  flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
...

  {
  public static final COLOR RED;
    descriptor: LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final COLOR BLUE;
    descriptor: LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final COLOR GREEN;
    descriptor: LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static COLOR[] values();
    descriptor: ()[LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field $VALUES:[LCOLOR;
         3: invokevirtual #2                  // Method "[LCOLOR;".clone:()Ljava/lang/Object;
         6: checkcast     #3                  // class "[LCOLOR;"
         9: areturn

  public static COLOR valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #4                  // class COLOR
         2: aload_0
         3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #4                  // class COLOR
         9: areturn

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #4                  // class COLOR
         3: dup
         4: ldc           #7                  // String RED
         6: iconst_0
         7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        10: putstatic     #9                  // Field RED:LCOLOR;
        13: new           #4                  // class COLOR
        16: dup
        17: ldc           #10                 // String BLUE
        19: iconst_1
        20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        23: putstatic     #11                 // Field BLUE:LCOLOR;
        26: new           #4                  // class COLOR
        29: dup
        30: ldc           #12                 // String GREEN
        32: iconst_2
        33: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        36: putstatic     #13                 // Field GREEN:LCOLOR;
        39: iconst_3
        40: anewarray     #4                  // class COLOR
        43: dup
        44: iconst_0
        45: getstatic     #9                  // Field RED:LCOLOR;
        48: aastore
        49: dup
        50: iconst_1
        51: getstatic     #11                 // Field BLUE:LCOLOR;
        54: aastore
        55: dup
        56: iconst_2
        57: getstatic     #13                 // Field GREEN:LCOLOR;
        60: aastore
        61: putstatic     #1                  // Field $VALUES:[LCOLOR;
        64: return
}
Signature: #32                          // Ljava/lang/Enum<LCOLOR;>;

这段字节码可以证实出上面的猜测,确实会有额外的二进制字节码被添加了,枚举类会被编译成为Ljava/lang/Enum的子类COLOR,而枚举类型中的枚举项会被编译成为COLOR类的常量字段,而且COLOR内部还会维护一个数组来保存这些常量字段,并进而添加valueOf(String)values()来访问这个数组。因此,对应地我们可以翻译这段二进制字节码为这样的代码:

final class COLOR extends Enum<COLOR> {
    private static final COLOR RED;
    private static final COLOR BLUE;
    private static final COLOR GREEN;

    private static final COLOR[] $VALUES;

    static {
        RED = new COLOR("RED", 0);
        BLUE = new COLOR("BLUE", 1);
        GREEN = new COLOR("GREEN", 2);
        COLOR[] $COLOR_ARRAY = new COLOR[3];
        $COLOR_ARRAY[0] = RED;
        $COLOR_ARRAY[1] = BLUE;
        $COLOR_ARRAY[2] = GREEN;
        $VALUES = $COLOR_ARRAY;
    }

    private COLOR(String color, int ordinal) {
        super(color, ordinal);
    }

    public static COLOR[] values() {
        return $VALUES.clone();
    }

    public static COLOR valueOf(String color) {
        return Enum.valueOf(COLOR.class, color);
    }
}

注意,这段代码并不能通过编译,因为源码这一层是不允许直接继承Ljava/lang/Enum的,这个继承过程只允许在编译器内部解语法糖的过程中被编译器添加,添加之后的类才会有ACC_ENUM的访问标识符。

我们可以看到的是在Ljava/lang/Enum内部实际上有nameordinal常量来标识一个枚举项,name会由枚举项名来设置,而ordinal是枚举项序号,由枚举项排列顺序决定。

我们再来看一下带有字段的枚举项编译后的效果。

源码:

enum COLOR {
    RED(0),
    BLUE(1),
    GREEN(2);

    int code;

    COLOR(int code) {
        this.code = code;
    }
}

编译后的字节码:

final class COLOR extends java.lang.Enum<COLOR>
  minor version: 0
  major version: 52
  flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
...
{
  public static final COLOR RED;
    descriptor: LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final COLOR BLUE;
    descriptor: LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final COLOR GREEN;
    descriptor: LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  int code;
    descriptor: I
    flags:

  public static COLOR[] values();
    descriptor: ()[LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field $VALUES:[LCOLOR;
         3: invokevirtual #2                  // Method "[LCOLOR;".clone:()Ljava/lang/Object;
         6: checkcast     #3                  // class "[LCOLOR;"
         9: areturn
      LineNumberTable:
        line 1: 0

  public static COLOR valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)LCOLOR;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #4                  // class COLOR
         2: aload_0
         3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #4                  // class COLOR
         9: areturn
      LineNumberTable:
        line 1: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=5, locals=0, args_size=0
         0: new           #4                  // class COLOR
         3: dup
         4: ldc           #8                  // String RED
         6: iconst_0
         7: iconst_0
         8: invokespecial #9                  // Method "<init>":(Ljava/lang/String;II)V
        11: putstatic     #10                 // Field RED:LCOLOR;
        14: new           #4                  // class COLOR
        17: dup
        18: ldc           #11                 // String BLUE
        20: iconst_1
        21: iconst_1
        22: invokespecial #9                  // Method "<init>":(Ljava/lang/String;II)V
        25: putstatic     #12                 // Field BLUE:LCOLOR;
        28: new           #4                  // class COLOR
        31: dup
        32: ldc           #13                 // String GREEN
        34: iconst_2
        35: iconst_2
        36: invokespecial #9                  // Method "<init>":(Ljava/lang/String;II)V
        39: putstatic     #14                 // Field GREEN:LCOLOR;
        42: iconst_3
        43: anewarray     #4                  // class COLOR
        46: dup
        47: iconst_0
        48: getstatic     #10                 // Field RED:LCOLOR;
        51: aastore
        52: dup
        53: iconst_1
        54: getstatic     #12                 // Field BLUE:LCOLOR;
        57: aastore
        58: dup
        59: iconst_2
        60: getstatic     #14                 // Field GREEN:LCOLOR;
        63: aastore
        64: putstatic     #1                  // Field $VALUES:[LCOLOR;
        67: return
      LineNumberTable:
        line 2: 0
        line 3: 14
        line 4: 28
        line 1: 42
}
Signature: #36                          // Ljava/lang/Enum<LCOLOR;>;

用java源码翻译下上面的结果:

final class COLOR extends Enum<COLOR> {

    private static final COLOR RED;
    private static final COLOR BLUE;
    private static final COLOR GREEN;

    int code;
    private static final COLOR[] $VALUES;

    static {
        RED = new COLOR("RED", 0, 0);
        BLUE = new COLOR("BLUE", 1, 1);
        GREEN = new COLOR("GREEN", 2, 2);
        COLOR[] $COLOR_ARRAY = new COLOR[3];
        $COLOR_ARRAY[0] = RED;
        $COLOR_ARRAY[1] = BLUE;
        $COLOR_ARRAY[2] = GREEN;
        $VALUES = $COLOR_ARRAY;
    }

    private COLOR(String color, int ordinal, int code) {
        super(color, ordinal);
        this.code = code;
    }

    public static COLOR[] values() {
        return $VALUES.clone();
    }

    public static COLOR valueOf(String color) {
        return Enum.valueOf(COLOR.class, color);
    }
}

其实有了之前的基础很容易看出来,新增加的code字段最终只是变成了编译器生成的COLOR类的一个字段,唯一的变化就是编译出的初始化方法也会增加为这个字段而添加的参数。

断言

java 1.4引入的断言,使用关键字assert来判断一个条件是否为true,通过如下的源码来分析一下:

class Main {
    public static void main(String[] args) {
        String judge = "yes";
        assert "no".equals(judge);
    }
}

断言在运行时默认是关闭的,我们可以通过运行时打开断言来启用:java -ea Main

    at Main.main(Main.java:4)

那么我们来看一下编译后的字节码:

{
  static final boolean $assertionsDisabled;
    descriptor: Z
    flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC

  Main();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #2                  // String yes
         2: astore_1
         3: getstatic     #3                  // Field $assertionsDisabled:Z
         6: ifne          26
         9: ldc           #4                  // String no
        11: aload_1
        12: invokevirtual #5                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        15: ifne          26
        18: new           #6                  // class java/lang/AssertionError
        21: dup
        22: invokespecial #7                  // Method java/lang/AssertionError."<init>":()V
        25: athrow
        26: return
      StackMapTable: number_of_entries = 1
        frame_type = 252 /* append */
          offset_delta = 26
          locals = [ class java/lang/String ]

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #8                  // class Main
         2: invokevirtual #9                  // Method java/lang/Class.desiredAssertionStatus:()Z
         5: ifne          12
         8: iconst_1
         9: goto          13
        12: iconst_0
        13: putstatic     #3                  // Field $assertionsDisabled:Z
        16: return
      StackMapTable: number_of_entries = 2
        frame_type = 12 /* same */
        frame_type = 64 /* same_locals_1_stack_item */
          stack = [ int ]
}

可以发现编译器为Main类添加了字段$assertionsDisabled,此字段即是启用断言的关键。在运行时加入启用断言的-ea会使得类初始化时Class.desiredAssertionStatus为真,进而字段$assertionsDisabled为真,这个逻辑在上述的字节码中可以看出。在断言的地方,如果条件为真则会正常返回,如果条件为false则会抛出java/lang/AssertionError错误导致程序终止。

用java源码翻译下上面的结果:

class Main {

    private static final boolean $assertionsDisabled;

    static {
        if (Main.class.desiredAssertionStatus()) {
            $assertionsDisabled = true;
        } else {
            $assertionsDisabled = false;
        }
    }

    public static void main(String[] args) {
        if($assertionsDisabled) {
            if (!"no".equals("yes")) {
                throw new AssertionError();
            }
        }
    }
}

switch处理枚举和字符串

我们先来看看在java 1.7以前就可以使用switch的类型在字节码层是如何工作的,这里以int类型为例:

class Main {
    public static void main(String[] args) {
        int a = 1;
        switch (a) {
            case 0:
                System.out.println("0");
                break;
            case 2:
                System.out.println("1");
                break;
            case 8:
                System.out.println("3");
                break;
            default:
                break;
        }
    }
}

编译后的字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: lookupswitch  { // 3
                       0: 36
                       2: 47
                       8: 58
                 default: 69
            }
        36: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: ldc           #3                  // String 0
        41: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        44: goto          69
        47: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        50: ldc           #5                  // String 1
        52: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        55: goto          69
        58: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        61: ldc           #6                  // String 3
        63: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        66: goto          69
        69: return
}

这里是用的字节码命令lookupswitch适用于判断switch的跳转语句的,即如果switch 0跳转到26行、switch 2跳转到47行、switch 8跳转到58行、其他跳转到69行。
受限于lookupswitch判断的条件的类型,在java 1.7以前是无法对非32位数字类型的类型做判断的,而java 1.7以后通过语法糖的解析实现了字符串的switch分支判断,可以想到的是,在不改变lookupswitch的能力的情况下,编译器会将字符串转换为32位数字。我们写这样的例子来分析下:

class Main {
    public static void main(String[] args) {
        String a = args[0];
        switch (a) {
            case "a":
                System.out.println("a");
                break;
            case "b":
                System.out.println("b");
                break;
            default:
                break;
        }
    }
}

编译后的结果:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: aload_0
         1: iconst_0
         2: aaload
         3: astore_1
         4: aload_1
         5: astore_2
         6: iconst_m1
         7: istore_3
         8: aload_2
         9: invokevirtual #2                  // Method java/lang/String.hashCode:()I
        12: lookupswitch  { // 2
                      97: 40
                      98: 54
                 default: 65
            }
        40: aload_2
        41: ldc           #3                  // String a
        43: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        46: ifeq          65
        49: iconst_0
        50: istore_3
        51: goto          65
        54: aload_2
        55: ldc           #5                  // String b
        57: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
        60: ifeq          65
        63: iconst_1
        64: istore_3
        65: iload_3
        66: lookupswitch  { // 2
                       0: 92
                       1: 103
                 default: 114
            }
        92: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        95: ldc           #3                  // String a
        97: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       100: goto          114
       103: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       106: ldc           #5                  // String b
       108: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       111: goto          114
       114: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0     115     0  args   [Ljava/lang/String;
            4     111     1     a   Ljava/lang/String;
}

我们可以发现编译器对要做分支判断的字符串计算了它的hashcode,而这个hashcode是符合lookupswitch要求的32位数字,因此将这个hashcode做lookupswitch分支判断,和switch条件中的"a""b"的hashcode做比较,如果进入了其中某个分支如"a"分支,则在分支中判断"a"和字符串是否相等,如果相等则确定此分支是正确的(只有hashcode相等并不能确定是值相等,hashcode的冲突原理不再展开),接下来再将分支条件直接设置为0、1、2这样的简单条件执行下一轮lookupswitch。我们同样可以用如下java源码翻译下上面的结果:

class Main {
    public static void main(String[] args) {
        String param = args[0];
        int hashcode = param.hashCode();
        final int condition_a = 97; //"a".hashCode()
        final int condition_b = 98; //"b".hashCode();
        int hashcodeSwitchResult = -1;
        switch (hashcode) {
            case condition_a:
                if("a".equals(param)){
                    hashcodeSwitchResult = 0;
                }
                break;
            case condition_b:
                if("b".equals(param)){
                    hashcodeSwitchResult = 1;
                }
                break;
            default:
                break;
        }
        switch (hashcodeSwitchResult) {
            case 0:
                System.out.println("a");
                break;
            case 1:
                System.out.println("b");
                break;
            default:
                break;
        }
    }
}
查看原文

赞 0 收藏 0 评论 0

JinhaoPlus 发布了文章 · 2018-09-10

Java语法糖的编译结果分析(一)

语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可不会被虚拟机接受,因此编译器为了让这些含有语法糖的代码正常工作其实需要对这些代码进行加工,经过编译器在生成class字节码的阶段完成解语法糖(desugar)的过程,那么这些语法糖最终究竟被编译成了什么呢,在这里列举了如下的一些Java典型的语法糖,结合实例和它们的编译结果分析一下。本文为该系列的第一篇。

泛型和类型擦除

java的泛型实际上是伪泛型,在编译后编译器会擦除泛型对象的参数化类型,也就是说源代码中的<T>类型其实都会擦除,最终成为class字节码中的Object类型,赋值等操作也就会直接转换为强制的类型转换,这样做无风险的原因是在编译的标注检查阶段其实已经进行了泛型的检查,如果当时无法通过检查的话编译无法通过。

另外,这个泛型信息不是真的就此丢掉了,class字节码中还是会保留Signature属性来记录泛型对象在源码中的参数化类型。

代码:

public class Main {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        strList.add("aaa");
        String strEle = strList.get(0);
    }
}

main方法在javap编译后的字节码

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String aaa
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_1
        18: iconst_0
        19: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        24: checkcast     #7                  // class java/lang/String
        27: astore_2
        28: return

上面我们演示了一个参数化类型为StringList的泛型对象strListaddget操作:

  • add操作:对应字节码中的8~16个字节:我们可以看到最关键的add操作其实就是

    invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

    调用的其实是java/util/List类的add方法,此方法的入参类型是Ljava/lang/Object;,返回值类型是Z,翻译过来就是List类的boolean add(Object o)方法,这里并没有参数化类型String的什么事情。

  • get操作:对应字节码中的17~27个字节:我们可以看到最关键的get操作其实就是

    invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
    checkcast     #7                  // class java/lang/String

    调用的其实是java/util/List类的get方法,此方法的入参类型是I,返回值类型是Ljava/lang/Object;,翻译过来就是List类的Object get(int i)方法,执行完后将获得的结果做了checkcast,检查返回的对象类型是否是String

从上面的分析我们不难看出,Java泛型到了编译出结果的时候参数化类型已经没有什么作用了,就是简单做了强制的类型转换。这段去掉了语法糖的代码如下:

public class Main {
    public static void main(String[] args) {
        List strList = new ArrayList();
        strList.add((Object)"aaa");
        String strEle = (String) strList.get(0);
    }
}

Java的泛型是伪泛型的原因如上,在运行时这个代码完全体会不到不同参数化类型的List有什么不同。而泛型参数化类型的用武之地更多的是在编译时用来做检验类型使用的,正常情况下如果编译时通过检验当然就不会在运行期类型强制转换的时候出现异常,更何况其实字节码中还有checkcast的显式类型检查。

如果使用javac-g:vars参数来保留class字节码中方法的局部变量信息,那么我们可以看到额外的信息:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      29     0  args   [Ljava/lang/String;
    8      21     1 strList   Ljava/util/List;
   28       1     2 strEle   Ljava/lang/String;
LocalVariableTypeTable:
Start  Length  Slot  Name   Signature
    8      21     1 strList   Ljava/util/List<Ljava/lang/String;>;

其中的LocalVariableTypeTable属性记录了strList的擦除泛型前的类型:Ljava/util/List<Ljava/lang/String;>;,翻译过来其实就是List<String>,如果在反射中获取泛型变量的类型元信息,其来源其实就是这个Signature。这也算是Java为了弥补因类型擦除而导致的class字节码中的类型数据缺失而做出的额外努力吧。

变长参数:编译后变成数组类型的参数

变长参数会被编译成为数组类型的参数,变长参数只能出现在参数列表的结尾以消除歧义。

代码:

public class Main {
    public static void method(String... args) {

    }
}

method方法在编译后:

public static void method(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=0, locals=1, args_size=1
         0: return

我们可以清楚地看到方法的特征符是([Ljava/lang/String;)V,即参数是[Ljava/lang/String;,翻译过来就是String[],即数组类型。

这段去掉了语法糖的代码如下:

public class Main {
    public static void method(String[] args) {

    }
}

自动装箱拆箱

编译后装箱通过valueOf()变成了对象,拆箱通过xxxValue()变成了原始类型值。

代码:

public class Main {
    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}

main方法编译后:

    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
         9: istore_2
        10: return

这里我们可以明显看到Integer x = 1;编译时x转换成了java/lang/Integer.valueOf生成的引用类型Integer变量,而int y = x;编译时y转换成了java/lang/Integer.intValue生成的原始类型int变量。

去掉了语法糖的代码如下:

public class Main {
    public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }
}

遍历循环

编译后变成了迭代器遍历。

代码:

public class Main {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        for (String str : strList) {
            System.out.println(str);
        }
    }
}

main方法编译后:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: invokeinterface #4,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
        14: astore_2
        15: aload_2
        16: invokeinterface #5,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
        21: ifeq          44
        24: aload_2
        25: invokeinterface #6,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        30: checkcast     #7                  // class java/lang/String
        33: astore_3
        34: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        37: aload_3
        38: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        41: goto          15
        44: return
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 15
          locals = [ class java/util/List, class java/util/Iterator ]
        frame_type = 250 /* chop */
          offset_delta = 28

从上面我们可以看到遍历循环的语法糖被替换成了List.iterator的循环操作,用下面的代码即可表达这段编译后的去掉语法糖的代码:

public class Main {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        Iterator strIterator = strList.iterator();
        while(strIterator.hasNext()){
            System.out.println((String) strIterator.next());
        }
    }
}

条件编译

编译后将常量不可达条件分支直接在编译结果中消除掉。

代码:

public class Main {
    public static void main(String[] args) {
        if (true) {
            System.out.println("Yes");
        } else {
            System.out.println("No");
        }
    }
}

main方法编译后:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Yes
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

从上面我们可以看到常量不可达条件直接就在编译结果中略去了,仿佛就没有这个分支一样,用下面的代码即可表达这段编译后的去掉语法糖的代码:

public class Main {
    public static void main(String[] args) {
        System.out.println("Yes");
    }
}

需要注意的是这里强调的是常量不可达条件才会略去,比如直接就是true的分支或者1==1这样的分支是会保留的,如果是变量经过运算后才被确定为不可达是不会发生这种条件编译的,比如:

public class Main {
    public static void main(String[] args) {
        int i = 1;
        if (i==1) {
            System.out.println("Yes");
        } else {
            System.out.println("No");
        }
    }
}

编译后还是会走ifelse判断:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: iconst_1
         4: if_icmpne     18
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #3                  // String Yes
        12: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: goto          26
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: ldc           #5                  // String No
        23: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 18
          locals = [ int ]
        frame_type = 7 /* same */
}

内部类

内部类即是类中类,我们来看这个简单的例子:

代码:

public class Main {

    class Person{
        String name;
        Integer age;

        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    }

    public void demo(String[] args) {
        Person person = new Person("ccc", 20);
    }
}

来看看编译后的结果,编译后会将内部类Person单独拿出来做编译,不过语法糖褪去后编译器做了一些处理,比如为Person类加了与外部的Main类相联系的字段this$0

...
class top.jinhaoplus.Main$Person
...
{
  java.lang.String name;
    descriptor: Ljava/lang/String;
    flags:

  java.lang.Integer age;
    descriptor: Ljava/lang/Integer;
    flags:

  final top.jinhaoplus.Main this$0;
    descriptor: Ltop/jinhaoplus/Main;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public top.jinhaoplus.Main$Person(top.jinhaoplus.Main, java.lang.String, java.lang.Integer);
    descriptor: (Ltop/jinhaoplus/Main;Ljava/lang/String;Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=4
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltop/jinhaoplus/Main;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: aload_0
        10: aload_2
        11: putfield      #3                  // Field name:Ljava/lang/String;
        14: aload_0
        15: aload_3
        16: putfield      #4                  // Field age:Ljava/lang/Integer;
        19: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Ltop/jinhaoplus/Main$Person;
            0      20     1 this$0   Ltop/jinhaoplus/Main;
            0      20     2  name   Ljava/lang/String;
            0      20     3   age   Ljava/lang/Integer;
}

这里翻译过来类似这样的:

class Person {
    String name;
    Integer age;
    final Main this$0;

    public Person(final Main this$0, String name, Integer age) {
        this.this$0 = this$0;
        this.name = name;
        this.age = age;
    }
}

public class Main {
    public void demo(String[] args) {
        Person person = new Person(this, "ccc", 20);
    }
}

至于为什么需要这个多余的外部类的字段呢,其实是为了通过它来获取外部类中的信息,我们对例子加以改造,添加两个外部类的字段secret1secret2

public class Main {

    private String secret1;
    private String secret2;

    class Person{
        String name;
        Integer age;

        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        public void getSecrets(){
            System.out.println(secret1);
            System.out.println(secret2);
        }
    }

    public void demo(String[] args) {
        Person person = new Person("ccc", 20);
        person.getSecrets();
    }
}

这个时候编译的结果是Main为了对外提供自己属性的值自动添加了静态方法access$000(Main)access$100(Main)

static java.lang.String access$000(top.jinhaoplus.Main);
    descriptor: (Ltop/jinhaoplus/Main;)Ljava/lang/String;
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field secret1:Ljava/lang/String;
         4: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   Ltop/jinhaoplus/Main;

static java.lang.String access$100(top.jinhaoplus.Main);
    descriptor: (Ltop/jinhaoplus/Main;)Ljava/lang/String;
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #1                  // Field secret2:Ljava/lang/String;
         4: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0    x0   Ltop/jinhaoplus/Main;
}

而内部类编译后的结果在获取外部类的属性的时候其实就是调用暴露出的这些方法:

public void getSecret();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #1                  // Field this$0:Ltop/jinhaoplus/Main;
         7: invokestatic  #6                  // Method top/jinhaoplus/Main.access$000:(Ltop/jinhaoplus/Main;)Ljava/lang/String;
        10: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: aload_0
        17: getfield      #1                  // Field this$0:Ltop/jinhaoplus/Main;
        20: invokestatic  #8                  // Method top/jinhaoplus/Main.access$100:(Ltop/jinhaoplus/Main;)Ljava/lang/String;
        23: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  this   Ltop/jinhaoplus/Main$Person;
}

翻译过来其实就是这样子的:

class Person {
    String name;
    Integer age;
    final Main this$0;

    public Person(final Main this$0, String name, Integer age) {
        this.this$0 = this$0;
        this.name = name;
        this.age = age;
    }

    public void getSecrets(){
        System.out.println(Main.access$000(this$0));
        System.out.println(Main.access$100(this$0));
    }
}

public class Main {
    private String secret1;
    private String secret2;

    public void demo(String[] args) {
        Person person = new Person(this, "ccc", 20);
    }

    public static String access$000(Main main) {
        return main.secret1;
    }

    public static String access$100(Main main) {
        return main.secret2;
    }
}
查看原文

赞 1 收藏 0 评论 0

JinhaoPlus 发布了文章 · 2018-09-03

Java泛型和编译优化的一个例子

public class Main {
    public static void main(String[] args) {
        ArrayList<String> strList = new ArrayList<String>();
        Type type = ((ParameterizedType)strList.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        System.out.println(type);
    }
}
public class Main {
    public static void main(String[] args) {
        ArrayList<String> strList = new ArrayList<String>(){};
        Type type = ((ParameterizedType)strList.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        System.out.println(type);
    }
}

这两个例子唯一的区别是后者的new ArrayList<String>(){}初始化strList的时候带了{}执行了赋初值,虽然语法层面没有什么区别,但是在编译之后的结果却完全不一样。而且执行的结果也完全不一样,
前者执行结果:

E

后者执行结果:

class java.lang.String

前者的编译结果:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        12: invokevirtual #5                  // Method java/lang/Class.getGenericSuperclass:()Ljava/lang/reflect/Type;
        15: checkcast     #6                  // class java/lang/reflect/ParameterizedType
        18: invokeinterface #7,  1            // InterfaceMethod java/lang/reflect/ParameterizedType.getActualTypeArguments:()[Ljava/lang/reflect/Type;
        23: iconst_0
        24: aaload
        25: astore_2
        26: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: aload_2
        30: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        33: return

这个逻辑很简单,就是简单的invokespecialArrayList<init>()方法。

后者的编译结果:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class Main$1
         3: dup
         4: invokespecial #3                  // Method Main$1."<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        12: invokevirtual #5                  // Method java/lang/Class.getGenericSuperclass:()Ljava/lang/reflect/Type;
        15: checkcast     #6                  // class java/lang/reflect/ParameterizedType
        18: invokeinterface #7,  1            // InterfaceMethod java/lang/reflect/ParameterizedType.getActualTypeArguments:()[Ljava/lang/reflect/Type;
        23: iconst_0
        24: aaload
        25: astore_2
        26: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: aload_2
        30: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        33: return

这里就奇怪了,加了{}之后竟然生成了内部类Main$1:

final class Main$1 extends java.util.ArrayList<java.lang.String>
...
{
  Main$1();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/util/ArrayList."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
}
Signature: #9                           // Ljava/util/ArrayList<Ljava/lang/String;>;
...

java.util.ArrayList<java.lang.String>的子类,这也就解释了后者的执行结果为何可以解析到strList的泛型参数化类型是java.lang.String了,因为它的实际类型在JVM执行的时候清楚地被标记成了内部类Main$1这个java.util.ArrayList<java.lang.String>的子类。而前者的strList的泛型参数化类型已经被擦除掉了。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 77 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • PlusOnlineJudge

    Java后台驱动的多语言在线编译网站PlusOnlineJudge:传送门 http://plusoj.top

注册于 2014-05-06
个人主页被 1.6k 人浏览