1_JVM堆栈执行.md

jvm堆栈&性能分析

课程目标

  • 解JVM架构及原理,要知其然并知其所以然,学会线上调优

  • 掌握不同版本JDK的垃圾回收器是如何工作的

  • 学会分析解决线上问题!

  • Java类加载机制以及双亲委派模型

  • Java类和对象的生命周期探究

  • 深入理解Jvm和Jvm基本参数调优

  • Tomcat 性能调优及性能工具使用

  • 几代垃圾回收算法及垃圾回收器原理解读及特性对比详解

  • Java JDK内置工具的使用说明

  • OOM分析,内存故障、CPU100%等问题分析

你为什么要电脑中安装一个jdk呢?然后还要配置环境变量?

1、Windows系统、Linux系统并不是sun或oracle公司,系统并不会在你没安装的情况提供的最基础的环境保障。

2、安装jdk的原因,让操作系统提供的java环境,这样的未来你安装tomcat、zookeeper、jemeter等这系列的应用都能够得到支持。

3、环境变量一定要配置吗?答案是一定要配置的:因为tomcat、zookeeper、jmeter等这些java软件都依赖jdk。这些java软件在启动的时候,就必须jdk环境,否则启动不起来,问题来?这些软件是怎么找到你的jdk呢?答案就通过JAVA_HOME 。只有你配置的环境变量叫JAVA_HOME。这些软件才能够指定你的jdk在那个位置。然后就会触发类加载器,把tomcat、zookeeper等java应用程序加载jvm中。然后运行起来。

01、Java技术体系

官方所定义的java技术体系:

  • Java 程序设计语言

  • Java虚拟机

  • Class文件格式

  • JavaAPI类库

  • 来自商业机构和开源社区的第三方java类库

面试题:jdk jre 和jvm的关系

  • JDK: java development kit, java开发工具包,用来开发Java程序的,针对java开发者。

  • JRE: java runtime environment, java运行时环境,针对java用户,程序如果要运行其实只需要jre环境即可

  • JVM: java virtual machine,java虚拟机 用来解释执行字节码文件(class文件)的。 就是代码最终存储和运行的区域。

02、Java的特征

Java能获得如此广泛的认可,除了它拥有一门结构严谨、面向对象的编程语言之外,还有许多不可忽视的优点:

02-1、简单性:

Java是纯面向对象语言,语法简单明了,易于掌握。 Java使用接口取代了多重继承,并且取消了指针,因为多重继承和指针会使程序变得复杂。 Java还会自动地收集内存垃圾,使得内存管理变得更为简单。 Java还提供了丰富的类库、API文档以及第三方开发包,还有大量Java的开源项目。

02-2、面向对象性:

面向对象性事Java语言的基础。 java语言提倡“万物皆对象”,语法中不能在类外面定义单独的数据和函数。 Java语言最外部的数据类型是对象,所有的元素都要通过类和对象来访问。

02-3、分布性:

Java的分布性包括:1.操作分布。2.数据分布 ⑴操作分布:即在多个不同的主机上不知相关操作。 ⑵数据分布:将数据分别存放在不同的主机上,这些主机是网络中的不同成员。

02-4、可移植性跨平台

Java程序具有与体系结构无关的特性。 Java的类库也提供了针对不同平台的接口,所有这些类库也可以被移植。

所谓跨平台:其实就是sun/oracle公司,给每种操作系统都提供的不同版本jvm。

有windows版本的,linux版本的。

02-5、安全性

Java语言删除了类C语言的指针和内存释放等语法,有效地避免了用户对内存的非法操作 Java程序代码要经过代码程序校验、指针校验等很多测试步骤才能够运行。所以未经允许的Java程序不能出现损害系统平台的行为。 Java可以编写出防病毒和防修改系统。

02-6、健壮性

Java会检查程序在编码和运行时的错误,并消除错误。

02-7、多线程性。

Java应用程序可以在同一时间并行执行多项任务。而且相应的同步机制可以保证不同线程能够正确地共享数据。

02-8、高性能性

Java编译后的字节码是在解释器中运行的,所以它的速度较多数交互式运用程序提高了很多。

02-9、动态性

Java可以动态调整库中方法和增加变量,并且客户端不需要任何更改。在Java中进行动态调整是非常简单和直接。

02-10、良好的生态

  • 培训机构 80%~90%

03、JVM组成

  • 类加载器(ClassLoader)

  • 运行时数据区(Runtime Data Area)

  • 执行引擎(Execution Engine)

  • 本地库接口(Native Interface)

01、问:你对jvm的认识什么?构成是什么?

我对jvm的认识是,它分别是是由、类加载器(ClassLoader)、运行时数据区(Runtime Data Area) 、字节执行引擎(Execution Engine) 、本地库接口(Native Interface),其中运行时数据区是有:

  • 线程区域共享区:方法区、堆

  • 非线程共享区:java虚拟机栈(线程栈)、本地方法栈、程序计数器.

