草庐IT

一.从零开始JVM实战高手

墨家巨子@俏如来 2023-12-12 原文

从零开始👉JVM实战高手,建议收藏,加源妹儿微信 “ ymsdsss”领取整套JVM实战资料和精品视频,关注微信公众号 “疯狂Java程序猿” ,后续会推出JVM,Mybatis,SpringBoot,Redis等等一些列从入门到源码剖析的精品视频和文章。你的鼓励是我最大的动力。

作者:源码时代-Raymon老师

前言

目前市面上已有太多的JVM相关教程和书籍,但是大部分偏理论,比较枯燥难懂,少有结合实际业务开发,站在项目开发的视角下去分析和讲解相关经验的教程;而本套教程会从零开始带着大家一步一步深入了解JVM底层原理,以及结合一些开发中的典型生产环境问题来进行实战剖析,并且几乎采用一步一图的方式进行讲解。

通过核心理论和实战案例的结合,希望能对大家对JVM的理解和应用更上一层楼。

为什么要学习JVM?

  1. 面试需要(中大厂必考核的一项技能),说说你的项目是如何处理JVM GC、OOM等问题…

  2. 深入的理解Java这门语言。(万丈高楼地基最重要),Java的 boolean类型在JVM中是如何表现?类路径和类名是否唯一确定一个类?

  3. 更好的解决线上排查问题(更好的解决生产线问题), 线上系统跑着跑着突然卡死,FullGC非常频繁,各种GC日志一大堆,无从下手分析,何谈优化,遇到OOM异常直接躺平,求救或跑路

  4. 走向高级程序员和架构师的必经之路(底层逻辑),知其然更要知其所以然,透过现象看本质。只有掌握了底层逻辑,只有探寻到万变中的不变,才能动态地、持续地看清事物的本质;可以通过不变的底层逻辑,推演出顺应时势的方法论。

学习路线图

我们的学习路线图就通过从一个类的加载开始,来学习Java是如何将代码运行起来的,由点到面的方式,一步一步深入理解JVM的整体运行机制。

  • 理论相关的必备知识,通过大量绘图让小白也能看懂
  • 分享JVM实践经验,解决JVM生产环境的参数优化,GC问题,以及OOM问题

整体内容划分:

  1. Java代码运行机制+类加载机制+JVM内存结构深入讲解+案例实战
  2. 垃圾回收器与算法+分配策略+案例剖析
  3. JVM过程剖析+性能监控+故障处理+优化实战
  4. OOM内存溢出+场景分析&解决方案

你的Java代码是如何运行的?

我们平时写的Java代码,到底是如何运行起来的?

我们都知道,我们平时创建的一个一个类,在本地磁盘中的文件名后缀就是 .java,比如User.java 、Product.java ,这也叫做源代码文件。这些源代码文件必须经历我们的javac工具进行编译后生成 .class 的字节码文件才能被运行。

那接着我们就要继续思考了:那这些 .class 字节码文件又是如何运行起来的?(这里我们可以借助于DOS窗口执行 java 命令进行启动)

> javac User.java
> java User

输出: hello World…

此时一旦采用 java 命令,实际上就是启动了一个JVM进程,由JVM来负责加载这些字节码文件到内存进行执行。

而将class字节码文件加载到虚拟机的内存,这个过程称为类加载,其中涉及到 【类加载机制】和【类加载器】的概念。

当字节码文件被类加载器加载进入到JVM内存中后,会通过JVM的执行引擎来执行我们内存中对应的类,比如类中的main方法,就会先被执行,而main方法中如果还涉及到其他的对象引用,类加载又会开始加载对应的字节码文件到内存,再由JVM进行调用执行。(如下图)

当我们通过Eclipse或IDEA工具开发完一个完整的项目后,一般都会将项目整体打成一个jar 包或者war包,然后部署到对应线上服务器进行运行;其实就是将我们写的所有java代码编译成对应的字节码文件后,加上一些项目的资源文件一起打包,部署进服务器比如Tomcat,当我们通过 java -jar之类的命令就可以运行和执行我们写好的代码。

ok,通过以上的分析,我们先整体对java代码的运行流程做了一个初步的介绍,接下来再深入分析类加载过程又是如何执行的,一步一步深入学习。

请说下JVM的类加载机制

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为链接(Linking),这7个阶段的发生顺序如图:

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

加载Loading阶段

加载Loading阶段就是JVM第一次去加载和读取对应位置上的文件,上述的整个类加载过程其实都是类加载器(后续讲)在完成。

相当于正式建立了IO通道,在这条通道上,我们需要做上述的一系列事情,比如验证、准备、解析等。

思考:JVM在什么情况下会加载一个类呢?

