学习路线推荐,如何啃下JVM这座大山(完结篇)

293次阅读  |  发布于3年以前

这是关于JVM的第六篇文章,前面写了五篇关于JVM的文章,都是一层一层带着大家来深入的认识JVM,关于JVM的基本用到的知识都讲解过了。

那么这一篇文章将作为JVM的最后一篇,有总结,也有补充还没有提及到的知识点。

首先,关于JVM的开门篇,关于如何阅读深入JVM虚拟机第三版的文章:[如何三天啃下《深入JVM虚拟机第三版》],主要是分享个人阅读这本神书的经验,在文章里面划重点,吸收到我们自己所需要的知识点。

然后,是很久之前写的第二篇关于JVM的运行时数据区以及GC的算法篇:[还在学JVM?我都帮你总结好了(附脑图)] 。

这篇是是作为理论的重点部分,因为这片关于JVM的运行时数据区的各个部分是干嘛的,以及重点JVM堆的分代理论和GC的基本回收算法,都是为后面的Java堆的调优实战做铺垫。

然后是第三篇也是调优的重中之重,主要聊的就是GC:[你是不是垃圾,心里没点数吗?] ,如何判断一个对象是否存活、以及常见的GC的种类,常见GC年轻代和老年代的搭配,各种GC的原理和特点、以及适用的场景,在文章中都有提及到。

有了第二篇运行时数据区和第三篇GC的理论基础后,然后第四篇的JVM调优实战篇一:[JVM调优实战篇一] ,这一篇主要是围绕着JVM调优实战讲解的工具篇,包括线上的Arthas工具、GUI工具(Visual VM)、内存分析工具(mat)以及linux的原始命令jps、jstack、jstat、jmap、jhat等命令的讲解和使用

熟悉了JVM调优的工具后,第五篇就是就是最后实战调优的场景、案例、经典排查OOM、磁盘不足排查、CPU飙高的排查、死锁的排查、调优的目的和理论已经调优案例场景解析:[JVM性能调优实战篇(二)]

其中,第五篇也是写了我最久的一篇,也是干货非常多的一篇,也是目的性最强的一篇,学习JVM就是为了调优,同时也收获不少的新读者的关注。

所以,按照上面帮你们排好的顺序,一篇一篇的往下读,应该对你的调优的技术和理论的深入认识应该是有所帮助的。

然后,自己有时间的话,也可以自己深入的啃啃《深入JVM虚拟机第三版》,关于JVM的其他书籍,我推荐:《Java虚拟机规范》、《垃圾回收算法手册:自动内存管理的艺术》、《Virtual Machines: Versatile Platforms for System and Processes》 这几本好书,有时间和能力的都可以去看一看。

最后,这是最后一篇也是第六篇,对于JVM虚拟机我们所需要的部分,还有一块就是类加载子系统还没有讲解,所以为了文章的完整性,这一补充JVM的类加载子系统,这一篇还是主要是理论。

类加载子系统概述

首先,我们来聊一聊JVM的类加载子系统,我们知道我们的代码敲完后是.java类,然后经过编译就会变成.class类型的字节码文件。

这些.class类型的字节码文件经过类加载系统加载到JVM的内存中来供我们使用,这些文件我们也成为了元数据。

下面我画了一张图来大概一个类被类加载系统加载的过程,供大家理解:

就这样一个java类经过上面层层的过程来到了我们的JVM的虚拟机中,首先在加载过程的.class文件可以来源终于下面几个方面:

当由我们的类加载子系统完成了类加载后,这部分信息(包括类信息、常量、静态变量、方法信息等)就会存在方法区的内存中(jdk 7以及以前,jdk 8及以上移动到元空间,本地内存中),然后由JVM的执行引擎来执行。

在这个过程类加载的过程就好像扮演着中间人的角色,目的为的是JVM的执行引擎能够执行这些类:.class -> JVM -> 元数据模板 -> 实例对象

那么,JVM在加载类的时候,这个过程的主角类加载器,又是怎么工作的呢?下面我们来聊一聊类加载器。

类加载器

