预见猿份
主题
首页面试题在线工具关于我们老苗一对一私教学员评价
实战项目
项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通Java数据结构与算法老苗一对一私教学员评价blog
blog
  • JVM 原理与实战调优

    • 课程介绍
    • JVM 虚拟机内存管理、性能工具、OOM & 死锁实战
    • 垃圾回收 判断垃圾、回收算法、各类收集器
    • JVM 调优实战、类文件结构、类加载机制、编译优化







----- 到底线了 -----

×

欢迎来到预见猿份,本站项目均为站长原创,学习中有问题可直接提交给站长老苗解决(微信:mrt_0607)。

苗润土老师,20余年一线项目经验,2014年加入黑马,星辰wms、云岚到家、学成在线项目作者,历任高级讲师、教学主管及课程研究员。 b站老苗

1、JVM调优实战 ​

通过前面的课程学习我们已经掌握了JVM调优的基本知识,下面我们进行实战,通过在Linux平台运行web项目,然后进行对其进行压力测试,我们通过对JVM的调优来观察是否对服务的性能有了提升。

最后我们将总结优化的经验以及一些建议。

1.1、环境准备 ​

测试环境需要1台Linux支持。

IP开放端口内存说明
192.168.31.13380804GB运行web项目

jdk使用的版本为jdk8,需要在Linux服务器中安装jdk8:

shell
#将资料中的jdk-8u144-linux-x64.tar.gz上传到/usr/local/src下
tar -xvf jdk-8u144-linux-x64.tar.gz

#修改环境变量
vim /etc/profile

#set java environment
#JAVA_HOME=/usr/local/src/jdk-11.0.7
JAVA_HOME=/usr/local/src/jdk1.8.0_144
CLASSPATH=.:$JAVA_HOME/lib.tools.jar
PATH=$JAVA_HOME/bin:$PATH
export JAVA_HOME CLASSPATH PATH

#使环境变量立即生效
source /etc/profile

#测试
[root@itcast src]# java -version
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

1.1.1、安装tomcat ​

tomcat服务器在JavaEE项目中使用率非常高,所以在生产环境对tomcat的优化也变得非常重要了。

对于tomcat的优化,主要是从2个方面入手,一是,tomcat自身的配置,另一个是tomcat所运行的jvm虚拟机的调优。

shell
cd /gc
wget http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.34/bin/apache-tomcat-8.5.34.tar.gz

tar -xvf apache-tomcat-8.5.34.tar.gz
cd apache-tomcat-8.5.34/conf

#对tomcat进行优化配置
vim server.xml

#优化一:禁用AJP服务,一般是使用Nginx+tomcat的架构,所以用不着AJP协议,所以把AJP连接器禁用。
#将下面的配置注释掉
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

#优化二:设置线程池,并且调整最大并发线程数
<!--将注释打开-->
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true" maxQueueSize="500"/>
<!--
参数说明:
maxThreads:最大并发数,默认设置 200,一般建议在 500 ~ 1000,根据硬件设施和业务来判断
minSpareThreads:Tomcat 初始化时创建的线程数,默认设置 25
prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化 minSpareThreads 的参数值,如果不等于 true,minSpareThreads 的值就没啥效果了
maxQueueSize,最大的等待队列数,超过则拒绝请求	
-->

<!--在Connector中设置executor属性指向上面的执行器-->
<Connector executor="tomcatThreadPool"  port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

#优化三:设置tomcat运行模式为nio2,tomcat的运行模式有3种:bio、nio、apr,其中nio2是nio的升级版,在tomcat8中才支持的,建议采用nio2模式。
    <Connector executor="tomcatThreadPool"  port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
               connectionTimeout="20000"
               redirectPort="8443" />
               
               
#启动tomcat
cd /gc/apache-tomcat-8.5.34/bin/
./startup.sh && tail -f ../logs/catalina.out

#打开浏览器进行测试访问
http://192.168.31.133:8080/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

image-20200513101514585

1.1.3、部署web项目 ​

为了方便测试性能,我们将部署一个java web项目,这个项目本身和本套课程没有什么关系,仅仅用于测试。

在资料中找到itcast-gc-demo-1.0.war,上传到linux服务器,进行部署安装。

shell
cd /gc/apache-tomcat-8.5.34/webapps
rm -rf *
mkdir ROOT
cd ROOT/

rz上传war包
jar -xvf itcast-gc-demo-1.0.war
rm -rf itcast-gc-demo-1.0.war
1
2
3
4
5
6
7
8
9

重新启动tomcat。

查看是否已经启动成功:http://192.168.31.133:8080/movie/query

image-20200520221416495

1.1.4、测试项目 ​

实现的功能:

  • 读取电影文件movies.dat,载入到内存
  • 查询电影数据时,随机返回1w ~ 10W个数据

代码:

java
package cn.itcast.gc.controller;

import cn.itcast.gc.service.MovieService;
import cn.itcast.gc.vo.Movie;
import cn.itcast.gc.vo.MyResult;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("movie")
public class MovieController {

    @Autowired
    private MovieService movieService;

