类加载机制

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

2.1 类加载时机

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

类加载机制+JVM调优实战+代码优化_类加载器

上图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持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虚拟机规范》中使用了一个非常强烈的限定语——“有且只有”,这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。

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

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

加载

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

通过一个类的全限定名来获取定义此类的二进制字节流。

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

在内存中生成一个代表这个类的java.lang. Class对象,作为方法区这个类的各种数据的访问入口。

验证

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

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

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

准备

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

解析

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

初始化

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

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

2.3 类加载器

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

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

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

双亲委派模型

从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$AppClassLoader 实现。 getSystemClassLoader() 方法返回的就是这个类加载器,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

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

 
webp
image

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

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

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

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

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

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

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;
}
03 JVM调优实战 3.1 JVM运行参数

在jvm中有很多的参数可以进行设置,这样可以让jvm在各种环境中都能够高效的运行。绝大部分的参数保持默认即可。

三种参数类型

  • 标准参数

  • -help

  • -version

  • -X参数(非标准参数)

  • -Xint

  • -Xcomp

  • XX参数(使用率较高)

  • -XX:newSize

  • -XX:+UseSerialGC

-X参数

jvm的-X参数是非标准参数,在不同版本的jvm中,参数可能会有所不同,可以通过java -X查看非标准参数。

-XX参数

-XX参数也是非标准参数,主要用于jvm的调优和debug操作。

-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型:

  • boolean类型

  • 格式:-XX:[+-] 表示启用或禁用属性

  • 如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效

  • 非boolean类型

  • 格式:-XX:= 表示属性的值为

  • 如:-XX:NewRatio=4 表示新生代和老年代的比值为1:4

-Xms和-Xmx参数

-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。
适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑得更快。
示例:


[root@node01 test]# java -Xms512m -Xmx2048m TestJVM
itcast

jstat

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数]

查看class加载统计

F:\t>jstat -class 12076
Loaded  Bytes  Unloaded  Bytes     Time
 5962 10814.2        0     0.0       3.75

说明:
Loaded:加载class的数量
Bytes:所占用空间大小
Unloaded:未加载数量
Bytes:未加载占用空间
Time:时间

查看编译统计

F:\t>jstat -compiler 12076
Compiled Failed Invalid   Time   FailedType FailedMethod
   3115      0       0     3.43          0

说明:
Compiled:编译数量。
Failed:失败数量
Invalid:不可用数量
Time:时间
FailedType:失败类型
FailedMethod:失败的方法

垃圾回收统计

F:\t>jstat -gc 12076
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
  CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
#也可以指定打印的间隔和次数,每1秒中打印一次,共打印5次
F:\t>jstat -gc 12076 1000 5
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
  CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062
3584.0 6656.0 3412.1  0.0   180224.0 89915.4   61440.0     5332.1   27904.0 2626
7.3 3840.0 3420.8      6    0.036   1      0.026    0.062

说明:
S0C:第一个Survivor区的大小(KB)
S1C:第二个Survivor区的大小(KB)
S0U:第一个Survivor区的使用大小(KB)
S1U:第二个Survivor区的使用大小(KB)
EC:Eden区的大小(KB)
EU:Eden区的使用大小(KB)
OC:Old区大小(KB)
OU:Old使用大小(KB)
MC:方法区大小(KB)
MU:方法区使用大小(KB)
CCSC:压缩类空间大小(KB)
CCSU:压缩类空间使用大小(KB)
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

3.2 Jmap的使用以及内存溢出分析

前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容,如:内存使用情况的汇总、对内存溢出的定位与分析。

查看内存使用情况

[root@node01 ~]# jmap -heap 6219
Attaching to process ID 6219, please wait... 
Debugger attached successfully.
Server compiler detected.
JVM version is 25.141-b15
using thread-local object allocation.
Parallel GC with 2 thread(s)
Heap Configuration: #堆内存配置信息
MinHeapFreeRatio         = 0
MaxHeapFreeRatio         = 100
MaxHeapSize              = 488636416 (466.0MB)
NewSize                  = 10485760 (10.0MB)
MaxNewSize               = 162529280 (155.0MB)
OldSize                  = 20971520 (20.0MB)
NewRatio                 = 2
SurvivorRatio            = 8
MetaspaceSize            = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize         = 17592186044415 MB
G1HeapRegionSize         = 0 (0.0MB)
Heap Usage: # 堆内存的使用情况
PS Young Generation #年轻代
Eden Space:
capacity = 123731968 (118.0MB)
used     = 1384736 (1.320587158203125MB)
free     = 122347232 (116.67941284179688MB)
1.1191416594941737% used
From Space:
capacity = 9437184 (9.0MB)
used     = 0 (0.0MB)
free     = 9437184 (9.0MB)
0.0% used
To Space:
capacity = 9437184 (9.0MB)
used     = 0 (0.0MB)
free     = 9437184 (9.0MB)
0.0% used
PS Old Generation #年老代
capacity = 28311552 (27.0MB)
used     = 13698672 (13.064071655273438MB)
free     = 14612880 (13.935928344726562MB)
48.38545057508681% used
13648 interned Strings occupying 1866368 bytes.

查看内存中对象数量及大小