通过以上的类加载流程,我们可以得知第一个环节就是加载一个类,因此当我们在IDEA中或直接运行某一个类的时候(比如First.java),其实是启动了JVM进程,然后JVM会通过类加载器将这个类的字节码(First.class)加载到内存,然后调用main方法开始执行。如果main方法中的代码是:

public class First {
    public static void main(String[] args) {
        //创建Second这个类的实例
        Second second = new Second();
    }
}

JVM这个时候会先检查内存中是否有该类的对象,如果没有会触发类加载器加载磁盘中的Second.class字节码到内存中,如下图:

如何验证类是否已经加载?可以通过如下方式演示:

  1. 在D盘创建cn/itsource/load/ 三个文件夹
  2. 将LoadTest.java文件拷贝到该文件夹
  3. 通过cmd编译
  4. 运行并查看:java -XX:+TraceClassLoading -cp . cn.itsource.load.LoadTest

验证阶段

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

主要包括四种验证,文件格式验证(魔数CAFEBABE),元数据验证,字节码验证,符号引用验证。

简单说就是我们的【.class】文件是否符合JVM规范,是否有被篡改,否则JVM是没法执行该字节码文件的。

每个符合规范的Java文件二进制开头应该是对应的魔数CAFEBABE

每个类在被加载到JVM内存前都会还行验证这个过程:

准备阶段

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

注意事项:static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾,(换句话理解就是:1.7之前存储于方法区,1.7之后存储于堆内存中)

static 变量分配空间和赋值是两个步骤,分配空间(内存分配)在准备阶段完成,赋值在初始化阶段完成

关于准备阶段,还有两个容易产生混淆的概念需要着重强调,首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value = 123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把 value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。

如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

上面提到在“通常情况”下初始值是零值,那言外之意是相对的会有某些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定 的初始值,假设上面类变量value的定义修改为:

public static final int value = 123;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置 将value赋值为123。

如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

Java代码在进行Javac编译的时候,在虚拟机加载Class 文件的时候进行动态连接。在Class文件中不会保存各个方法、字段最终 在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号 引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引 用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,

    但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。

  • 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚 拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

这三个阶段中,大家最应该关心的核心是:准备阶段,这个阶段是给加载进来的类进行空间的分配,以及static静态变量的空间分配,并且给与初始化值。

类的初始化

这里安利一款查看字节码的IDEA插件:IDEA中安装插件:<查看对应字节码文件二进制>

通过准备阶段类变量已经赋过一次系统要求的初始零值,而初始化阶段就是在给类变量进行赋值操作。

初始化阶段会执行类构造器方法<clinit>() ,该方法不同于类的构造器(是虚拟机视角下的<init>());该方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

通过代码观察:我们second类中的静态变量b是在 <clinit>方法中才被真正初始化的–>对应了静态变量的赋值操作

如果我们在second类中再添加一个静态代码块,去修改b的值为11,可以验证静态代码块的操作也是在<clinit>中执行的:

注意1: 编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

public class InitDemo {
    static {
        i = 20;// 给变量赋值可以正常编译通过
        //System.out.println(i);// 这句编译器会提示“非法向前引用”
    }
    public static int i = 10;
}

注意2:父类初始化先执行<clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的 clinit方法执行前,父类的clinit方法已经执行完毕。因此在Java虚拟机中第一个被执行的 clinit 方法的类型肯定是java.lang.Object。

注意3:线程同步 : Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行完毕()方法。如果在一个类的()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