    @GetMapping("query")
    public MyResult queryMovieList(){
        MyResult myResult = new MyResult();
        List<Movie> movies = this.movieService.queryMovieList();
        myResult.setCount(movies.size());
        myResult.setTop10(movies.subList(0, 9)); //只返回前10条数据,不要将性能损耗在传输上
        return myResult;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java
package cn.itcast.gc.service;

import cn.itcast.gc.vo.Movie;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

@Service
public class MovieService {

    private static final List<Movie> MOVIE_LIST = new ArrayList<>();

    @Autowired
    private ResourceLoader resourceLoader;

    @PostConstruct
    public void init() {
        try {
            //读取电影数据文件到内存
            Resource resource = resourceLoader.getResource("classpath:movies.dat");
            InputStream inputStream = resource.getInputStream();
            List<String> lines = IOUtils.readLines(inputStream, "UTF-8");
            for (String line : lines) {
                String[] ss = StringUtils.split(line, "::");
                Movie movie = new Movie();
                movie.setId(Long.valueOf(ss[0]));
                movie.setTitle(ss[1]);
                movie.setPeoples(ss[2]);
                //将数据放入到集合中
                MOVIE_LIST.add(movie);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public List<Movie> queryMovieList() {
        //随机1w ~ 10w条数据
        int count = RandomUtils.nextInt(10000, 100000);
        List<Movie> list = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            int index = RandomUtils.nextInt(0, MOVIE_LIST.size() - 1);

            Movie movie = MOVIE_LIST.get(index);

            Movie newMovie = new Movie(); //为了加大内存的损耗,创建新的对象
            newMovie.setId(movie.getId());
            newMovie.setPeoples(movie.getPeoples());
            newMovie.setTitle(movie.getTitle());

            list.add(newMovie);
        }
        return list;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

1.2、压力测试 ​

下面我们通过jmeter进行压力测试,先测得在初始状态下的并发量等信息,然后我们在对jvm做调优处理,再与初始状态测得的数据进行比较,看调好了还是调坏了。

首先需要对jmeter本身的参数调整,jmeter默认的的内存大小只有1g,如果并发数到达300以上时,将无法正常执行,会抛出内存溢出等异常,所以需要对内存大小做出调整。

修改jmeter.bat文件:

set HEAP=-Xms1g -Xmx4g -XX:MaxMetaspaceSize=512m

在该文件中可以看到,jmeter默认使用的垃圾收集器是G1.

Defaults to '-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20'

1.2.1、添加gc日志相关参数 ​

shell
#内存设置较小是为了更频繁的gc,方便观察效果,实际要比此设置的更大

JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms64m -Xmx128m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
1
2
3

重新启动tomcat。

1.2.2、创建测试用例 ​

image-20200515095110608

image-20200515095055594

1.2.3、初始测试 ​

初始测试使用200的并发进行测试,每个线程执行100次,总数为20000次请求。

测试结果:

image-20200520233011095

平均响应时间:3033ms

吞吐量:32.4/sec (每秒的请求量)

错误率:0.87%

image-20200516095910522

测试过程中的cpu、内存等信息。

1.2.4、GC日志解析 ​

在初始测试中,将生成的gc日志上传到gceasy进行分析,我们将看到一些非常有价值的信息,对我们的后续调优有着重要的指导作用。

image-20200516102656542

在jvm内存统计中可以看出,年轻代与老年代在高峰时,基本将可用空间都占满了,说明内存空间不足,需要调整内存大小。

image-20200516103021947

关键指标中,吞吐量为98.694%,gc平均停顿时间为8.19ms,最大停顿时间为180ms。

可以看出,在初始状态时,吞吐量并不高,最大停顿时间较长,平均停顿时间表现不错。

image-20200516103752776

在图表中可以看出,gc之后堆内存的使用基本是在60m ~ 80m之间。

image-20200516103946735

在图表中可以看出,gc之前的堆内存的使用在高峰时,将占用到80m ~ 120m之间。

image-20200516104200626

在gc持续时间统计中,可以看出full gc的时间要远高于younggc的时间,在调优时应当尽量的减少full gc。

image-20200516104437510

在清理垃圾的统计中,可以看出gc清理的垃圾基本维持在40m左右,最多的一次是发生在full gc,可以推断此次是内存即将耗尽,发生了fullgc,释放了大量的内存空间,这也是在前面年轻代对象有部分进入到老年代,在此次fullgc时被清理了。

image-20200516104817613

从年轻代的gc情况来看,gc之前与gc之后差较大,说明垃圾对象在年轻代被清理的比较多,就是说临时性的对象居多。

image-20200516105519182

从老年代的gc情况来看,gc之前与之后的差并不大,说明老年代的垃圾对象并不是很多。

image-20200516105647952

Meta Space空间充足,基本没有变化,占用空间40m左右。

image-20200516105923726

从该图中看出,晋升到老年代的对象与可以分配对象相比,非常的少,也说明了上面我们看到的,对象主要集中在young区。

image-20200516110511764

在GC统计中,可以看出:

  • Minor GC清理掉的垃圾对象合计24.13gb,说明产生的临时对象非常的多
  • Minor GC的执行间隔为636ms,说明发生gc的行为是比较频繁的
  • Full GC发生了10次,较为频繁
  • Full GC的平均持续时间为90ms,时间较长
  • GC的暂停次数为690次,暂停次数将影响到服务的响应时间

image-20200516111736684

在对象的统计中,可以看出对象的平均生成率:57.85m/s,平均的晋升率:610kb/s。

image-20200516112219172

在GC原因统计中可以看出,大部分发生gc的原因都为分配失败,也就说内存不足导致;

需要说明的是虽然发生了4次Metadata GC,并不是Metaspace不足导致,前面我们看到Metaspace空间充足,而是该gc发生在最开始时间,说明初始的Metaspace不足,导致了Metaspace扩容,并发生了GC,可以适当调整Metaspace初始大小以减少Metadata GC次数。

1.3、调优一:调整内存 ​

在jvm调优中,调整内存大小是调优手段中最为基本的一种手段,但是需要注意的是,内存的调整并不是简单的加大内存,而是需要结合业务特性、gc类型等内容进行调整。

对于我们目前测试的应用而言,属于及时响应、低延迟的应用,这样的应用在jvm堆内存中,对象的存活时间较短,所以应该将年轻代的内存调大些。

shell
#设置堆内存为1024m
#设置年轻代大小为512m,默认是堆内存的1/3
#设置初始的Metaspace大小为64m

JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms1024m -Xmx1024m -XX:NewSize=512m -XX:MetaspaceSize=64m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
1
2
3
4
5

测试结果:

image-20200516153705072

image-20200516154018928

可以看到内存基本够用,从结果来看,可以进一步扩大年轻代的大小。

image-20200516154126081

吞吐量与停顿时间都比较理想。

image-20200516154204633

可以看出,没有发生Full GC,Minor GC的次数明显下降。情况有了很大的改善。

image-20200516154306311

GC原因中,也只有一种原因,这是属于正常的情况。

总体来讲,通过调整内存大小,对于服务的性能有了显著的提升。

下面尝试下,加大并发到400,看下表现怎么样?

image-20200516154850700

可以看到,在400并发的情况下,吞吐量为395,平均响应时间为945ms,相比较200并发下的结果,稍微差了一下,整体还算ok。

可以按照这个方法,测试500、800、1000或者更高并发。

关于jmeter测试时,可能出现的java.net.BindException: Address already in use: connect异常,解决方案如下:

https://blog.csdn.net/zhangxiaohui4445/article/details/100162317

1.4、调优二:更换G1收集器 ​

选择性能更优的垃圾收集器也是调优的手段之一,在jdk8中,使用率最高的当属G1收集器了,下面我们就尝试切换成G1收集器,来看下它的表现。

shell
JAVA_OPTS="-XX:+UseG1GC -Xmx1024m -XX:MetaspaceSize=64m -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
1

测试结果:

image-20200516194058365

这个结果要比同等内存大小的ParallelGC性能稍好一些,但是提升并不明显。

image-20200516194430240

自分配的内存区域大小,也基本符合需求。

image-20200516194510815

在吞吐量指标中,有所下降,但是停顿时间都有所减少。

总结:

更换G1垃圾收集器后,其性能有所提升,但是并不明显,原因有两个:

  • 调优一中并未出现明显的问题,所以相对比,不是很明显
  • G1垃圾收集器适合大内存低延迟的场景,比如设置6G、8G内存的场景下保持低延迟

1.5、调优三:更换ZGC收集器 ​

下面我们将垃圾收集器换成ZGC,需要注意的时,jdk需要切换到jdk11版本。

shell
JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx1024m -XX:MetaspaceSize=64m -Xlog:gc*:../logs/gc.log -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
1

测试结果:

image-20200516205415495

测试结果中的吞吐量与平均响应时间要比G1稍差一些,差距并不太大。

image-20200516205857488

在GC方面的表现,无论是吞吐量还是停顿时间均有不俗的表现。

综合起来看的话,ZGC的表现还是很不错的,如果给其设置大内存,依然可以得到较短的停顿时间。

1.6、调优建议 ​

对于JVM的调优,给出大家几条建议:

  • 生产环境的JVM一定要进行参数设定,不能全部默认上生产。
  • 对于参数的设定,不能拍脑袋,需要通过实际并发情况或压力测试得出结论。
  • 对于内存中对象临时存在居多的情况,将年轻代调大一些。如果是G1或ZGC,不需要设定。
  • 仔细分析gceasy给出的报告,从中分析原因,找出问题。
  • 对于低延迟的应用建议使用G1或ZGC垃圾收集器。
  • 不要将焦点全部聚焦jvm参数上,影响性能的因素有很多,比如:操作系统、tomcat本身的参数等。

1.7、PerfMa ​

PerfMa提供了JVM参数分析、线程分析、堆内存分析功能,界面美观,功能强大,我们在做jvm调优时,可以作为一个辅助工具。官网:https://www.perfma.com/

image-20200517111303283

1.7.1、XXFox ​

XXFox是Java虚拟机参数分析工具,可以对JVM参数进行查询、检查、优化等。

参数优化:

image-20200517112655905

参数生成:

image-20200517112732210

1.7.2、XSheepdog ​

XSheepdog是Java线程Dump分析工具,通过jstack生成线程的dump文件,进行分析。

image-20200517112828151

1.7.3、XElephant ​

XElephant是Java内存Dump分析工具。

获取内存 Dump 文件的命令:

shell
jmap -dump:format=b,file=生成的文件名 进程号
1

生成 Dump 文件后,就可以通过 控制台 > Java内存Dump分析 ,选择合适的途径上传到服务器进行分析。

image-20200517113324581

2、百亿级流量电商大促高并发系统下JVM如何调优? ​

image-20200518120538745

结合前面我们所学习到知识,我们来看个实际的案例,百亿流量电商大促高并发,如何调优?

面对这个问题,首先不要被标题吓到,需要我们冷静、仔细的分析,才能解决这个问题。

image-20200518145401873

  • 百亿流量的电商网站,一般日活跃用户可能在1亿左右,大促期间的参与人较平常会增加很多,比如5亿左右。

  • 电商系统的业务线非常多,大促期间压力最大的属订单、支付业务,我们聚焦到订单系统。

  • 假设,部署了订单系统的微服务数量为100,每台机器4核8G内存。

  • 一般情况下,电商网站的付费转化率为10%左右,所以,1亿的活跃用户会产生1000w订单。

    • 一天24小时,以12小时下单时间计算,每小时产生83w订单,每秒钟产生230左右个订单。
    • 平均到100台机器,每台机器2~3个订单/秒,可以说毫无压力。
  • 大促期间,转化量会提升,假设提升到50%左右,所以,5亿活跃用户会产生2.5亿订单。

    • 大促期间的用户下单与平常的有所不同,这里我们假设,一天下单时间以20小时计算,前1小时,下单30%,其它时间段产生70%的订单。
    • 前一小时产生30%订单,也就是7千500万订单,每秒钟将会产生20833个订单,分摊到100台服务器,每台服务器208个/秒订单的产生。
    • 其它时间段产生70%的订单,也就是1亿7千5百万个订单,每小时产生920万订单,每秒产生2558个订单,分摊到100台机器,每天机器26个/秒订单的产生。
  • 通过上面的分析,一般情况和大促的非集中时间段,产生的订单量较少,每台机器的压力并不大,所以我们只需要关注大促开始的1小时的压力即可。

  • 假设每个订单对象的大小为1KB,考虑到每个订单的产生会有复杂的业务流程,我们将订单对象大小扩大100倍,也就是,每产生一个订单对象,会占用100KB的内存空间。

  • 也就是说,在大促期间,前一小时,每秒钟占用的内存空间为:100KB * 208 = 20800KB 也就是 20M左右空间。

  • 每台机器的内存是8G,除去操作系统、监控等内存开销,配置到jvm虚拟机的堆内存空间为6G。

  • 考虑到订单对象在内存中创建后,会很快被持久化到数据库,随后就会GC回收,所以属于短暂性存活对象,尽可能的让其待在年轻代,避免过多的对象进入老年代,从而导致Full GC。

  • 在JDK8中,建议使用G1垃圾收集器,下面是给出的jvm主要参数建议:

    • shell
      -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -Xmx6G -XX:MetaspaceSize=128m -XX:MaxGCPauseMillis=100 -XX:G1NewSizePercent=50 -XX:G1MaxNewSizePercent=80
      1
    • -XX:G1NewSizePercent

      • 设置要用作年轻代大小最小值的堆百分比。默认值是 Java 堆的 5%。(实验性质参数)
    • -XX:G1MaxNewSizePercent

      • 设置要用作年轻代大小最大值的堆大小百分比。默认值是 Java 堆的 60%。(实验性质参数)
    • 注意:该参数仅供参考,因为每个人的环境、业务等均不相同,大家需领悟其意义,不可生搬硬套。

3、类文件结构 ​

了解Java的类文件结构,可以帮助我们进一步的对jvm的理解,在做jvm调优时,可以做到知其所以然。

Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是什么样的的结构,虚拟机又是如何处理去执行class文件里面的内容呢?

3.1、MyDemo ​

java
package cn.itcast.jvm;

public class MyDemo {

    private int a = 1;
    private int b = 2;

    public int sum() {
        int c =  a + b;
        return c;
    }

    public static void main(String[] args) {
        MyDemo myDemo = new MyDemo();
        int sum = myDemo.sum();
        System.out.println(sum);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

上面的java文件编译得到的class文件,class文件是一个16进制字节码的二进制文件,我们通过winhex软件打开如下:

image-20200518160828247

3.2、字节码 ​

要想读懂上面的字节码内容,就需要通过字节码表来查询对应的含义了。

3.2.1、字节码表 ​

需要说明的是,class文件只有两种数据类型:无符号数 和 表。

  • 无符号数
    • 无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。
    • 其中无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节。
  • 表
    • 表是由多个无符号数或其他表构成的复合数据结构。
    • 所有的表都以“_info”结尾,由于表没有固定长度,所以通常会在其前面加上个数说明。
类型名称说明长度
u4magic魔数,识别Class文件格式4个字节
u2minor_version副版本号2个字节
u2major_version主版本号2个字节
u2constant_pool_count常量池计算器2个字节
cp_infoconstant_pool常量池n个字节
u2access_flags访问标志2个字节
u2this_class类索引2个字节
u2super_class父类索引2个字节
u2interfaces_count接口计数器2个字节
u2interfaces接口索引集合2个字节
u2fields_count字段个数2个字节
field_infofields字段集合n个字节
u2methods_count方法计数器2个字节
method_infomethods方法集合n个字节
u2attributes_count附加属性计数器2个字节
attribute_infoattributes附加属性集合n个字节

3.2.2、魔数 ​

在class文件中,前4个字节就是魔数,如下:

image-20200518162002007

魔数是用来区分文件类型的一种标识,0XCAFEBABE (咖啡豆/咖啡宝贝)表示就是class文件,之所以不使用文件后缀名区别就是后缀名太容易被改了,不安全。

3.2.3、版本号 ​

魔数后面的4位就是版本号了,同样也是4个字节,其中前2个字节表示副版本号,后2个字节表示主版本号。

image-20200518164635988

前面两个字节是0x0000,也就是其值为0; 后面两个字节是0x0034,也就是其值为52; 所以上面的代码就是52.0版本来编译的,也就是jdk1.8.0。

image-20200518164806469

3.3、常量池 ​

在版本号的后面就是常量池了。

3.3.1、常量池容量计数值 ​

image-20200518165709246

由于常量池的数量不固定,所以需要通过2个字节来记录常量池的大小,其值为0X002E,用十进制表示就是46,需要注意的是,常量池中只有45个常量。

与Java中语言习惯不同,这个容量计数是从1而不是0开始的,在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

3.3.2、常量类型和结构 ​

常量池中的每一项都是一个表,其项目类型共有14种,如下表格所示:

类型标志描述
CONSTANT_utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整形字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MothodType_info16标志方法类型
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点

这14种类型的结构各不相同,如下表格所示:

img

注:此表格的类型的单位不对,不是bit,应该是byte(字节)。

从上面的表格可以看到,虽然每一项的结构都各不相同,但是他们有个共同点,就是每一项的第一个字节都是一个标志位,标识这一项是哪种类型的常量。

3.3.3、第一个常量 ​

image-20200518202118853

可以看到,第一个常量的值为10,对应到表中的标志位,找到常量为 CONSTANT_Methodref_info (类中方法的符号引用),它的结构为:

image-20200518202321773

后面的4个字节都是它的内容,记录着2个索引值:

image-20200518202928025

第一个索引值为:0X0009,也就是9,指向常量池中第9项的索引。

第二个索引值为:0X001E,也就是30,指向常量池中第30项的索引。

3.3.4、第二个常量 ​

image-20200518210545461

可以看到,第一个常量的值为9,对应到表中的标志位,找到常量为 CONSTANT_Fieldref_info(字段的符号引用),它的结构为:

image-20200518210635165

后面的4个字节都是它的内容,记录着2个索引值:

image-20200518210744329

第一个索引值为:0X0004,也就是4,指向常量池中第4项的索引。

第二个索引值为:0X001F,也就是31,指向常量池中第31项的索引。

3.3.5、javap 编译字节码 ​

通过javap命令就可以将class文件转化为可读的字节码指令。

shell
javap -v MyDemo.class > MyDemo.txt
1

生成的字节码内容如下,可以看到与我们前面分析的是一致的。

shell
Classfile /F:/code/itcast-jvm/out/production/itcast-jvm/cn/itcast/jvm/MyDemo.class
  Last modified 2020-5-18; size 743 bytes
  MD5 checksum 871ca2c3441314943f47358a3a709d3c
  Compiled from "MyDemo.java"
public class cn.itcast.jvm.MyDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#30         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#31         // cn/itcast/jvm/MyDemo.a:I
   #3 = Fieldref           #4.#32         // cn/itcast/jvm/MyDemo.b:I
   #4 = Class              #33            // cn/itcast/jvm/MyDemo
   #5 = Methodref          #4.#30         // cn/itcast/jvm/MyDemo."<init>":()V
   #6 = Methodref          #4.#34         // cn/itcast/jvm/MyDemo.sum:()I
   #7 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Methodref          #37.#38        // java/io/PrintStream.println:(I)V
   #9 = Class              #39            // java/lang/Object
  #10 = Utf8               a
  #11 = Utf8               I
  #12 = Utf8               b
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcn/itcast/jvm/MyDemo;
  #20 = Utf8               sum
  #21 = Utf8               ()I
  #22 = Utf8               c
  #23 = Utf8               main
  #24 = Utf8               ([Ljava/lang/String;)V
  #25 = Utf8               args
  #26 = Utf8               [Ljava/lang/String;
  #27 = Utf8               myDemo
  #28 = Utf8               SourceFile
  #29 = Utf8               MyDemo.java
  #30 = NameAndType        #13:#14        // "<init>":()V
  #31 = NameAndType        #10:#11        // a:I
  #32 = NameAndType        #12:#11        // b:I
  #33 = Utf8               cn/itcast/jvm/MyDemo
  #34 = NameAndType        #20:#21        // sum:()I
  #35 = Class              #40            // java/lang/System
  #36 = NameAndType        #41:#42        // out:Ljava/io/PrintStream;
  #37 = Class              #43            // java/io/PrintStream
  #38 = NameAndType        #44:#45        // println:(I)V
  #39 = Utf8               java/lang/Object
  #40 = Utf8               java/lang/System
  #41 = Utf8               out
  #42 = Utf8               Ljava/io/PrintStream;
  #43 = Utf8               java/io/PrintStream
  #44 = Utf8               println
  #45 = Utf8               (I)V
{
  public cn.itcast.jvm.MyDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: aload_0
        10: iconst_2
        11: putfield      #3                  // Field b:I
        14: return
      LineNumberTable:
        line 3: 0
        line 5: 4
        line 6: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcn/itcast/jvm/MyDemo;

  public int sum();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: aload_0
         5: getfield      #3                  // Field b:I
         8: iadd
         9: istore_1
        10: iload_1
        11: ireturn
      LineNumberTable:
        line 9: 0
        line 10: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lcn/itcast/jvm/MyDemo;
           10       2     1     c   I

  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           #4                  // class cn/itcast/jvm/MyDemo
         3: dup
         4: invokespecial #5                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #6                  // Method sum:()I
        12: istore_2
        13: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #8                  // Method java/io/PrintStream.println:(I)V
        20: return
      LineNumberTable:
        line 14: 0
        line 15: 8
        line 16: 13
        line 17: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      21     0  args   [Ljava/lang/String;
            8      13     1 myDemo   Lcn/itcast/jvm/MyDemo;
           13       8     2   sum   I
}
SourceFile: "MyDemo.java"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

内容大致分为4个部分:

第一部分:显示了生成这个class的java源文件、版本信息、生成时间等。

第二部分:显示了该类中所涉及到常量池,共45个常量。

第三部分:显示该类的构造器,编译器自动插入的。

第四部分:显示了sum、main方的信息。(这个是需要我们重点关注的)

3.4、描述符 ​

3.4.1、字段描述符 ​

官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2

FieldType termTypeInterpretation
Bbytesigned byte
CcharUnicode character code point in the Basic Multilingual Plane, encoded with UTF-16
Ddoubledouble-precision floating-point value
Ffloatsingle-precision floating-point value
Iintinteger
Jlonglong integer
LClassName;referencean instance of class ClassName
Sshortsigned short
Zbooleantrue or false
[referenceone array dimension

3.4.2、方法描述符 ​

官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3

示例:

The method descriptor for the method:

java
Object m(int i, double d, Thread t) {...}
1

is:

java
(IDLjava/lang/Thread;)Ljava/lang/Object;
1

3.5、实例:字符串拼接 ​

字符串的拼接在开发过程中使用是非常频繁的,常用的方式有三种:

  • +号拼接: str+"456"
  • StringBuilder拼接
  • StringBuffer拼接

StringBuffer是保证线程安全的,效率是比较低的,我们更多的是使用场景是不会涉及到线程安全的问题的,所以更多的时候会选择StringBuilder,效率会高一些。

那么,问题来了,StringBuilder和“+”号拼接,哪个效率高呢?接下来我们通过字节码的方式进行探究。

首先,编写个示例:

java
package cn.itcast.jvm;

public class TestString {

    public static void main(String[] args) {
        new TestString().m1();
        new TestString().m2();
    }

    public void m1(){
        String s1 = "123";
        String s2 = "456";
        String s3 = s1 + s2;
        System.out.println(s3);
    }

    public void m2(){
        String s1 = "123";
        String s2 = "456";
        StringBuilder sb = new StringBuilder();
        sb.append(s1);
        sb.append(s2);
        String s3 = sb.toString();
        System.out.println(s3);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

通过字节码的方式进行查看:

java
#命令:javap -c TestString.class
    
Compiled from "TestString.java"
public class cn.itcast.jvm.TestString {
  public cn.itcast.jvm.TestString();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class cn/itcast/jvm/TestString
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: invokevirtual #4                  // Method m1:()V
      10: new           #2                  // class cn/itcast/jvm/TestString
      13: dup
      14: invokespecial #3                  // Method "<init>":()V
      17: invokevirtual #5                  // Method m2:()V
      20: return

  public void m1();
    Code:
       0: ldc           #6                  // String 123
       2: astore_1
       3: ldc           #7                  // String 456
       5: astore_2
       6: new           #8                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: aload_2
      18: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_3
      29: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      32: return

  public void m2();
    Code:
       0: ldc           #6                  // String 123
       2: astore_1
       3: ldc           #7                  // String 456
       5: astore_2
       6: new           #8                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
      13: astore_3
      14: aload_3
      15: aload_1
      16: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: pop
      20: aload_3
      21: aload_2
      22: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      25: pop
      26: aload_3
      27: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      30: astore        4
      32: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: aload         4
      37: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

从解字节码中可以看出,m1()方法源码中是使用+号拼接,但是在字节码中也被编译成了StringBuilder方式。

所以,可以得出结论,字符串拼接,+号和StringBuilder是相等的,效率一样。

小结:可以通过查看字节码的方式对代码的底层做研究,探究其原理。

4、类加载机制 ​

Java源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行。虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

4.1、类加载时机 ​

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如下图所示。

image-20200519144638985

上图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

需要注意的是,按部就班地“开始”,而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。

关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  • 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令;
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候;
  • 当初始化一个类的时候,发现其父类还没有进行初始化的时候,需要先触发其父类的初始化;
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类;
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有初始化。
  • 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

对于这六种会触发类型进行初始化的场景,《Java虚拟机规范》中使用了一个非常强烈的限定语——“有且只有”,这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。

比如如下几种场景就是被动引用:

  • 通过子类引用父类的静态字段,不会导致子类的初始化; image-20200519145923590
  • 通过数组定义来引用类,不会触发此类的初始化; image-20200519150021606
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化; image-20200519150038247

4.2、类加载过程 ​

4.2.1、加载 ​

在加载阶段,Java虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

4.2.2、验证 ​

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段大致上会完成下面 4 个阶段的检验动作:

  • 文件格式验证
    • 第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能够被当前版本的虚拟机处理。验证点主要包括:
      • 是否以魔数 0xCAFEBABE 开头;
      • 主、次版本号是否在当前虚拟机处理范围之内;
      • 常量池的常量中是否有不被支持的常量类型;
      • Class 文件中各个部分及文件本身是否有被删除的或者附加的其它信息等等。
  • 元数据验证
    • 第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,这个阶段的验证点包括:
      • 这个类是否有父类;
      • 这个类的父类是否继承了不允许被继承的类;
      • 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法;
      • 类中的字段、方法是否与父类产生矛盾等等。
  • 字节码验证
    • 第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证
    • 最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段--解析阶段中发生。
    • 符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。

4.2.3、准备 ​

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。

4.2.4、解析 ​

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

4.2.5、初始化 ​

类初始化阶段是类加载过程中的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制的。

到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。

4.3、类加载器 ​

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

4.3.1、类与类加载器 ​

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

4.3.2、双亲委派模型 ​

从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 来实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由 Java 来实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

从 Java 开发者的角度来看,类加载器可以划分为:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在 <java_home>\lib 目录中的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,那直接使用 null 代替即可;
  • 扩展类加载器(Extension ClassLoader):这个类加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 <java_home>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;
  • 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$App-ClassLoader 实现。getSystemClassLoader() 方法返回的就是这个类加载器,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这 3 种类加载器互相配合进行加载的,在必要时还可以自己定义类加载器。它们的关系如下图所示:

image-20200519153346549

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

双亲委派模型的工作过程是:

  • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类
  • 而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此
  • 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

这样做的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object,它放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器来加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基本的行为也就无法保证了。

双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现却异常简单,用以实现双亲委派的代码只有短短十余行,全部集中在java.lang.ClassLoader的loadClass()方法之中:

java
protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    // 首先,检查请求的类是不是已经被加载过
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类抛出 ClassNotFoundException 说明父类加载器无法完成加载
        }

        if (c == null) {
            // 如果父类加载器无法加载,则调用自己的 findClass 方法来进行类加载
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

5、编译优化 ​

如何使我们编写的代码可以更加高效的执行,除了源码层面的优化外,虚拟机中的编译器也是在背后默默的为代码做着优化工作,下面我们就来了解下编译器做的一些优化行为。

5.1、前端编译器 ​

前端编译器就是将*.java文件编译成*.class文件的过程。

前端编译器能够做的优化是非常有限的,主要的优化工作在后端编译器完成。

5.1.1、javac编译过程 ​

javac的编译过程大致可以分为1个准备过程和3个处理过程,它们分别如下所示。

  • 准备过程:初始化插入式注解处理器。
  • 解析与填充符号表过程
    • 词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
    • 填充符号表。产生符号地址和符号信息。
      • 符号表(Symbol Table)是由一组符号地址和符号信息构成的数据结构。
  • 插入式注解处理器的注解处理过程。
  • 分析与字节码生成过程
    • 标注检查。对语法的静态信息进行检查。
    • 数据流及控制流分析。对程序动态运行过程进行检查。
    • 解语法糖。将简化代码编写的语法糖还原为原有的形式。
    • 字节码生成。将前面各个步骤所生成的信息转化成字节码。

image-20200519202757532

上图是Javac编译动作的入口是com.sun.tools.javac.main.JavaCompiler类主要执行流程,页反应出javac的执行流程。

5.2、后端编译与优化 ​

后端编译器是指把Class文件转换成与本地基础设施(硬件指令集、操作系统)相关的二进制机器码的过程。

后端编译器一般分为即时编译器与提前编译器:

  • 即时编译器
    • 在HotSpot中有三种编译器
    • 客户端编译器”(Client Compiler)
    • 服务端编译器”(Server Compiler)
    • Graal编译器(JDK10中出现)
  • 提前编译器
    • Android中的ART就是提前编译器

5.2.1、编译器的优化 ​

编译器的目标虽然是做由程序代码翻译为本地机器码的工作,但其实难点并不在于能不能成功翻译出机器码,输出代码优化质量的高低才是决定编译器优秀与否的关键。

OpenJDK的官方Wiki上,HotSpot虚拟机设计团队列出了一个相对比较全面的、即时编译器中采用的优化技术列表,地址:https://wiki.openjdk.java.net/display/HotSpot/PerformanceTacticIndex

image-20200519231038983

一般来讲,最具代表性的优化技术有四种,分别是:

  • 方法内联
  • 逃逸分析
  • 公共子表达式消除
  • 数组边界检查消除

5.2.2、方法内联 ​

方法内联,是指JVM在运行时将调用次数达到一定阈值的方法调用替换为方法体本身,从而消除调用成本,并为接下来进一步的代码性能优化提供基础,是JVM的一个重要优化手段之一。

举个例子:

java

static class B {
    int value;
    final int get() {
        return value;
    }
}

//未内联的代码
public void foo() {
    y = b.get();
    // ...do stuff...
    z = b.get();
    sum = y + z;
}

//内联优化的代码
public void foo() {
    y = b.value;
    // ...do stuff...
    z = b.value;
    sum = y + z;
}

//可以继续优化

//冗余存储消除的代码
public  void foo() {
    y = b.value;
    // ...do stuff...
    z = y;
    sum = y + z;
}

//复写传播的代码
public  void foo() {
    y = b.value;
    // ...do stuff...
    y = y;
    sum = y + y;
}

//进行无用代码消除的代码
public  void foo() {
    y = b.value;
    // ...do stuff...
    sum = y + y;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

需要注意的是,以上优化是在编译器内部完成,而不是在代码中完成的,只是用代码来说明问题。

5.2.3、逃逸分析 ​

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术,它并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。

逃逸分析的基本原理是:

  • 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;
  • 甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
  • 从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低,则可能为这个对象实例采取不同程度的优化。

从JDK 7时服务端编译器默认开启了这项优化服务。

示例(伪代码):

java
// 完全未优化的代码
public int test(int x) {
    int xx = x + 2;
    Point p = new Point(xx, 42); //包含x和y坐标的POJO
    return p.getX();
}

//第一步,将Point的构造函数和getX()方法进行内联优化:
public int test(int x) {
    int xx = x + 2;
    Point p = point_memory_alloc();   // 在堆中分配P对象的示意方法
    p.x = xx;                         // Point构造函数被内联后的样子
    p.y = 42
    return p.x;                       // Point::getX()被内联后的样子
}

//第二步,经过逃逸分析,发现在整个test()方法的范围内Point对象实例不会发生任何程度的逃逸
public int test(int x) {
    int xx = x + 2;
    int px = xx;
    int py = 42
    return px;
}
//第二步的优化,将内存的开销从堆转移到栈,内存空间会随着线程销毁而释放,省去了堆中垃圾回收等操作

//第三步,通过数据流分析,发现py的值其实对方法不会造成任何影响,那就可以放心地去做无效代码消除得到最终优化结果
public int test(int x) {
    return x + 2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

5.2.4、公共子表达式消除 ​

公共子表达式消除是一项非常经典的、普遍应用于各种编译器的优化技术。

它的含义是:如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。

对于这种表达式,没有必要花时间再对它重新进行计算,只需要直接用前面计算过的表达式结果代替E。

如果这种优化仅限于程序基本块内,便可称为局部公共子表达式消除,如果这种优化的范围涵盖了多个基本块,那就称为全局公共子表达式消除。

java
//源码
int d = (c * b) * 12 + a + (a + b * c);

//编译器检测到c*b与b*c是一样的表达式,而且在计算期间b与c的值是不变
int d = E * 12 + a + (a + E);

//还可以进一步优化
int d = E * 13 + a + a;
1
2
3
4
5
6
7
8

5.2.5、数组边界检查消除 ​

数组边界检查消除是即时编译器中的一项语言相关的经典优化技术。

如果有一个数组foo[],在Java语言中访问数组元素foo[i]的时候系统将会自动进行上下界的范围检查,即i必须满足“i>=0&&i<foo.length”的访问条件,否则将抛出一个运行时异常:java.lang.ArrayIndexOutOfBoundsException。

对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这必定是一种性能负担。

对于编译器而言,根据数据流分析来确定是否越界,如果没有越界,那么执行时就不需要再判断了。

如果在循环中,本身就是通过循环变量来控制对数组的访问,执行时也就不用再判断了,

这样就可以把整个数组的上下界检查消除掉,这可以节省很多次的条件判断操作。

← 垃圾回收 判断垃圾、回收算法、各类收集器








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系方式
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节