02、jvm中这些区域的关系?

1、类加载器负责把程序中的所有的classes和jar以及网络资源jar都找到,然后加载的运行时数据区。

2、把类信息、常量、静态常量等写入到方法区中。

3、当程序执行某个方法的时候,字节执行引擎就开启线程,比如执行main函数,比如执行makeorder方法的时候,线程执行方法的称之为为入栈,说明要干活了,如果线程把活干完了,就说名方法执行结束了,接下来:出栈。一句话:

方法执行入栈,方法执行完毕叫出栈

4:在方法的执行过程中,可能你方法中存在对象的申请,就是堆区申请一块内存空间进行指针映射。然后会线程一个本地局部变量的gcroots。来维持整个对象的生命周期。

5、无论执行方法过程中,还是方法执行结束,都可能会引发垃圾回收,

  • 比如在执行makeorder过程中,创建new Order()可能会堆申请。如果空间不足,就触发 minor gc 和full gc。

  • 比如在执行makeorder结束,创建new Order()可能就进行回收处理,别的对象申请过来。如果内存空间不足,就触发gc

03、通过上面的简单分析?你会得到一个什么信息?

  • 我们的优化其实就是围绕:运行时数据区进行内存调整和优化。

  • 以及GC垃圾回收的处理机制,不同的垃圾回收机制造成的stw时间不一样。

04、为什么要优化垃圾回收机制?

  • 其实不同的垃圾回收机制(CMS,ZG1 ,G1)等。他们的性能都不一样,但是他们都进行升级更替,其目的都是为来解决一个问题:STW 。 stop the word ,意思:垃圾回收的停顿时间,具体含义是:当堆内存不足时候会触发minor gc ,当老年代空间不足的情况触发一次:full gc ,如果还不够:oom 。在进行minor gc或者full gc过程中,字节引擎就会把所有者正在执行的线程全部挂起,挂起过程中就是:STW

  • 使用不同的垃圾收集器,它STW性能和时间是不一样的。

  • 我们区优化内存空间,其实很大的程度就是为了减少:minor gc 和 fullgc 因为只要内存空间充足的情况,才能够尽量的避免造成的stw。

04、类&对象在JVM是怎么运行和存储的?

04-01、案例分析

比如有一个类TestMain.java

package com.kuang.utils;
​
import com.kuang.entity.User;
import com.kuang.entity.user.User;
​
/**
 * @author August Lee
 * @Title: 
 * @Description: 
 * @date 2021/7/28 17:06
 */
public class TestMath {
​
    // 成员变量
    public static final int num = 100;
    public static User user = new User();
​
    // 求和方法
    public int sum() {
        int x = 1;
        int y = 2;
        int z = (x + y) * 10;
        return z;
    }
​
    // main方法 
    public static void main(String[] args) {
        TestMath testMath = new TestMath();
        testMath.sum();
    }
​
}

1、 比如上面的代码如下idea、eclipse开发的话,首先你的工具必须要依赖一个jdk.

2、 当你在执行main函数的时候,就触发工具依赖的jdk,调用javac进行编译class字节文件

3、 同时生成字节文件之后,会触发java命令,调用类加载加载器,把类信息写入方法区中。

4、 同时字节执行引擎开启一个线程,在java虚拟机栈(线程栈)把main方法包裹栈帧进行入栈,main方法开始执行了

5、 在执行过程中,如果main函数有局部变量,就会在栈帧的局部变量表中进行存储,如果计算就在操作数中进行计算。

6、 如果存在对象引用

,就在堆中申请内存空间,通过引用进行指向。并且在指向的过程中会形成一个gcroots树状的结构,目的就是判别方法在执行过程中,对象是否还在使用,没有使用的话,gcroots就会割掉,一旦割掉。当堆空间不足的情况,字节引擎就触发gc,进行垃圾回收,回收过程中就会把那些没有gcroots头进行标记,然后在清除。

如果gcroots结构下面的对象还存活的呢,gc会把存活的对象进行转移suvivor区。如果一直或者转移和复制次数达到15次,就会写入老年代

一句话:垃圾回收gc其实做两件事情:一个存活对象的转移 和 死亡对象的标记清理

整个加载和运行如下图:

  • xxx.java文件通过编译javac以后会生产类字节class文件,通过过类加载器(ClassLoader)转换成字节码。

  • 运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不 能直接交给底层操作系统去执行。

  • 因此需要特定的命令解析器执行引擎(Execution Engine),

  • 将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能

04-02、JVM运行时数据区

  • 方法区(元空间)

  • 虚拟机 栈(线程栈)

  • 本地方法栈

  • 程序计数器

1、调优的思考和方向