public class ThreadInitTest {
    public static void main(String[] args) {
        Runnable script = new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread() + "start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}

class DeadLoopClass {
    static {
        if (true) {
            System.out.println(Thread.currentThread() + "init DeadLoopClass");
            while (true) {
            }
        }
    }
}

其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒后则不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会被初始化一次。

总结一句话:一个类如果已经被一个线程加载到内存中,也就代表加载过程中的这些阶段(加载-链接)都已经执行完毕了,那么后续如果有其他线程想要访问对应的字节码对象直接访问即可,不需要再次去加载这个类的字节码文件了。

对应了我们JavaSE中的语法问题:一个类中的静态修饰的内容会随着类的加载而加载,只会被加载一次《这个说话仅仅限于同一个类加载器》

总结

  • 类加载阶段:建立通道,把class文件加载到JVM内存中
  • 验证阶段:就是校验字节码文件是否符合JVM的规范
  • 准备阶段:就是分配内存空间,给类变量设置初始化值
  • 解析阶段:将符号引用(占位符)转化为直接引用(真实内存地址值)
  • 初始化阶段:给类变量进行赋值的操作

文章对应的配套视频请关注微信公众号:疯狂Java程序猿,

有关一.从零开始JVM实战高手的更多相关文章

  1. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  2. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  3. ruby-on-rails - Ruby/Rails 中的夏令时开始和结束日期 - 2

    我正在开发一个Rails应用程序,我需要在其中找到给定特定偏移量或时区的夏令时开始和结束日期。我基本上在我的数据库中保存了从用户浏览器接收到的时区偏移量(“+3”,“-5”),我想在它出现时修改它由于夏令时的变化。我知道Time实例变量有dst?和isdst方法,如果存储在它们中的日期在夏令时与否。>Time.new.isdst=>true但是使用它来查找夏令时的开始和结束日期会占用太多资源,而且我还必须为我拥有的每个时区偏移量执行此操作。我想知道更好的方法。 最佳答案 好的,基于你所说的和@dhouty'sanswer:您希望能够

  4. ruby-on-rails - phusion passenger 和 ruby​​ 1.9.1 已经开始工作了吗? - 2

    我有一台生产机器和一台开发机器,都运行ubuntu8.10并且都运行最新的phusionpassenger。当我在osx上的本地开发机器上使用ruby​​1.9.1时,我想知道外面的人是否已经在使用带有ruby​​1.9.1甚至1.9.2的phusionpassenger?如果是这样,请告诉我们您的设置!此外,有没有办法在apache上使用phusionpassenger同时运行ruby​​1.8.7(ree)和1.9.1?感谢您的指点,我在任何地方都找不到任何提示... 最佳答案 是的,从某些2.2.x版本开始就正式支持它,我不记

  5. ruby - Rails 3 - 我可以将开始日期设置为 date_select 方法吗? - 2

    date_select方法只能设置:start_year,但我想设置开始日期(例如3个月前的日期)(但没有这样的选项)。那么,我可以将开始日期设置为date_select方法吗?或者,要制作这样的选择框,我应该使用select_tag和options_for_select吗?或者,有什么解决办法吗?谢谢, 最佳答案 有可能……例如:start_year–设置年份选择的开始年份。默认为Time.now.year-5参见thisresource. 关于ruby-Rails3-我可以将开始日期

  6. ruby - 从特定索引开始迭代数组 - 2

    我想从特定索引开始遍历数组。我该怎么做?myj.eachdo|temp|...end 最佳答案 执行以下操作:your_array[your_index..-1].eachdo|temp|###end 关于ruby-从特定索引开始迭代数组,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/44151758/

  7. ruby - Heroku - 如何开始工作人员(延迟工作)? - 2

    我有一些使用delayed_job的小程序。在我的本地主机上一切正常,但是当我将我的应用程序部署到Heroku并单击应该由delayed_job执行的链接时,没有任何反应,“任务”只是保存到表delayed_job中。Inthisarticleonherokublog写入时,执行delayed_job表中的任务,当运行此命令时rakejobs:work。但是我怎样才能运行这个命令呢?命令应该放在哪里?在代码中,还是从终端控制台? 最佳答案 如果您正在运行Cedar堆栈,请从终端控制台运行以下命令:herokurunrakejobs:

  8. node.js - 从未编写过任何自动化测试,我应该如何开始行为驱动开发? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。多年来,我一直在使用多种语言进行编程,并且认为自己总体上相当擅长。但是,我从未编写过任何自动化测试:没有单元测试,没有TDD,没有BDD,什么都没有。我已经尝试开始为我的项目编写适当的测试套件。我可以看到在进行任何更改后能够自动测试项目中所有代码的理论值(value)。我可以看到像RSpec和Mocha这样的测试框架应该如何使设置和运行所述测试变得相当容易

  9. ruby - 是否有任何命令可以使用 vim 转到 Ruby block 的末尾(或开始) - 2

    有没有办法使用vim结束Rubyblock?例如moduleSomeModule#defsome_methodendend我想用一个命令从光标所在的位置移动到block的末尾,这可能吗?我读过thisdocumentation,但它似乎不适用于.rb文件,我在某些地方读到它只适用于C(虽然还没有尝试过)。提前致谢。 最佳答案 rubyforge好像有官方包对此有一些支持:TheRubyftpluginnowincludesRubyspecificimplementationsforthe[[,]],[],][,[m,]m,[M,an

  10. ruby-on-rails - 从 Ruby 1.9.x、Rails 3 和/或 Merb 开始 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭7年前。Improvethisquestion我需要认真阅读Ruby1.9.1和即将推出的Rails3/Merb的变化。人们可以推荐任何文章阅读吗?并不是真的在寻找一个答案,只是在寻找人们正在使用的资源汇编,以跟上即将发生的事情和当前存在的事情,所以如果你路过,请告诉我你在看什么。谢谢!

随机推荐