1. java字节码技术
1.1 什么是字节码?
字节码(Java bytecode),是由Java编译器把Java代码转换后,可以由java虚拟机无脑执行的指令集。也是java跨平台的核心所在。Java维护者(组织)为所有主流操作系统提供了一个Java虚拟机,这些虚拟机向上可以识别java字节码,向下则适配本地环境,执行字节码里面的指令,在转换成cpu执行指令。
它是程序的一种低级表示,可以运行于Java虚拟机上。将程序抽象成字节码可以保证Java程序在各种设备上的运行。计算机里面的很多事情问题都可以通过增加一个中间层来解决,很显然,字节码+JVM就是这么一个中间层,解决跨平台的问题。
Java bytecode由单字节(byte)的指令组成,理论上最左支持256个操作码(opcode)。实际上Java只是用了200个左右的操作码,还有一些操作码则保留给调试操作。
根据指令的性质,主要分为四个大类:
- 栈操作指令,包括与局部变量交互的指令
- Java虚拟机(JVM)是一个基于栈的计算机。所有的计算都发生在栈上。
- 程序流程控制指令
- 程序的流程控制指令,比如,for,if,函数调用
- 对象操作指令,包括方法调用指令
- java是一个面向对象的语言,创建一个对象,调用对象的方法。
- 算数运算以及类型转换指令。
1.2 查看字节码
- 代码
- 查看字节码
- javac xxx.java 生成class文件
- javap -c xxx.class 查看字节码
实际上,class文件里面保存的都是字节码,0到255的数字,上图展示的是助记符,方便记忆和阅读用的。aload_0,return等都有自己对应的操作码的。
- javap -c -verbose xxx.class 查看更信息的字节码信息
上图中,有jdk的版本号,类的属性(public),还有常量表。代码行号表(调试的时候可以看到指令对于的行号)。
- 执行流程
执行的时候,从常量表中获取到常量值,在放到程序栈(变量表)中尽心计算。
2. 类加载
2.1 类的生命周期
- 加载(Loading):找class文件,并读入程序内存中
- 通过类名com.xxx.Class从各种classpath目录里面找到对应类,也可以自定义类加载器,从网络上加载类或jar包。
- 验证(Verification): 验证格式,依赖
- 验证格式是否正确。版本号。
- 类之间的相互引用关系。
- 准备(Preparation): 静态字段,方法表
- 抽取类里面的静态字段
- 抽取类里面的方法。
- 搭建类的结构(骨架)
- 解析(Resolution): 符号解析为引用
- 把各种符号替换成引用。
- 初始化(Initialization): 构造器,静态变量赋值,静态代码
- 静态变量赋值
- 静态代码执行
- 然后这个类就可以创建实例了。可以被使用了。
- 使用(Using): 创建类实例,并使用
- 卸载(Unloading): 清除类的信息
2.2 什么时候会加载类,会初始化类
- 当虚拟机启动时,初始化用户指定的主类。(main方法所在的类)
- 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类,就是new一个类的时候要初始化。(创建类的实例,那肯定需要类被加载了。)
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类。
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类。
- 子类的初始化会触发父类的初始化。
- 如果一个接口定义了default方法,那么直接实现或间接实现该接口的类的初始化,会触发该接口的初始化。
- 使用反射API对某个类进行反射调用时,初始化这个类,反射调用要么是已经有实例了,要么是静态方法,都需要初始化。
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
2.3 什么时候会加载类,不会初始化类
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。但子类肯定是被加载了的。
- 定义对象数组,不会触发类的初始化。数组只是一个声明,实际上还没有创建对象呢。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接一用定义常量的类,不会触发定义常量所在的类。比如:java字符串字面量"xxxx"其实就是一个String常量了。但是并不会触发String类的初始化。
- 通过类名获取Class对象,不会触发类的初始化。Hello.Class不会让Hello类初始化。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,不会触发类初始化,其实这个参数是告诉虚拟机,是否需要对类进行初始化。Class.forName("jvm.Hello")默认会初始化Hello类。
- 通过ClassLoader默认的loadClass方法,不会触发初始化动作(类加载了,但是不初始化)
所谓加载类,不初始化类指的就是,会从class文件里面加载类的字节码,但是并不会执行静态字段的赋值,静态代码块的执行。
2.4 类加载器
- 启动类加载器(BootstrapClassLoader)
- 加载JVM启动的核心系统类
- 扩展类加载器(ExtClassLoader)
- 扩展的类,也是在jdk里面自带的。
- 应用类加载器(AppClassLoader)
- 加载程序员写的代码,jar包。
- 怎么保证类不会重复加载?
- 双亲委托:应用类加载器在加载类的时候,会先去扩展类加载器里面找类是否已被加载,如果没有就去启动类加载器找,如果还没有,则自己加载类。
- 负责依赖:加载一个类的时候,还需要把这个类依赖的其它类也给加载进来。
- 缓存加载:类被加载完后,会缓存起来。
- 自定义类加载器
- 自定义类加载器加载出来的类是不一样的,哪怕都是从同一个class文件加载进来的类,但是实例是不能相互类型转换的。因为没有共同的上一级加载器。java中类其实也是一个对象,不同的加载器加载的类,就相当于2个类对象,虽然对象的内容是一样的,但是地址什么的就不一样了(比喻)。基于这个特性,可以加载不同版本的类,解决类兼容的问题,比如引入了一个外部工具,这个工具依赖了xxx.class 1.0.0。但是现有的代码也依赖的却是xxx.class 1.1.0这样就势必要加载2个版本的xxx.class了。那么自定义类加载器就派上用场了。
2.5 添加引用类的几种方式(就是发现类)
- 把类/jar放到JDK的lib/ext下,或者-Djava.ext.dirs指定类查找路径
- java-cp/classpath或者class文件放到当前路径
- 自定义ClassLoader加载。这个就比较灵活了,可以从网络上下载一个类。
- 拿到当前执行类的ClassLoader,反射调用addUrl方法添加jar或者路径。说白了,还是添加路径其他的类加载器才能找到类、jar包。(JDK9就不能用这种方法了,提供了新方法。)
3. JVM内存模型
4. JDK内置的命令行工具
4.1 工具功能展示
4.1.1 jps/jinfo
- 查看当前系统启动的java进程。就跟linux的ps命令一样,只是jps只显示java进程
- jps -mlv :查看更详细的信息。jvm的启动参数,垃圾回收算法等。
4.1.2 jstat
- jstat -gc 98800 1000 20 查看进程98800的内存,和gc情况,1000表示每秒刷新一次,显示20次。s0c存活区0的容量,s0u表示存活区0使用的内存数。EC表示伊甸区的容量。OC老年区的容量。MC表示元数据区的容量。YGC表示youngGC次数。YGCT表示youngGC的总时间。FGC全量垃圾回收的册数,FGCT表示全量GC的总时间。单位都是字节。
- jstat -gcutil 98800 1000 20: 查看各个区域内存的使用率。百分比
4.1.3 jmap 查看更详细的jvm信息,jvm里面的所有对象,以及对象个数,使用的字节数。如果某个对象特别多。可能就是内存泄漏了。
4.1.4 jstack
4.1.5 jcmd
- jcmd是一个比较综合的命令行工具。和上面的那些都是职责单一的。
- jcmd 14068 help :查看指定进程,支持哪些工具。
- jcmd pid Thread.print: 查看jvm的所有线程栈。
4.1.6 jrunscript/jjs
5. JDK内置图形化工具
说明:功能其实和命令行工具差不多,只是有窗口界面,比较方便。
5.1 jconsole
图形化显示JVM内存,线程,cpu的使用情况。
5.2 jvisualvm
抽样统计功能,比jconsole更好用一点。更强大一点。可以查看一段时间(单位时间)系统的状态。
5.3 jmc
死锁都能检测出来。