写点什么

深入 java week1-01 字节码、内存、GC、调试工具

发布于: 2020 年 10 月 21 日

1. java 字节码技术

1.1 什么是字节码?

字节码(Java bytecode),是由 Java 编译器把 Java 代码转换后,可以由 java 虚拟机无脑执行的指令集。也是 java 跨平台的核心所在。Java 维护者(组织)为所有主流操作系统提供了一个 Java 虚拟机,这些虚拟机向上可以识别 java 字节码,向下则适配本地环境,执行字节码里面的指令,在转换成 cpu 执行指令。

它是程序的一种低级表示,可以运行于 Java 虚拟机上。将程序抽象成字节码可以保证 Java 程序在各种设备上的运行。计算机里面的很多事情问题都可以通过增加一个中间层来解决,很显然,字节码+JVM 就是这么一个中间层,解决跨平台的问题。


Java bytecode 由单字节(byte)的指令组成,理论上最左支持 256 个操作码(opcode)。实际上 Java 只是用了 200 个左右的操作码,还有一些操作码则保留给调试操作。


根据指令的性质,主要分为四个大类:

  1. 栈操作指令,包括与局部变量交互的指令

  2. Java 虚拟机(JVM)是一个基于栈的计算机。所有的计算都发生在栈上。

  3. 程序流程控制指令

  4. 程序的流程控制指令,比如,for,if,函数调用

  5. 对象操作指令,包括方法调用指令

  6. java 是一个面向对象的语言,创建一个对象,调用对象的方法。

  7. 算数运算以及类型转换指令。


1.2 查看字节码

  1. 代码


  1. 查看字节码

  • javac xxx.java 生成 class 文件

  • javap -c xxx.class 查看字节码

实际上,class 文件里面保存的都是字节码,0 到 255 的数字,上图展示的是助记符,方便记忆和阅读用的。aload_0,return 等都有自己对应的操作码的。

  • javap -c -verbose xxx.class 查看更信息的字节码信息

上图中,有 jdk 的版本号,类的属性(public),还有常量表。代码行号表(调试的时候可以看到指令对于的行号)。


  1. 执行流程



执行的时候,从常量表中获取到常量值,在放到程序栈(变量表)中尽心计算。


2. 类加载

2.1 类的生命周期

  1. 加载(Loading):找 class 文件,并读入程序内存中

  2. 通过类名 com.xxx.Class 从各种 classpath 目录里面找到对应类,也可以自定义类加载器,从网络上加载类或 jar 包。

  3. 验证(Verification): 验证格式,依赖

  4. 验证格式是否正确。版本号。

  5. 类之间的相互引用关系。

  6. 准备(Preparation): 静态字段,方法表

  7. 抽取类里面的静态字段

  8. 抽取类里面的方法。

  9. 搭建类的结构(骨架)

  10. 解析(Resolution): 符号解析为引用

  11. 把各种符号替换成引用。

  12. 初始化(Initialization): 构造器,静态变量赋值,静态代码

  13. 静态变量赋值

  14. 静态代码执行

  15. 然后这个类就可以创建实例了。可以被使用了。

  16. 使用(Using): 创建类实例,并使用

  17. 卸载(Unloading): 清除类的信息

2.2 什么时候会加载类,会初始化类

  1. 当虚拟机启动时,初始化用户指定的主类。(main 方法所在的类)

  2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是 new 一个类的时候要初始化。(创建类的实例,那肯定需要类被加载了。)

  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类。

  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类。

  5. 子类的初始化会触发父类的初始化。

  6. 如果一个接口定义了 default 方法,那么直接实现或间接实现该接口的类的初始化,会触发该接口的初始化。

  7. 使用反射 API 对某个类进行反射调用时,初始化这个类,反射调用要么是已经有实例了,要么是静态方法,都需要初始化。

  8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。


2.3 什么时候会加载类,不会初始化类

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。但子类肯定是被加载了的。

  2. 定义对象数组,不会触发类的初始化。数组只是一个声明,实际上还没有创建对象呢。

  3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接一用定义常量的类,不会触发定义常量所在的类。比如:java 字符串字面量"xxxx"其实就是一个 String 常量了。但是并不会触发 String 类的初始化。

  4. 通过类名获取 Class 对象,不会触发类的初始化。Hello.Class 不会让 Hello 类初始化。

  5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,不会触发类初始化,其实这个参数是告诉虚拟机,是否需要对类进行初始化。Class.forName("jvm.Hello")默认会初始化 Hello 类。

  6. 通过 ClassLoader 默认的 loadClass 方法,不会触发初始化动作(类加载了,但是不初始化)


所谓加载类,不初始化类指的就是,会从 class 文件里面加载类的字节码,但是并不会执行静态字段的赋值,静态代码块的执行。


2.4 类加载器

  1. 启动类加载器(BootstrapClassLoader)

  2. 加载 JVM 启动的核心系统类

  3. 扩展类加载器(ExtClassLoader)

  4. 扩展的类,也是在 jdk 里面自带的。

  5. 应用类加载器(AppClassLoader)

  6. 加载程序员写的代码,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 里面的所有对象,以及对象个数,使用的字节数。如果某个对象特别多。可能就是内存泄漏了。

  • jmap -histo pid


  • jmap -heap 14068 查看堆内存信息


4.1.4 jstack

  • jstack pid:查看 jvm 所有线程的栈。


4.1.5 jcmd

  • jcmd 是一个比较综合的命令行工具。和上面的那些都是职责单一的。

  • jcmd 14068 help :查看指定进程,支持哪些工具。


  • jcmd pid Thread.print: 查看 jvm 的所有线程栈。


4.1.6 jrunscript/jjs

  • 执行 js 脚本命令。


5. JDK 内置图形化工具

说明:功能其实和命令行工具差不多,只是有窗口界面,比较方便。

5.1 jconsole

图形化显示 JVM 内存,线程,cpu 的使用情况。


5.2 jvisualvm

抽样统计功能,比 jconsole 更好用一点。更强大一点。可以查看一段时间(单位时间)系统的状态。


5.3 jmc

死锁都能检测出来。


用户头像

还未添加个人签名 2018.11.15 加入

还未添加个人简介

评论

发布
暂无评论
深入java week1-01 字节码、内存、GC、调试工具