在JVM中经典的类加载器分为如下三层:

  1. 启动类加载器(BootStrap ClassLoader):该加载器是由C++实现,加载<JAVA_HOME>\lib下的文件,这类加载起不会被Java程序所直接使用,该类加载器一般加载包名为java、javax、sun等开头的类
  2. 扩展类加载器(Extension ClassLoader):扩展类加载器是加载<JAVA_HOME>\lib\ext目录下的资源,它可以用来扩展Java SE的功能,如果用户创建的JAR包放在此目录下,就会被扩展类加载器加载
  3. 应用类加载器(Application ClassLoader):它负责加载用户类路径(ClassPath)上的所有类库,开发者同样可以直接在代码中使用这个类加载器。

除了上面的经典三层还有一个就是用户自定义类加载器(User ClassLoader),它可以在程序中加载自己需要的类,所以完整的JVM类加载器如下图所示:

他们的关系并不是继承的关系,而是通常以组合的关系来复用父类加载器的代码。

类加载过程

我们了解完各种类加载器后,接下来详细的了解类加载的过程,一个完整的类加载过程主要包括一下几个阶段:加载-> 验证->准备->解析->初始化

加载阶段是通过类的全类名,然后通过类加载器将class文件的二进制字节流转化为运行时数据区的方法区中。

并且,会在内存中生成一个java.lang.Class对象,作为这个类的各种数据的访问入口。

验证阶段既是验证字节码class文件中的字节码是否服务规范,保证里面的字节码不会对JVM自身造成伤害。

准备阶段既是为类中的类变量(static修饰的变量,但没有被final修饰一起修饰)分配内存以及初始化类变量的零值,这里并不包括实例变量,实例变量是在对象一起分配在Java堆中。

所谓的零值也就是数据的默认初始值,比如int为0,boolean默认为false,float默认为0.0f,引用类型的默认为null。

解析阶段的作用是将虚拟机内的常量池的符号引用替换为直接引用的过程。所谓的符号引用就是可以是任何形式的字面量,只要能够定位到目标即可;而直接引用可以是指向目标的指针、相对偏移量间接定位到目标的句柄。

最后是初始化,初始化是类加载的最后一个阶段,也是在这个阶段,Java虚拟机才真正开始执行类中的Java程序代码。

上面说到在准备阶段变量已经初始化一次零值了,那么在这一阶段才会将变量初始化为程序代码中主观设置的值。

对于这一部分,我之前也写过一篇详细的,所以这里做一个大概的介绍,详细的可以参考这一篇文章:[面试官:你知道java类是怎么跑起来的吗?问的我一脸懵]

双亲委派原则

双亲委派原则是各种加载器之前的一种工作方式,它目的是为了实现更加高效的进行类的加载。

当一个类加载器收到加载类的请求时,不会自己去尝试先加载该类,而是把该加载请求委托给父类,若是没有父类就是直接找顶层类加载器,若是有父类,并且父类还有父类加载器,依次向上委托,直到上面的所有父类都无法完成加载是,才会自己去加载,若是加载失败就会抛出异常。

它的好处就是加载一个类时,不用重复加载,当父类已经加载了,就不用加载第二年份,保证内存中只有一份。

我们来看看双亲委派实现的源代码,它的源代码主要实现是java.lang.ClassLoader的loadClass() 方法中:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 // 检查该类是否已经被加载过了
 Class c = findLoadedClass(name);
 // c为null没有被加载过,则使用双亲委派原则进行类加载
 if (c == null) {
  try {
      // 存在父类加载器
   if (parent != null) {
    c = parent.loadClass(name, false);
   } else {
       // 不存在服务加载器,则直接使用顶层类加载器进行加载
    c = findBootstrapClassOrNull(name);
   }
  } catch (ClassNotFoundException e) {
   // 父类加载抛出异常,说明父类无法完成加载
  }
  if (c == null) {
   // 父类无法完成加载,则尝试自己去完成加载动作
   c = findClass(name);
  }
 }
 if (resolve) {
  resolveClass(c);
 }
 return c;
}

好了,这一篇文章是的主要内容也讲完了,文字比较短,没有像以前那样基本都是万字,因为主要的内容都已经讲完了,这一篇还是比较简单的,更加倾向于对以前写的JVM的文章的总结。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8