其实我们在开发中,进行性能的调优很大程度都是针对这个区域进行内存的扩大和调整。这里的每个区域都是内存划分的区域,每个区域存储的数据都不一样,都只是配合完成一个类或者一个对象整个的存储和运行的过程。

2、方法的执行过程

如果我们开发过程中,只要执行一个方法,发起一个请求,最终都是开辟一个线程去执行,这个时候就在为没个线程都创建一个虚拟机栈,本地方法栈,程序计数器 来调度和共享堆区的数据,然后GC也不停的回收内存空间。如下图:

当程序在执行方法的过程中如下:

04-03、栈帧

它会把线程执行的方法在虚拟机栈申请一块空间出来,然后把整个方法形成一个栈帧,而这个栈帧中包含如下几个部分:

何谓栈帧:一个方法在线程栈中对应的一块内存区域。就称之为栈帧。

  • 局部变量表:存储方法的局部变量的存储结构

  • 操作数栈:提供方法在运算过程中的操作数的临时的存储结构。比如:+ - * / 等。

  • 方法入口信息(动态链接):把符号引用转换成直接引用, 比如在执行方法obj.sum()的过程中,我们需要找到方法代码存储的位置,我们知道方法的代码存储在方法区中的类信息中,如果找到这块区域,就是通过动态链接获取,动态链接会把方法区中的代码取出压栈到栈帧中,然后提供给线程执行。

  • 方法出口信息:记录方法执行结束的位置,并且保存线程的生命状态,然后如果栈还有栈帧就继续执行,直到结束位置。

04-04、出栈和入栈

在上面我们已经明白在java中执行某个方法的调用,就是栈调用的过程,这个时候我们会把方法中的变量结果都放入到Java栈中,我们称之为入栈,入下图:

当方法执行完毕以后:就会把栈帧从栈中移除,获取返回的结果,继续执行main,执行完毕释放栈,GC开始回收。

总之一句话:执行方法在线程栈分配内存空间是入栈,执行方法结束释放内存空间就是出栈,和数据结构中的FIFO是一个道理。

栈的规则

stack 栈,是一个 先入后出(FILO,First In Last Out)的 有序列表,可以形象地理解为手枪的弹匣,装子弹是入栈,打枪是出栈。

先进的后出来,后进来的先出去,因为这样才符合方法执行调用的顺序。

05、方法区

也被称为永久代,在jdk1.8中称之为:元数据空间,是线程共享的区域。存储已被虚拟机加载的**类信息、常量、静态变量、即使编译器编译后的代码等数据**。方法区无法满足内存分配需求时,抛出OutOfMemoryError。JVM规范被没要求这个区域需要实现垃圾收集,因为方法区域回收主要针对的是:类和常量池的信息回收,回收结果往往难以令人满意。

运行时常量池:是方法区的一部分。Java语言不要求常量只能在编译期产生,换言之,在运行期间也能将新的常量放入。

垃圾回收:是会关顾这方法区,但是回收的效果往往是很差强人意的,一般很难回收。因为这个大部分情况下都是强引用。如果成员变量是大对象,可能会直接写入到老年代。

如果我们无限的增加类,或者加载的类过多,可能就会造成这块的内存不够,就会出现内存溢出的现象。比如在springboot和spring中依赖的jar包过多,可能会引发这块的区域不够,我们就需要调整大小,如果没有出现的情况下不建议去调整。

方法区空间大小设置: -XX:PermSize -XX:MaxPermSize 1.8之后设置: -XX:MetaspaceSize -XX:MaxMetaspaceSize

java -jar  -XX:MetaspaceSize=256m  -XX:MaxMetaspaceSize=256m  xxx.jar

06、字节码文件class的反编译

05-01、class反编译

使用sublimetext、或classpy.jar 可以得到字节码文件的内容如下:

05-02、javap反编译

由于上面的反编译不便于理解和查看,而且都是经过加密处理,当然上面大的每个编码都已经经过了加密处理。在oracle官网都有对照表,但是这种理解程度看的是很头大,我们可以通过javap进行反编译能够让我们开发者能够阅读的方式如下:

javap -c TestMath.class >TestMath.txt

编译以后如下:

Compiled from "TestMath.java"
public class com.kuang.utils.TestMath {
  public static final int num;
​
  public static com.kuang.entity.User user;
​
  public com.kuang.utils.TestMath();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
​
  public int sum();
    Code:
       0: iconst_1 在局部变量表中申请空间x
       1: istore_1 通过操作数栈将1复制给x
           