#查看所有对象,包括活跃以及非活跃的
jmap -histo <pid> | more
#查看活跃对象 
jmap -histo:live <pid> | more
[root@node01 ~]# jmap -histo:live 6219 | more
num     #instances         #bytes  class name
----------------------------------------------1:         37437        7914608  [C
2:         34916         837984  java.lang.String
3:           884         654848  [B
4:         17188         550016  java.util.HashMap$Node
5:          3674         424968  java.lang.Class
6:          6322         395512  [Ljava.lang.Object;
7:          3738         328944  java.lang.reflect.Method
8:          1028         208048  [Ljava.util.HashMap$Node;
9:          2247         144264  [I
10:          4305         137760  java.util.concurrent.ConcurrentHashMap$Node
11:          1270         109080  [Ljava.lang.String;
12:            64          84128  [Ljava.util.concurrent.ConcurrentHashMap$Node;
13:          1714          82272  java.util.HashMap
14:          3285          70072  [Ljava.lang.Class;
15:          2888          69312  java.util.ArrayList
16:          3983          63728  java.lang.Object
17:          1271          61008  org.apache.tomcat.util.digester.CallMethodRule
18:          1518          60720  java.util.LinkedHashMap$Entry
19:          1671          53472  com.sun.org.apache.xerces.internal.xni.QName
20:            88          50880  [Ljava.util.WeakHashMap$Entry;
21:           618          49440  java.lang.reflect.Constructor
22:          1545          49440  java.util.Hashtable$Entry
23:          1027          41080  java.util.TreeMap$Entry
24:           846          40608  org.apache.tomcat.util.modeler.AttributeInfo
25:           142          38032  [S
26:           946          37840  java.lang.ref.SoftReference
27:           226          36816  [[C
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
#对象说明
B  byte
C  char
D  double
F  float
I  int
J  long
Z  boolean
[  数组,如[I表示int[]
[L+类名 其他对象

将内存使用情况dump到文件中

#用法:
jmap -dump:format=b,file=dumpFileName <pid>
#示例
jmap -dump:format=b,file=/tmp/dump.dat 6219

 
24613101-5c5350fce88ed891.jpg
image

可以看到已经在/tmp下生成了dump.dat的文件。

通过jhat对dump文件进行分析

在上一小节中,我们将jvm的内存dump到文件中,这个文件是一个二进制的文件,不方便查看,这时我们可以借助于jhat工具进行查看。

#用法:
jhat -port <port> <file>
#示例:
[root@node01 tmp]# jhat -port 9999 /tmp/dump.dat 
Reading from /tmp/dump.dat...
Dump file created Mon Sep 10 01:04:21 CST 2018
Snapshot read, resolving...
Resolving 204094 objects...
Chasing references, expect 40 dots........................................
Eliminating duplicate references........................................
Snapshot resolved.
Started HTTP server on port 9999
Server is ready.

打开浏览器进行访问:http://192.168.40.133:9999/

 
webp
image

在最后由OQL查询功能

 
webp
image
 
webp
image
3.3 Jmp使用以及内存溢出分析

使用MAT对内存溢出的定位与分析

内存溢出在实际的生产环境中经常会遇到,比如,不断地将数据写入到一个集合中,出现了死循环,读取超大的文件等等,都可能会造成内存溢出。

如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正常还是非正常情况,如果是正常的需求,就应该考虑加大内存的设置,如果是非正常需求,那么就要对代码进行修改,修复这个bug。首先,我们得先学会如何定位问题,然后再进行分析。如何定位问题呢,我们需要借助于jmap与MAT工具进行定位分析。

接下来,我们模拟内存溢出的场景。

模拟内存溢出

编写代码,向List集合中添加100万个字符串,每个字符串由1000个UUID组成。如果程序能够正常执行,最后打印ok。

public class TestJvmOutOfMemory {
public static void main(String[] args) { 
    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 10000000; i++) {
            String str = "";
            for (int j = 0; j < 1000; j++) {
            str += UUID.randomUUID().toString();
            }
             list.add(str);
        }
    System.out.println("ok");
    }
}

为了演示效果,我们将设置执行的参数

#参数如下:
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

运行测试

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid5348.hprof ...
Heap dump file created [8137186 bytes in 0.032 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at
java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.itcast.jvm.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:14)
Process finished with exit code 1

可以看到,当发生内存溢出时,会dump文件到java_pid5348.hprof。

 
webp
image

导入到MA T工具中进行分析

 
webp
image

可以看到,有91.03%的内存由Object[]数组占有,所以比较可疑。
分析:这个可疑是正确的,因为已经有超过90%的内存都被它占有,这是非常有可能出现内存溢出的。

 
webp
image

可以看到集合中存储了大量的uuid字符串

3.4 Jsatck的使用

有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等,我们该如何分析呢?

由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要看下jvm的内部线程的执行情况,然后再进行分析查找出原因。

这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进行快照,并且打印出来:

#用法:jstack <pid>
[root@node01 bin]# jstack 2203
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.141-b15 mixed mode):
"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007fabb4001000 nid=0x906 waiting on
condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"http-bio-8080-exec-5" #23 daemon prio=5 os_prio=0 tid=0x00007fabb057c000 nid=0x8e1
waiting on condition [0x00007fabd05b8000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
"http-bio-8080-exec-4" #22 daemon prio=5 os_prio=0 tid=0x00007fab9c113800 nid=0x8e0
waiting on condition [0x00007fabd06b9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000f8508360> (a
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueue
dSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads