JVM 从入门到精通(调优实战)

一、字节码篇

1、jvm概述

1.1JRE、JDK和JVM的关系

JRE(Java Runtime Environment, Java运行环境)是Java平台,所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。

JDK(Java Development Kit,Java开发工具包)是用来编译、调试Java程序的开发工具包。包括Java工具(javac/java/jdb等)和Java基础的类库(java API )。

JVM(Java Virtual Machine, Java虚拟机)是JRE的一部分。JVM主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。Java语言是跨平台运行的,不同的操作系统会有不同的JVM映射规则,使之与操作系统无关,完成跨平台性。

1.2Oracle与OpenJDK之间的主要区别

1. Oracle JDK版本将每三年发布一次LTS版本,而OpenJDK版本每三个月发布一次。

2. Oracle JDK将更多地关注稳定性,它重视更多的企业级用户,而OpenJDK经常发布以支持其他性能,这可能会导致不稳定。

3. Oracle JDK支持长期发布的更改,而Open JDK仅支持计划和完成下一个发行版。

4. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 使用Oracle平台时会产生一些许可影响。如Oracle 宣布的那样,在没有商业许可的情况下,在2019年1月之后发布的Oracle Java SE 8的公开更新将无法用于商业,商业或生产用途。但是,OpenJDK是完全开源的,可以自由使用。

5. Oracle JDK的构建过程基于OpenJDK,因此OpenJDK与Oracle JDK之间没有技术差异。

6. 顶级公司正在使用Oracle JDK,例如Android Studio,Minecraft和IntelliJ IDEA开发工具,其中Open JDK不太受欢迎。

7. Oracle JDK具有Flight Recorder,Java Mission Control和Application Class-Data Sharing功能,Open JDK具有Font Renderer功能,这是OpenJDK与Oracle JDK之间的显着差异。

8. Oracle JDK具有良好的GC选项和更好的渲染器,而OpenJDK具有更少的GC选项,并且由于其包含自己的渲染器的分布,因此具有较慢的图形渲染器选项。

9. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能。

10. 与OpenJDK相比,Oracle JDK的开源社区较少,OpenJDK社区用户的表现优于Oracle JDK发布的功能,以提高性能。

11. 如果使用Oracle JDK会产生许可影响,而OpenJDK没有这样的问题,并且可以以任何方式使用,以满足完全开源和免费使用。

12. Oracle JDK在运行JDK时不会产生任何问题,而OpenJDK在为某些用户运行JDK时会产生一些问题。

13. 根据使用方的使用和许可协议,现有应用程序可以从Oracle JDK迁移到Open JDK,反之亦然。

14. Oracle JDK将从其10.0.X版本将收费,用户必须付费或必须依赖OpenJDK才能使用其免费版本。

15. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本。

16. Oracle JDK以前的1.0版以前的版本是由Sun开发的,后来被Oracle收购并为其他版本维护,而OpenJDK最初只基于Java SDK或JDK版本7。

17. Oracle JDK发布时大多数功能都是开源的,其中一些功能免于开源,并且根据Sun的许可授权,而OpenJDK发布了所有功能,如开源和免费。

18. Oracle JDK完全由Oracle公司开发,而Open JDK项目由IBM,Apple,SAP AG,Redhat等顶级公司加入和合作。

JVM架构图

这个架构可以分成三层看:

最上层:javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域。

中间层:称为Runtime Data Area,主要是在Java代码运行时用于存放数据的,从左至右为方法区(永久代、元数据区)、堆(共享,GC回收对象区域)、栈、程序计数器、寄存器、本地方法栈(私有)。

最下层:解释器、JIT(just in time)编译器和 GC(Garbage Collection,垃圾回收器)

2.字节码文件概述

2.1如何解读class文件

如何解读供虚拟机解释执行的二进制字节码?

方式一:一个一个二进制的看。这里用到的是Notepad++,需要安装一个HEX-Editor插件,或者使用Binary Viewer

方式二:使用javap指令:jdk自带的反解析工具

方式三:使用IDEA插件:jclasslib 或jclasslib bytecode viewer客户端工具。(可视化更好)

2.2哪些类型对应有Class的对象?

(1)class:

外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类

(2)interface:接口

(3)[]:数组

(4)enum:枚举

(5)annotation:注解@interface

(6)primitive type:基本数据类型

(7)void

@Test

public void test(){

Class c1 = Object.class;

Class c2 = Comparable.class;

Class c3 = String[].class;

Class c4 = int[][].class;

Class c5 = ElementType.class;

Class c6 = Override.class;

Class c7 = int.class;

Class c8 = void.class;

Class c9 = Class.class;

int[] a = new int[10];

int[] b = new int[100];

Class c10 = a.getClass();

Class c11 = b.getClass();

// 只要元素类型与维度一样,就是同一个Class

System.out.println(c10 == c11);

}

2.3class文件的基作用

常量池:存放所有常量

常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。

常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。

常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。

常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。

2.4为什么需要常量池计数器?

constant_pool_count (常量池计数器)

由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。

常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项。

Demo的值为:

其值为0x0016,掐指一算,也就是22。

需要注意的是,这实际上只有21项常量。索引为范围是1-21。为什么呢?

通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。

int[] arr = new int[10];

arr[0];

arr[1];

ar[10 - 1];

二、类的加载篇

Java 类加载过程?

按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:

其中:

①第一过程的加载(loading)也称为装载

②验证、准备、解析 3 个部分统称为链接(Linking)

从程序中类的使用过程看:

类的装载

过程一:类的装载

所谓装载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。

装载完成的操作

装载阶段,简言之,查找并加载类的二进制数据,生成Class的实例。

在加载类时,Java虚拟机必须完成以下3件事情:

通过类的全名,获取类的二进制数据流。

解析类的二进制数据流为方法区内的数据结构(Java类模型)

创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

过程一:类模板对象

所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。

反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。

类模型的位置

加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后:元空间)。

二进制流的获取方式

对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合JVM规范即可)

虚拟机可能通过文件系统读入一个class后缀的文件(最常见)

读入jar、zip等归档数据包,提取类文件。

事先存放在数据库中的类的二进制数据

使用类似于HTTP之类的协议通过网络进行加载

在运行时生成一段Class的二进制信息等

在获取到类的二进制信息后,Java虚拟机就会处理这些数据,并最终转为一个java.lang.Class的实例。

如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。

Class实例的位置

类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。(instanceKlass -->mirror :Class的实例)

图示

外部可以通过访问代表Order类的Class对象来获取Order的类数据结构。

4.再说明

Class类的构造方法是私有的,只有JVM能够创建。

java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过Class类提供的接口,可以获得目标类所关联的.class文件中具体的数据结构:方法、字段等信息。

数组类的加载

数组类的加载

创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称A)的过程:

1. 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型;

2. JVM使用指定的元素类型和数组维度来创建新的数组类。

3. 如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public。

int[] arr

String[] arr

Object[] arr