       2: iconst_2 在局部变量表中申请空间y
       3: istore_2 通过操作数栈将2复制给 y
           
           
       4: iload_1 将局部变量表中的第一个int类型取出 x = 1
       5: iload_2 将局部变量表中的第一个int类型取出 y = 2
       6: iadd  进行相加处理   3
       7: bipush    将10出栈10
       9: imul  乘以  30
      10: istore_3 z  = 30 
      11: iload_3 30
      12: ireturn 30
        
          
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/kuang/utils/TestMath -- 找到这个类
       3: dup
       4: invokespecial #3                  // Method "<init>":()V --- 调用构造函数
       7: astore_1  ------在局部变量表中生成一个引用指针指向堆空间
       8: aload_1 ------- 把testMath引用操局部变量表取出来
       9: invokevirtual #4                  // Method sum:()I --- 调用一个方法
      12: pop
      13: return
​
  static {};
    Code:
       0: new           #5                  // class com/kuang/entity/User
       3: dup
       4: invokespecial #6                  // Method com/kuang/entity/User."<init>":()V
       7: putstatic     #7                  // Field user:Lcom/kuang/entity/User;
      10: return
}

分析

0: iconst_1 
1: istore_1

1:在栈帧的局部变量表中申请一块空间存放局部变量a。然后在操作数栈申请一块空间存储值:1.

2:然在操作数栈把数值1,出栈,然后赋值给局部变量表a。完成局部变量赋值的过程。

07、程序计数器

概述 相信学过计算机组成原理的人都知道,CPU内部的寄存器中就包含一个程序计数器,存放程序执行的下一条指令地址。 程序每次执行时,CPU都会自动存储下一条的指令的地址,以便下次继续执行

Jvm偷学了CPU JVM也是参考了CPU的程序计数器,自己设计了一个程序计算器。用于存储JVM当前执行bytecode的地址(节码指令地址)。 程序计算器:记录当前正在执行的字节码地址。 程序计数器是线程隔离的:每一个线程在工作的时候都有一个独立的计数器

程序计数器的修改计数是:字节码执行引擎来修改的。

字节码解释器的工作是通过改变这个计数值来读取下一条要执行的字节码指令。 多线程是通过线程轮流切换并分配处理器执行时间来实现的,任何一个时刻,一个内核只能执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。这就是一开始说的“线程私有”。如果线程正在执行的方法是Java方法,计数器记录的是虚拟机字节码的指令地址;如果是Native方法,计数器值为空。

程序计数器是唯一一个在Java虚拟机规范中没有规定OOM(OutOfMemoryError)情况的区域

08、本地方法栈

上述虚拟机栈为JVM执行Java方法服务,本地方法则为执行Native服务。其他和虚拟机栈类似,也会抛出 StackOverflowError、OutOfMemoryError。

但是虽然本地方法栈不是用java编写的,但是原理和java虚拟机栈执行是一样的,也是栈帧出栈入栈的过程。那么那些方法才是本地方法呢?在java中我们的定义父类或者说超类:Object如下:

package java.lang;
​
public class Object {
​
    private static native void registerNatives();
    static {
        registerNatives();
    }
    public final native Class<?> getClass();
    public native int hashCode();
    public boolean equals(Object obj) {
        return (this == obj);
    }
    protected native Object clone() throws CloneNotSupportedException;
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    public final native void notify();
    public final native void notifyAll();
    public final native void wait(long timeout) throws InterruptedException;
    public final void wait(long timeout, int nanos) throws InterruptedException {
        
    }
    public final void wait() throws InterruptedException {
        wait(0);
    }
    protected void finalize() throws Throwable { }
}
​

面试题

下面不属于Object类中方法的是:

  • hashCode()

  • finally()

  • wait()

  • toString()

09、堆内存结构示意图

10、可达性分析算法

将"GC Roots"对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为:非垃圾对象,其余未标记的对象都是理解对象。

GC Roots根节点:

  • 线程栈的本地变量

  • 静态变量

  • 本地方法栈的变量

  • 方法区常量引用的对象 等等。

你可以理解成为:一个方法执行的时候,会开启一个线程去执行该方法。这个时候就可以当做一个GC Root, 它会自动去收集该方法中所有的对象的引用关系,变成一个树结构,当执行完毕一会就自动删除GC Root.先所有的对象会自动全部释放。

  • 比如:线程栈的本地变量,user--在堆里面申请了一块空间,这个时候如果用户还引用了别的对象

  • 这个时候如果内存不够,就会触发gc,就会开始对整个对象的链路进行标记,看是否是垃圾对象还是非垃圾对象,如果是对象没有被引用了。直接进行标记,

  • 如果对象还存在引用关系,执行引擎会把这些个非垃圾对象进行内存的转移迁移。也就是触发一次minior gc。

  • 会把存活的对象迁移到s0 并且累加 + 1

  • 如果对象还一直存活内存不够s0也会触发gc进行迁移,累加+1

  • 如果堆内存不够,对象还存活中,会把对象在s0和s1区反复的迁移,迁移累加次数超过15次,如果还存活就会把存活的对象,写入到老年代中。


Comments