JVM入门和实战

JVM入门和实战

一、JVM的介绍
1、JVM是什么

1.JVM(Java Virtual Machine):Java程序的运行环境(Java虚拟机)
2.所谓的虚拟机是指:通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的计算机系统
3.JVM是通过软件来模拟Java字节码的指令集(物理机上是CPU来执行指令集),是Java程序的运行环境

2、JVM的概述
  • 开发过程
    1.编写Java源代码即对应的.java文件
    2.通过Java编译器如javac命名和一些IDE工具将,把.java文件编译成对应的.class的java字节码文件

  • 运行期的过程(简易)
    1.Java平台运行期环境会通过类装载器(ClassLoader)将上面生成的字节码文件通过本地或者网络的方式,将字节码文件装载到虚拟机中
    2.装载过程中类装载器会对字节码进行校验和认证,验证该字节码符不符合虚拟机规范,格式是否正确,同时还会去装载必要的Java类库,例如:基本的数据类型,基本的if else、for循环处理,基本的java安全的处理
    3.如果上面校验通过并将类库装载进来之后,就会将这些交给Java解释器,Java解释器就会对字节码进行解释并执行,一条一条的解释然后执行
    4.即时编译器即我们的JIT
    5.当我们把.class交到JVM虚拟机运行,首先会进行内存分配,分配完成,便能进行执行,这里涉及字节码执行引擎,运行过程当中这个内存中产生的垃圾,就要进行垃圾回收,如果涉及到很多并发的代码就会有一些高效的并发处理

  • 硬件运行
    1.如果应用需要和操作系统或者硬件交互的话,通过JMI就会出虚拟机,由虚拟机和操作系统进行交互,操作系统和硬件进行交互

在这里插入图片描述

3、JVM主要功能

1、通过ClassLoader 寻找和装载class文件
2、解释字节码成为指令并执行,提供class文件的运行环境
3、进行运行期间的内存分配和垃圾回收
4、提供与硬件交互的平台

4、虚拟机是Java平台无关的保障

1.java源程序即*.java文件,通过javac编译成对应的.class文件,这个使我们的开发过程
2.当生成了.class文件后我们会装载到Java虚拟机中运行
3.java虚拟机向我们的程序屏蔽了平台相关的一些东西,例如操作系统:Linux、Windows、Unix,每个平台特性相关的东西我们的应用程序是不管的,即我们的应用程序与平台无关,平台相关的特性有Java虚拟机屏蔽了
4.Java虚拟机在不同的操作系统上安装的版本是不一样的,我们Java虚拟机和平台是有关的
5.我们开发的应用程序是和平台无关的,因为我们在写程序的时候不会为Linux、Windows、Unix分别写一套程序,而是写一套东西统一针对虚拟机而虚拟机在那个平台上我们不做关心
6.我们的Java的程序是和我们的Java虚拟机平台相关的
在这里插入图片描述

4、虚拟机是Java平台无关的保障

1.Java 虚拟机规范为不同的硬件平台提供了一种编译Java的技术规范

不管底层是什么样的硬件平台、什么样的操作系统,Java源文件编译出来的 .class 文件,必须符合 JVM的规范,JVM就认可该文件,因为操作系统方面是通过JVM去做交互的

2.该规范使 Java 软件独立于平台,因为编译是针对作为虚拟机的 “一般机器” 而做

开发语言的无关性,因为JVM只认 .class字节码文件,并不是只有Java语言会编译成 .class文件,类似:jRuby、jPython等等,还有ASM能直接编写字节码的方式生成 .class文件
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210107231839725.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTQ0NTg3OQ==,size_16,color_FFFFFF,t_70#pic_center

3.这个 “一般机器” 可用软件模拟并运行于各种现存的计算机系统,也可用硬件来实现

5、JVM 规范定义的主要内容

1.字节码指令集(相当于中央处理器CPU)

可以查看 《Java虚拟机规范 Java SE 8版》

2.Class文件的格式(后续会具体讲)
3.数据类型和值

可以查看 《Java虚拟机规范 Java SE 8版》

4.运行时数据区(后续会具体讲)
5.栈帧(后续会具体讲)
6.特殊方法

:实例初始化方法,通过 JVM 的 invokespecial 指令来调用

我们使用的构造方法实际上是虚拟机创建完实例的一个回调方法,通过该方法
:类或接口的初始化方法,不包含参数,返回 void
static 静态的类无需实例化

7.类库

Java 虚拟机必须要对一些 Java 类库提供支持,否则这些类库根本无法实现,比如下面这些:

反射
加载和创建 类或接口,如 ClassLoader
连接和初始化类和接口的类
安全,如security
多线程
弱引用

8.异常
9.虚拟机的启动、加载、连接和初始化

5、Class文件
  • Class 文件格式概述
    1、Class 文件是JVM的输入,Java 虚拟机的规范定义了 Class 文件的结构。Class 文件是JVM实现平台无关、技术无关的基础

①:Class 文件是一组以8字节为单位的字节流,各个数据项目按顺序紧凑排列
②:对于占用空间大于8字节的数据项,按照高位在前的方式分割成多个8字节进行存储
③:Class文件格式里面只有两种类型:无符号数,表

(1)无符号数:基本数据类型,以u1、u2、u4、u8来代表几个字节的无符号数
(2) 表:由多个无符号数和其它表构成的复合数据类型,通常以"_info"结尾

2、创建一个Java类来生成 .class 文件
1.通过命令窗口来生成:javap -verbose java类的绝对路径(经过反编译)
在这里插入图片描述
2.使用UltraEdit 16进制编译器打开我们上面的Hello.java文件(.class文件的原始信息)
在这里插入图片描述

package com.capuda.jvm.classdef;

/**
 * @author: Capuda
 * @date: 2021年01月09日 13:39
 */
public class Hello {

    private static String msg = "Good morning";

    public static void main(String[] args) {
        System.out.println("msg==" + msg);
    }
}

  • Class 文件格式
    1.javap 工具生成非正式的 “虚拟机汇编语言” ,格式如下:
    <index> <opcode> [ <operand1> [ <operand2> ... ]] [ <comment> ]

①:<index> 是指令操作码在数组中的下标,该数组以字节形式来存储当前方法的Java虚拟机代码;也可以是相对于方法起始处的字节偏移量
②:<opcode> 是指令的助记码、<operand> 是操作数、<comment> 是行尾的注释

在这里插入图片描述- 阅读Class字节码文件
1.Class 文件格式说明

①:constant_pool_count:是从1开始的
②:不同的常量类型,用tag来区分的,它后面对应的 info 结构是不一样的
③:‘L’ 表示对象,’[’ 表示数组,V 表示 void

2.了解预定义attribute的含义

可以在《Java虚拟机规范 Java SE 8版》一书中的第四章的第4.7小节可以查看

3.stack:方法执行时,操作栈的深度
4.Locals:局部变量所需的存储空间,单位是 slot
5.slot 是虚拟机为局部变量分配内存所使用的最小单位
6.args_size:参数个数,为1的话,因实例方法默认会传入this,locals也会预留一个slot来存放

Classfile /D:/Project/self_projecct/JVMStudy/target/classes/com/capuda/jvm/classdef/Hello.class
  Last modified 2021-1-9; size 842 bytes
  MD5 checksum 733c3fd80154a0324923b67dc7e6d7e9
  Compiled from "Hello.java"
public class com.capuda.jvm.classdef.Hello
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#29        // java/lang/Object."<init>":()V
   #2 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #32            // java/lang/StringBuilder
   #4 = Methodref          #3.#29         // java/lang/StringBuilder."<init>":()V
   #5 = String             #33            // msg==
   #6 = Methodref          #3.#34         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Fieldref           #11.#35        // com/capuda/jvm/classdef/Hello.msg:Ljava/lang/String;
   #8 = Methodref          #3.#36         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = String             #39            // Good morning
  #11 = Class              #40            // com/capuda/jvm/classdef/Hello
  #12 = Class              #41            // java/lang/Object
  #13 = Utf8               msg
  #14 = Utf8               Ljava/lang/String;
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/capuda/jvm/classdef/Hello;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               <clinit>
  #27 = Utf8               SourceFile
  #28 = Utf8               Hello.java
  #29 = NameAndType        #15:#16        // "<init>":()V
  #30 = Class              #42            // java/lang/System
  #31 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
  #32 = Utf8               java/lang/StringBuilder
  #33 = Utf8               msg==
  #34 = NameAndType        #45:#46        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #35 = NameAndType        #13:#14        // msg:Ljava/lang/String;
  #36 = NameAndType        #47:#48        // toString:()Ljava/lang/String;
  #37 = Class              #49            // java/io/PrintStream
  #38 = NameAndType        #50:#51        // println:(Ljava/lang/String;)V
  #39 = Utf8               Good morning
  #40 = Utf8               com/capuda/jvm/classdef/Hello
  #41 = Utf8               java/lang/Object
  #42 = Utf8               java/lang/System
  #43 = Utf8               out
  #44 = Utf8               Ljava/io/PrintStream;
  #45 = Utf8               append
  #46 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #47 = Utf8               toString
  #48 = Utf8               ()Ljava/lang/String;
  #49 = Utf8               java/io/PrintStream
  #50 = Utf8               println
  #51 = Utf8               (Ljava/lang/String;)V
{
  public com.capuda.jvm.classdef.Hello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/capuda/jvm/classdef/Hello;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String msg==
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: getstatic     #7                  // Field msg:Ljava/lang/String;
        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        27: return
      LineNumberTable:
        line 12: 0
        line 13: 27
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      28     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #10                 // String Good morning
         2: putstatic     #7                  // Field msg:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 9: 0
}
SourceFile: "Hello.java"
6、ASM开发
  • ASM概述
    1.ASM 是一个Java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能
    2.ASM 可以直接产生二进制class文件,也可以在类被加载入虚拟机之前动态改变类行为,ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能根据要求生成新类
    3.目前许多框架如:cglib、Hibernate、Spring 都直接或间接地使用ASM操作字节码

  • ASM 编程模型
    1.Core API:提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存,但这种编程方式难度较大
    2.Tree API:提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存中,所以这种方法需要更多的内存,这种编程方式较简单

  • ASM的Core API
    1.ASM Core API 中操纵字节码的功能基于 ClassVisitor 接口。
    2.ASM 提供了三个基于 ClassVisitor 接口的类来实现 class 文件的生成和转换

①:ClassReader:ClassReader 解析一个类的 class 字节码
②:ClassAdapte:ClassAdapte 是 ClassVisitor 的实现类,实现要变化的功能
③:ClassWriter:ClassWriter 也是 ClassVisitor 的实现类,可以用来输出变化后的字节码(也可以不输出,将修改后的字节码保存在内存中有ClassLoader进行加载)

3.ASM给我们提供了了ASMifier工具来帮助开发,可使用ASMifier工具生成ASM结构来对比

  • 使用ASM开发
    1.我使用的是idea开发工具,下载 ASM Bytecode Outline 插件进行使用
    在这里插入图片描述2.ASM Bytecode Outline 的使用方法,选中对应的类,然后点击下午按钮,可以生成对应的ASM文件
    在这里插入图片描述3.通过ASM来增加类中方法的计时功能能,下面查看下待加强代码和待加强代码的ASMified

Java代码

public class CC {

    public void m1() {
        System.out.println("now in method m1-------->");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.这里是通过一堆的visit_ 方法来操作字节码

public class CCDump implements Opcodes {

    public static byte[] dump() throws Exception {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;

        cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, "com/capuda/jvm/asm/CC", null, "java/lang/Object", null);

        cw.visitSource("CC.java", null);

        { // 调用实例化方法(即JVM中的 <init>方法初始化类)
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(7, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv.visitInsn(RETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lcom/capuda/jvm/asm/CC;", null, l0, l1, 0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        {// 调用Java类中的m1方法
            mv = cw.visitMethod(ACC_PUBLIC, "m1", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            Label l1 = new Label();
            Label l2 = new Label();
            mv.visitTryCatchBlock(l0, l1, l2, "java/lang/InterruptedException");
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(10, l3);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("now in method m1-------->");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv.visitLabel(l0);
            mv.visitLineNumber(12, l0);
            mv.visitLdcInsn(new Long(100L));
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
            mv.visitLabel(l1);
            mv.visitLineNumber(15, l1);
            Label l4 = new Label();
            mv.visitJumpInsn(GOTO, l4);
            mv.visitLabel(l2);
            mv.visitLineNumber(13, l2);
            mv.visitVarInsn(ASTORE, 1);
            Label l5 = new Label();
            mv.visitLabel(l5);
            mv.visitLineNumber(14, l5);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);
            mv.visitLabel(l4);
            mv.visitLineNumber(16, l4);
            mv.visitInsn(RETURN);
            Label l6 = new Label();
            mv.visitLabel(l6);
            mv.visitLocalVariable("e", "Ljava/lang/InterruptedException;", null, l5, l4, 1);
            mv.visitLocalVariable("this", "Lcom/capuda/jvm/asm/CC;", null, l3, l6, 0);
            mv.visitMaxs(2, 2);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}

5.我们可以写一个加强的代码,我们增加了执行方法时间的计算

public class CC {

    public void m1() {
        long a1 = System.currentTimeMillis();
        System.out.println("now in method m1-------->");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long a2 = System.currentTimeMillis();
        System.out.println("invoke method total time====" + (a2 - a1));
    }
}

6.我们为加强后的代码生成ASMified文件,和上面文件进行对比就知道我们需要增加神什么,然后通过ASM的visit_方法将差别的地方即代码添加到未加强代码的字节码中
开头添加了:

            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LSTORE, 1);

结尾添加了

			mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, 3);
            Label l7 = new Label();
            mv.visitLabel(l7);
            mv.visitLineNumber(18, l7);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("invoke method total time====");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, 3);
            mv.visitVarInsn(LLOAD, 1);
            mv.visitInsn(LSUB);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

7.通过代码进行添加(如果报错注意一下引入的包路径是否正确)

package com.capuda.jvm.asm;


import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * @author: Capuda
 * @date: 2021年01月10日 21:25
 */
public class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM7, classVisitor);
    }

    /**
     *
     * @param version 版本
     * @param access 访问权限
     * @param name 名称
     * @param signature 签名
     * @param superName 父类名称
     * @param interfaces 接口数组
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    /**
     * 访问方法
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);

        // 当我们的方法不是初始化的方法,并且不为null
        if (!"<init>".equals(name) && null != mv) {
            // 为这样的方法增加记录方法执行时间的功能
            mv = new MyMethodVisitor(mv);
        }
        return mv;
    }

    class MyMethodVisitor extends MethodVisitor{

        public MyMethodVisitor(MethodVisitor methodVisitor) {
            super(Opcodes.ASM7, methodVisitor);
        }

        /**
         * 类似于结尾吧
         */
        @Override
        public void visitCode() {
            super.visitCode();
            // 对比未增强ASMified文件,开头增加的代码
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LSTORE, 1);
        }

        /**
         * 类似于结尾
         */
        @Override
        public void visitInsn(int opcode) {

            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                    || opcode == Opcodes.ATHROW) {
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitVarInsn(Opcodes.LSTORE, 3);
                Label l7 = new Label();
                mv.visitLabel(l7);
                mv.visitLineNumber(18, l7);
                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
                mv.visitInsn(Opcodes.DUP);
                mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                mv.visitLdcInsn("invoke method total time====");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                mv.visitVarInsn(Opcodes.LLOAD, 3);
                mv.visitVarInsn(Opcodes.LLOAD, 1);
                mv.visitInsn(Opcodes.LSUB);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }

            mv.visitInsn(opcode);

        }
    }
}

8.创建一个启动来调用上面的类中方法,进行添加

package com.capuda.jvm.asm;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream;

/**
 * @author: Capuda
 * @date: 2021年01月10日 23:34
 */
public class Generator {
    public static void main(String[] args) throws Exception {
        ClassReader cr = new ClassReader("com.capuda.jvm.asm.CC");

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);

        ClassVisitor cv = new MyClassVisitor(cw);

        // 跳过校验
        cr.accept(cv, ClassReader.SKIP_DEBUG);

        byte[] data = cw.toByteArray();

        // 输出
        File f = new File("D:\\Project\\self_projecct\\JVMStudy\\src\\main\\java\\com\\capuda\\jvm\\asm\\CC.java");

        FileOutputStream fos = new FileOutputStream(f);
        fos.write(data);
        fos.close();

        System.out.println("Generator CC class success!!!!");
    }
}

9.之后查看下面代码的字节码文件就会发现增加了计时的功能

public class CC {

    public void m1() {
        System.out.println("now in method m1-------->");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

10.创建测试类对增强后的代码测试

public class MyTest {

    public static void main(String[] args) {
        CC cc = new CC();
        
        cc.m1();
    }
}

11.启动后会发现出现如下错误

Exception in thread “main” java.lang.VerifyError: (class: com/capuda/jvm/asm/CC, method: m1 signature: ()V) Register pair 1/2 contains wrong type
at com.capuda.jvm.asm.MyTest.main(MyTest.java:10)

12.错误的原因是因为增强后局部变量的类型改变了
```java
// 增强前的类型
 mv.visitVarInsn(ASTORE, 1);
// 字节码中的局部变量就是catch中的InterruptedException e
	try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
// 增强后的类型
 mv.visitVarInsn(LSTORE, 1);
 
 mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LSTORE, 1);// 将类型改成了LSTORE

13.处理问题的方法:我们在方法中进行实践的计算,改变了该方法的局部变量类型,我们在现在可以将其进行封装后使用,如下

// 封装计时方法
public class MyTimeLogger {

    private static long a1 = 0;

    public static void start() {
        a1 = System.currentTimeMillis();
    }

    public static void end() {
        long a2 = System.currentTimeMillis();
        System.out.println("now invoke method use time==" + (a2 - a1));
    }
}

14.在类中使用

    public class CC {

    public void m1()  {
        MyTimeLogger.start();
        System.out.println("now in method m1-------->");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        MyTimeLogger.end();
    }
}

15.增加的字节码如下

// 开头
mv.visitMethodInsn(INVOKESTATIC, "com/capuda/jvm/asm/MyTimeLogger", "start", "()V", false);
// 结尾
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/capuda/jvm/asm/MyTimeLogger", "end", "()V", false);

16.修改成如下代码

		/**
         * 类似于结尾吧
         */
        @Override
        public void visitCode() {
            mv.visitCode();
            // 对比未增强ASMified文件,开头增加的代码
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/capuda/jvm/asm/MyTimeLogger", "start", "()V", false);
        }

        /**
         * 类似于结尾
         */
        @Override
        public void visitInsn(int opcode) {

            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                    || opcode == Opcodes.ATHROW) {
                mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/capuda/jvm/asm/MyTimeLogger", "end", "()V", false);
            }

            mv.visitInsn(opcode);

        }

17.之后重新生成,测试就可以正常运行了,所以我们之后使用一些方法的时候最好将其封装

2、为什么要学JVM

1.面试需要

2.更深入理解Java

1.一个类是如何装载到JVM的
2.它在JVM中是如何存储的
3.如何运行的
4.方法是如何调用的
5.运行时内存怎么分配的
6.产生垃圾后是如何回收的

3.排查解决故障

1.内存溢出导致宕机了
2.CPU使用率居高不下
3.应用频繁卡顿
4.应用越跑越慢

4.性能调优

1.你需要去合理的分配内存
2.调整参数
3.选择GC垃圾回收器
4.第三方的中间件(有些也是通过java开发的也需要进行优化)

类加载器
一、类加载器的概述

1.类从被加载到JVM开始,到卸载出内存,整个生命周期如图

一、加载:查找并加载类文件的二进制数据
二、连接:就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去,包含如下几个步骤:

①验证:确保被加载类的正确性
②准备:为类的 静态变量 分配内存,并初始化它们(初始化默认是0)
③解析:把常量池中的符号引用转换成直接引用(所谓直接引用,可以直接执行目标的指针,就是我们的类可以直接引用常量池中的变量,而不用进行进行什么别的操作)

三、初始化:为类的静态变量赋初始值(默认为5)
之后就是可以被调用了

在这里插入图片描述

  • 类加载要完成的功能
    1、通过类的全限定名来获取该类的二进制字节流
    2、把二进制字节流转化为方法区的运行时数据结构(类被加在进来的二进制流就按照符合虚拟机的二进制流被存在方法区)
    3、在堆上创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构,并向外提供了访问方法区内数据结构接口(我们在去 new 一些使用都可以调用这个 .class 方法,这个方法就是这里的 java.lang.Class对象,拿到这个.Class就可以通过反射拿到这个对象的所有可访问的基本信息,例如:方法、属性等基本信息)

  • 加载类的方式
    1、最常见的方式:本地文件系统中加载、从jar等归档文件中加载
    2、动态的方式:将java源文件动态编译成class
    3、其它方式:网络下载、从专有数据库中加载等等

  • 类加载器
    1、Java虚拟机自带的加载器包括如下几种:

启动类加载器(BootstrapClassLoader)
平台类加载器(PlatformClassLoader)
应用程序类加载器(AppClassLoader)
JDK8没有平台类加载器(PlatformClassLoader),JDK8的是:扩展类加载器(ExtensionClassLoader),在DJK9该加载器被剔除了
:加载jre下ext文件下的jar包,就是当我们进行扩展的时候,就可以把我们的jar包放在ext文件下,但是这种方式不太安全,所以后面就被剔除了,之后JDK9有了模块化的开发,对这种扩展机制,就被模块化带来的天然能量给取代了 (其他原因可自行百度)
2、用户自定义的加载器,是java.lang.ClassLoader的子类,用户可以定制类的加载方式;只不过自定义类加载器其加载的顺序是在所有系统类加载器的最后

  • 类加载的关系
    1、下图的指针,子类指向父类
    在这里插入图片描述
二、类加载器的使用
  • 类加载器说明
    1.启动类加载器:用于加载启动的基础模块类,比如:java.base、java.management、java.xml等等

使用下面的代码来验证启动来加载器,结果返回的是null,因为该加载器是虚拟机内部的加载器不允许被外部改变和使用

public class ClassLoaderStudy {
    public static void main(String[] args) {
        String str = "Hello Class Loader";
        System.out.println("str class loader == " + str.getClass().getClassLoader());
    }
}

结果

str class loader == null

Process finished with exit code 0

2.平台类加载器:用于加载一些平台相关的模块,比如:java.scripting、java.compiler*、java.corba*等等

3.应用程序类加载器:用于加载应用级别的模块,比如:jdk.compiler、jdk.jartool、jdk.jshell 等等;还加载classpath路径中的所有类库;以上是JDK8以后的一些类加载器的介绍

4.JDK8:启动类加载器:负责将 <JAVA_HOME>\lib,或者 -Xbootclasspath 参数指定的路径中的,且是虚拟机识别的类库加载到内存中(按照名字照别,比如rt.jar,对于不能识别的文件不预装载)

5.JDK8:扩展类加载器:负责加载 <JRE_HOME>/lib/ext,或者java.ext.dirs 系统变量所指定路径中的所有类库

6.JDK8:应用程序类加载器:负责加载 classpath 路径中的所有类库

7.Java程序不能直接引用启动类加载器,直接设置classLoader 为 null,默认就使用启动类加载器

8.类加载器并不是需要等到某个类 “首次主动使用” 的使用才加载它,JVM规范允许类加载器在预料到某个类将要被使用的时候就预先加载它

9.如果在加载的时候 .class 文件缺失,会在该类首次主动使用时报告 LinkageError 错误,如果一直没有被使用,就不会报错(报错一般出现在下载的情况下,ClassA加载依赖ClassB,但是下载的使用先下载来ClassA运行的话就会报错)

  • 双亲委派模型说明
    1、(JDK13)JVM中的ClassLoader通常采用双亲委派模型,要求除了启动类加载器外,其余的类加载器都应该有自己的父级加载器(依赖关系上文有图)。这里的父子关系是组合而不是继承,工作过程如下:

①:一个类加载器接收到类加载请求后,首先搜索它的内建加载器定义的所有 “具名模块”
②:如果找到了合适的模块定义,将会使用该加载器来加载
③:如果class没有在这些加载器定义的具名模块中找到,那么将会委托给父级加载器,直到启动类加载器
④:如果父级加载器反馈它不能完成加载请求,比如在它的搜索路径下找不到这个类,那子的类加载器才自己来加载(过程是:通过classpath找对应路径下是否存在,如果不存在的话则调用其下面的子类进行加载)
⑤:在类路径下找到的类将成为这些加载器的无名模块

2、(JDK8)当一个类加载器接收到类加载请求后,他不会自己去加载而是委派给父类加载器去加载一级一级往上找,如果最顶级的父类也反馈找不到,这个时候子加载器才由自己去加载

3、双亲委派模型对于保证Java程序的稳定运作很重要

公用且具有一致性:一些公用使用的类只会被加载一次,例如java.lang包下的, String这些类,当你要找String这个类的时候不管你是自定义加载器还是其他什么加载,都会一级一级往上找,最后又启动类加载器去加载,个性化的东西在交给自己的自定义的加载器去完成,这样能保证找到的是同一个String的类,从未避免了你使用不同的类而导致应用的错误,安全性:当一个类被加载过后了就不能被重新加载,即不能有同包同名的类被加载,保证这些系统类不会被恶意修改或者恶意覆盖

4、实现双亲委派的代码在java.lang.ClassLoader的loadClass()方法中,如果自定义类加载器的话,推荐覆盖实现findClass()方法

  • 创建一个自定义加载及器,代码如下
// 自定义类加载器,重写了findClass方法
public class MyClassLoader extends ClassLoader {
    private String myName = "";

    public MyClassLoader(String myName) {
        this.myName = myName;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = this.loadClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }

    private byte[] loadClassData(String clsName) {
        byte[] data = null;

        InputStream in = null;

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        clsName = clsName.replace(".", "/");

        try {
            in = new FileInputStream(new File("classes/" + clsName + ".class"));

            int a = 0;
            while ((a = in.read()) != -1) {
                out.write(a);
            }

            data = out.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return data;
    }
}
// 类加载器加载的类
public class MyClass {
    public void t() {
        System.out.println("now in myclass t()");
    }
}
// 使用自定义类加载器
public class ClassLoaderStudy {
    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader myClassLoader = new MyClassLoader("myClassloader1");

        Class cls1 = myClassLoader.loadClass("com.capuda.jvm.classloader.MyClass");
        System.out.println("cls1 class loader == " + cls1.getClassLoader());
        System.out.println("cls1 class loader == " + cls1.getClassLoader().getParent());

    }
}

结果如下

cls1 class loader == sun.misc.Launcher$AppClassLoader@58644d46 // 应用加载器(自定义类加载器的父级)
cls1 class loader == sun.misc.Launcher$ExtClassLoader@3caeaf62 // 扩展类加载器(应用加载器的父级)

1.如果我们想获取到MyClassLoader自定义加载器,需要将类更换位置,如下图,启动类启动后类加载器会从src路径下加载名字叫作MyClass的类,会先由父级寻找,最终找不到会根据子类的classpath找到对应的类,所以我们需要删除src下的MyClass类
在这里插入图片描述结果如下(我idea上没有弄成功,各位大哥可以去调一下)

在这里插入图片描述
5、如果有一个类加载器能加载某个类,称为 定义类加载器,所有能成功返回该类的Class 的类加载器 都被称为 初始类加载器

6、如果没有指定父加载器,默认就是启动加载器

7、每个类加载器都有自己的命名空间,命名空间由该加载器及其所有父加载器所加载类的类构成,不同的命名空间,可以出现 类的全路径名 相同的情况(当我们有两个线程,两个线程中的父加载器中会出现两个类的全路径名相同的事)

8、运行时包由同一个类加载器的类构成,决定两个类是否属于同一个运行时包,不仅要看全路径名是否一样,还要看定义类加载器是否相同。只有属于同一个运行时包的类才能实现相互包内可见

  • 破坏双亲委派模型
    1、双亲模型有个问题:父加载器无法向下识别子加载器加载的资源

例如我们的 DriverManger 和我们的 jdbc.Driver,DriverManger是由扩展类加载器加载的,Driver是由应用程序类加载器加载的,所以我们连接数据库是通过你DriverManger,DriverManger又会通过Driver 去建立连接,及这里父加载器需要去获取子加载器中的数据(扩展类加载器是应用程序类加载器的父加载器)

2、未解决上面这个问题,引入了线程上下文类加载器,可以通过Thread的setContextClassLoader()进行设置,例如下文的代码

// 线程在加载的时候讲子加载器的内容存入上下文中,当要使用的使用通过getContextClassLoader获取对应的类进行使用
 synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

3、另外一种典型情况就是实现热替换,比如OSGI的模块化热部署,它的类加载器就不再是严格按照双亲委派模型,很多可能就在平级的类加载器中执行了

三、类连接主要验证的内容
  • 类连接中的验证
    1、类文件结构检查:按照JVM规范规定的类文件结构进行

2、元数据验证:对字节码描述的信息进行语义分析,保证其符合Java语言规范要求(比如:这个类是否有父类,父类是否允许继承,如果父类被final修饰就不允许被继承;看父类是不是抽象类,里面是不是有一些该实现的方法没有实现,有没有重载等等)

3、字节码验证:通过对数据流和控制流(程序中的 if else,for循环这些)进行分析,确保程序语义是合法和符合逻辑的(对方法体进行校验,比如说:保证运行期间操作出栈的类型和压栈的数据类型要匹配;保证跳转的指令不会跳转到方法体外去;保证类型转换是有效的可用的),这里主要对方法体进行校验

4、符号引用验证:对类自身以外的信息,也就是常量池中的各种符号引用,进行匹配校验(验证该类是否缺少或者被禁止访问他依赖的某些外部类、方法、字段等等资源;例如:符号引用当中通过字符串描述的类的全限定名,会根据该全限定名判断是否可以在虚拟机中找到这个类;符号引用当中你要去调类、调符号或者方法、访问字段等等,他就回去检查看能不能访问到,有没有权限 )

  • 类连接中的验证

1、所谓解析就是把常量池中的符号引用转换成直接引用的过程,包括:符号引用:以一组无歧义的符号来描述所引用的目标,与虚拟机无关

2、直接引用:直接指向目标的指针、相对偏移量、或是能间接定位到目标的句柄,是和虚拟机实现相关的

3、主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符

  • 类的初始化
    1、类的初始化就是为类的静态变量赋初始值,或者说是执行类构造器 方法的过程

①:如果类还没有加载和连接,就先加载和连接
②:如果类存在父类,且父类没有初始化,就先初始化父类(演示代码如下)

// 父类
public class MyParent {

    static {
        System.out.println("my parent class init");
    }
}

// 子类
public class MyChild extends MyParent {

    static {
        System.out.println("my child class init");
    }
}

// 测试类
public class Test1 {

    public static void main(String[] args) {
        MyChild myChild = new MyChild();
    }
}

// 结果会发现先会先调用父类的静态方法,然后在调用子类的静态方法
com.capuda.jvm.classinit.Test1
my parent class init
my child class init

Process finished with exit code 0

3、如果类中存在初始化语句,就依次执行这些初始化语句
4、如果是接口的话

a、初始化一个类的时候,并不会先初始化它实现的接口
b、初始化一个接口时,并不会初始化它的父接口
c、只有当程序首次使用接口里面的变量或者是调用接口方法的时候,才会导致接口初始化

5、调用Classloader类的loadClass方法来装载一个类,并不会初始化这个类,不是对类的主动使用

  • 类的初始化时机
    1、Java程序对类的使用方式分成:主动使用和被动使用,JVM必须在每个类或接口 “首次主动使用” 时才初始化他们;被动使用类不会导致类的初始化,主动使用的情况:

1、创建类实例
2、访问某个类或接口的静态变量
3、调用类的静态方法
4、反射某个类

Class cls = Class.forName(“com.cc.jvm.classinit.MyChild”);

5、初始化某个类的子类,而父类还没有初始化
6、JVM启动的时候运行的主类(如下问所示)

// 当我们启动Test1 的main方法的时候 Test1就是主类也会被加载
public class Test1 {

    static {
        System.out.println("now Test1 class init");
    }

    public static void main(String[] args) {
        MyChild myChild = new MyChild();
    }
}


结果如下
在这里插入图片描述

7、定义default方法的接口,当接口实现类初始化时,例如下文,有调用某个人继承了Api但是没有调用Api接口中的东西,则也会对其初始化

public interface Api {
    
    public default void t3() {
        System.out.println("now in api t3()");
    }
}
  • 示例代码
public class MyClassA {

    private static MyClassA myClassA = new MyClassA();

    private static int a = 0;
    private static int b;

    public MyClassA() {
        a++;
        b++;
    }

    public static MyClassA getInstance() {
        return myClassA;
    }

    public int getA() {
        return a;
    }

    public int getB() {
        return b;
    }
}
public class Test1 {

    static {
        System.out.println("now Test1 class init");
    }

    public static void main(String[] args) {
        MyClassA myClassA = MyClassA.getInstance();
        System.out.println("myClassA.a == " + myClassA.getA());
        System.out.println("myClassA.a == " + myClassA.getB());
    }
}

结果如下;出现下面的代码的原因:当我们调用MyClassA的getInstance方法就是对MyClassA进行初始化,会调用MyClassA构造方法中的 a++和b++,此时 a=1,b=1;之后又会执行private static int a = 0;private static int b;两个赋值操作,a被重新赋值为0,而b没有做任何操作所以最好结果为 a = 0;b = 1;如果MyClassA构造方法和private static int a = 0;private static int b;两个赋值操作换个位置最终结果 a=1,b=1
在这里插入图片描述
被动使用
1、子类调用父类的静态方法不会触发子类的初始化
2、通过数组定义应用类也不会触发类的初始化

MyChild[] mcs = new MyChild[2];

3、访问类中的常量,该常量被final修饰,也不会触发类的初始化,因为被final修饰后在初始化前就会将对应的常量存入常量池中

public final static String childStr = “now in MyChild!!!”;

  • 类的卸载
    1、当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载(这个有JVM虚拟机操作,不用我们操)
    2、JVM 自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载
四、内存分配
  • JVM的简化架构

1、当一个类通过类装载器(ClassLoader)装载到虚拟机内存区域的方法区中,
在这里插入图片描述

  • 运行时数据区
    一、包括:PC寄存器、Java虚拟机栈、Java栈、方法区、运行时常量池、本地方法栈等

1、PC(Program Counter)寄存器说明:

1:每个线程拥有一个PC寄存器,是线程私有的,用来存储指向下一条指令的地址
2:在创建线程的时候,创建相应的PC寄存器
3:执行本地方法时,PC寄存器的值为undefined
4:是一块较小的内存空间,是唯一一个在JVM规范中没有规定OutOfMemoryError的内存区域

2、Java栈(他描述的是java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储像:局部变量表、操作:数栈、动态连接、方法出口等等信息)

1:栈由一系列帧(Frame)组成(因此Java栈也叫做栈帧),是线程私有的
2:帧是用来保存一个方法的局部变量、操作数栈(Java没有寄存器,所有参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等
3:每一次方法调用创建一个帧,并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁
4:局部变量表存放了编译器可知的各种基本数据类型和引用类型,每个slot存放32位的数据,long、double占两个槽位
5:栈的优点:存取速度比堆快,仅次于寄存器
6:栈的缺点:存在栈中的数据大小、生存期是在编译期决定的,缺乏灵活性

3、Java堆

1:用来存放应用系统创建的对象和数组,所有线程共享Java堆
2:GC主要就管理堆空间,对分代GC来说,堆也是分代的(该分代只是一个思想,好管理,为了迎合垃圾回收的分代算法,并不是一定要分代,对于内存来说本身没有分代这一说)
3:堆的优点:运行期动态分配内存大小,自动进行垃圾回收;堆的缺点:效率相对较慢

4、方法区

1:方法区是线程共享的,通常用来保存装载的类的结构信息(例如:运行时的常量池、字段、方法的字节码、类和实例初始化用到的一些特殊方法等等)
2:通常和元空间关联在一起,但具体的跟JVM实现和版本有关
3:JVM规范把方法区描述为堆的一个逻辑部分,但它有一个别名称为Non-heap(非堆),应是为了与Java堆区分开

5、运行时常量池

1:是Class文件中每个类或接口的常量池表,在运行期间的表示形式,通常包括:类的版本、字段、方法、接口等信息
2:在方法区中分配
3:通常在加载类和接口到JVM后,就创建相应的运行时常量池

6、本地方法栈

1:在JVM中用来支持native方法执行的栈就是本地方法栈

  • 栈、堆、方法区交互关系
    1、运行期间有一个栈,存放着局部变量表例如:A、b、引用类型:user这个类型可定指向对应的实例,这个实例就是存放在堆上了,堆里面一般存放着我们自己创建的对象例如User对象包含下图中所示数据,其中user类中的源数据信息就会去方法区中寻找,堆上的User类的元数据信息就会指想方法区中User类定义的信息;
    在这里插入图片描述
  • Java 堆内存概述
    1、用来存放应用系统创建的对象和数组,所有线程共享Java堆(根据Java虚拟机规范的规定Java堆内存需要在逻辑上是连续的,物理上不需要,在实现的时候堆内存的大小可以是固定的也可以是可扩展的,目前主流的虚拟机的堆都是可扩展的,如果在执行垃圾回收之后如果依然没有足够的内存来用于分配,也不能扩展了就会抛出OutOfMemory)

2、Java堆是在运行期动态分配内存大小,自动进行垃圾回收(在运行期间我们程序所需要的内存大小起初我们是不太好估计的,这些就交给虚拟机自动来完成,由它来判断运行期间你的堆大概是要用1个G还是800M,它回动态分配这个大小,不够的话它会去扩容,但是,是在一定范围内,这个是有我们设置的最大堆内存是在这个限度中)

3、Java垃圾回收(GC)主要就是回收堆内存(比如说一开始你需要的堆内存是1.5个G,跑了一段时间后,就只需要800M了,剩余的不被使用的700M就会被回收),对分代GC来说,堆也是分代(该分代只是一个思想,方便虚拟机对内存的管理,为了迎合垃圾回收的分代算法,并不是一定要分代)

  • Java 堆的结构
    1、Eden为新生代、From和To是存活区,两者相加就是新生代,Survivor Ratio用来设置存活区和Eden的比例
    2、当多次回收,回收不了的内容或者虚拟机判断需要存到老年代的内容他会从 To Space 转移到老年代中
    在这里插入图片描述
    3、新生代用来放新分配的对象;新生代中经过垃圾回收,没有回收掉的对象,被复制到老年代

4、老年代存储对象比新生代存储对象的年龄大得多

5、老年代存储一些大对象

6、整个堆大小 = 新生代 + 老年代

7、新生代 = Eden + 存活区

8、从前的持久代,用来存放Class、Method等元信息的区域,从JDK8开始去掉了,取而代之的是元空间(MetaSpace),元空间并不在虚拟机里面,而是直接使用本地内存

  • 对象的内存布局
    1、对象在内存中存储的布局(这里以HotSpot虚拟机为例来说明),分为:对象头、实例数据和对齐填充

2、对象头,包含两个部分:

①:Mark Word:存储对象自身的运行数据,如:HashCode、GC分代年龄、锁状态标志等
②:类型指针:对象指向它的类元数据指针

3、实例数据

真正存放对象实例数据的地方

4、对其填充

这部分不一定存在,也没有什么特别含义,仅仅是占位符。因为HotSpot要求对象起始地址都是8字节的整数倍,如果不是,就对齐

  • 对象的方位定位
    1、对象的访问定位

在JVM规范中只规定了reference类型是一个指向对象的引用,但没有规定这个引用具体如何去定位、访问堆中对象的具体位置

2、因此对象的访问方式取决于JVM实现,目前主流的有:使用句柄 或 使用指针两种方式

3、使用句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储句柄的地址,句柄中存储对象的实例数据和类元数据的地址,如下图所示:

在Java栈的本地变量表里面 reference(是一个引用对象的指针)他指向堆中的一个句柄池(在堆中划分出了一个区域叫句柄池)一个方框是一个句柄,一个句柄中放着两个指针,一个是到实例池中的对象实例数据的指针,另一个世道方法区的对象类型数据
优点:对象被移动的时候,只需要修改句柄池中实例数据的指针而reference不需要被修改(数据间接引用,不用直接去访问对应的数据源而是通过句柄);缺点:运行速度慢,需要两次指针定位才能找到具体的数据
在这里插入图片描述
4、使用指针:Java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址,如下图所示:
在Java栈的本地变量表里面 reference(是一个引用对象的指针)他直接指向堆内存空间的对象的实例,在对象实例里有着对象头,在对象头中存放着到对象类型的指针直接指向方法区的对象类型元数据
优点:速度快,相对于句柄的方式少了一次定位的开销
在这里插入图片描述

  • Java内存分配的参数
    Oracle官网地址

  • Trace 跟踪参数
    1、可以打印GC的简要信息:-Xlog:gc
    2、打印GC详细信息:-Xlog:gc*
    3、指定GC log位置,以文件输出:-Xlog:gc:garbage-collection.log(这个文件位置需要体提前创建好,他不会给你创建)
    4、每一次GC,都打印堆信息:-Xlog:gc+heap=debug
    在这里插入图片描述

  • GC日志格式
    1、GC发生的时间,也就是JVM从启动以来经过的秒数
    2、日志级别信息(debug,info),和日志类型标记(GC的类型())
    3、GC识别号(跑起来之后,他会标记这是触发的第几次GC)
    4、GC类型(明确的告诉你是发生在新生代还是Full GC)和说明GC的原因(简要的描述和触发GC的原因)
    5、容量:GC前容量->GC后容量(该区域总容量)
    6、GC持续时间,单位秒。有的收集器会有更详细的描述,比如:user表示应用程序消耗的时间,sys表示系统内核消耗的时间、real表示操作从开始到结束的时间

  • Java堆的参数
    1、Xms:初始堆大小,默认物理内存的1/64(JDK13现在要求该值必须是1024的倍数,并且大于1M)

// Orcal官网内容
-Xms size

    Sets the minimum and initial size (in bytes) of the heap. This value must be a multiple of 1024 and greater than 1 MB. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. The following examples show how to set the size of allocated memory to 6 MB using various units:

    -Xms6291456
    -Xms6144k
    -Xms6m

    Instead of the -Xms option to set both the minimum and initial size of the heap, you can use -XX:MinHeapSize to set the minimum size and -XX:InitialHeapSize to set the initial size.

    If you don't set this option, the initial size is set as the sum of the sizes allocated for the old generation and the young generation. The initial size of the heap for the young generation can be set using the -Xmn option or the -XX:NewSize option.

2、Xmx:最大堆大小,默认物理内存的1/4(JDK13现在要求该值必须是1024的倍数,并且大于2M)

// Orcal官网内容
-Xmx size

    Specifies the maximum size (in bytes) of the heap. This value must be a multiple of 1024 and greater than 2 MB. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, or g or G to indicate gigabytes. The default value is chosen at runtime based on system configuration. For server deployments, -Xms and -Xmx are often set to the same value. The following examples show how to set the maximum allowed size of allocated memory to 80 MB using various units:

    -Xmx83886080
    -Xmx81920k
    -Xmx80m

    The -Xmx option is equivalent to -XX:MaxHeapSize.

4、Xmx:最大堆大小 和 Xms:初始堆大小 这两个值配置的时候一般配置值相等,这样做的好处就避免每次GC过后就去的调整堆的大小

5、获取本机的内存数据

public class memoryTest1 {
	// 通过下面的方法获取对应内存信息
    public static void main(String[] args) {
        System.out.println("totalMemory ===" + Runtime.getRuntime().totalMemory()/1024/1025 + "M");
        System.out.println("freeMemory ===" + Runtime.getRuntime().freeMemory()/1024/1025 + "M");
        System.out.println("maxMemory ===" + Runtime.getRuntime().maxMemory()/1024/1025 + "M");
    }
}

// 结果
totalMemory ===245M
freeMemory ===240M
maxMemory ===3621M

6、修改Xmx和Xms的值
在这里插入图片描述在这里插入图片描述

7、再次运行结果如下

totalMemory ===9M
freeMemory ===7M
maxMemory ===9M

8、JDK13默认使用的垃圾回收器是G1,G1的特点:需要消耗一部分的内存来提高他的速度,正常来说他会消耗掉10%的内存多的会达到20%

9、-XX:MinHeapSize(最小堆内存大小); -XX:InitialHeapSize(初始化堆内存大小)我们也可以使用这两个参数替换 -Xms,但是在设置的时候要主要 -XX:MinHeapSize的值要小于等于 -XX:InitialHeapSize的值,否则会报错

10、Xmn:新生代大小,默认整个堆的3/8(新生代的大小十分重要,如果设置的过小,就会经常触发垃圾回收,如果设置的过大好像所有对象都能装的下,最后导致仅仅执行Full GC,这个就需要很长时间完成,也会导致虚拟机停顿,如果使用的是G1收集器,就不要设置新生代的大小,就由系统自动内内存进行分配和管理,设置新生代大小不要查过初始堆内存大小的一半,不然会出现莫名的问题)

// Orcal官网内容
-Xmn size

    Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery) in the generational collectors. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, or g or G to indicate gigabytes. The young generation region of the heap is used for new objects. GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. It is recommended that you do not set the size for the young generation for the G1 collector, and keep the size for the young generation greater than 25% and less than 50% of the overall heap size for other collectors. The following examples show how to set the initial and maximum size of young generation to 256 MB using various units:

    -Xmn256m
    -Xmn262144k
    -Xmn268435456

    Instead of the -Xmn option to set both the initial and maximum size of the heap for the young generation, you can use -XX:NewSize to set the initial size and -XX:MaxNewSize to set the maximum size.

9、现在默认使用的G1垃圾回收器,但是该收集器对垃圾回收器新生代和老年代划分的并不清晰,他是使用的是分区,会将内存区分成很对块,块并不固定是老年代还是新生代,根据需要来,这个块如果挂在了新生代上他就是新生代

10、如果我们想清楚的看到新生代,老年代,from,to这些区域需要切换cms垃圾回收器,需要进行如下配置:-XX:+UseConcMarkSweepGC

在这里插入图片描述堆日志对应信息
在这里插入图片描述

11、-XX:+HeapDumpOnOutOfMemoryError:OOM时导出堆到文件;-XX:+HeapDumpPath:导出OOM的路径
在这里插入图片描述

12、内存泄漏出现的原因:1、程序配置存在问题,导致对象使用内存的空间持续上涨而不会被释放即不会被垃圾回收器回收掉 2、在运行期间的确有这么多的对象,就是要在内存中就是要存这么多东西,但是内存分配参数过小;如果是内存泄漏就要去具体分析是哪个点出现问题,由于我一个不恰当的引用,导致我这个引用的对象持有了大量的对象,由于我在引用它,但是我对他有没有使用,就导致他所持有的大量对象不会被释放掉,这个内存就会越来越多,例如:Map,List等集合对象,需要找到具体的原因去找到对应的问题,如果是内存分配过小,就会比较容易处理,只需调整对应的参数即可,或者确认是否真的需要这么多对象

13、使用造成OOM问题的代码

public class memoryTest1 {

    private byte[] bs = new byte[1024 * 1024];

    public static void main(String[] args) {

        List<memoryTest1> list = new ArrayList<memoryTest1>();

        int num = 0;
        try {
            while (true) {
                list.add(new memoryTest1());
                num++;
            }
        } catch (Throwable err) {
            System.out.println("now error, num ==" + num);
            err.printStackTrace();
        }

        System.out.println("totalMemory ===" + Runtime.getRuntime().totalMemory()/1024.00/1024.00 + "M");
        System.out.println("freeMemory ===" + Runtime.getRuntime().freeMemory()/1024.00/1024.00 + "M");
        System.out.println("maxMemory ===" + Runtime.getRuntime().maxMemory()/1024.00/1024.00 + "M");
    }
}

14、由于我们配置了 -XX:+HeapDumpOnOutOfMemoryError 参数,如果出现OOM问题,我们就会出现下面的文件,在这里插入图片描述15、之后通过JDK自带的jvisualvm.exe分析hprof来获取定位OOM的问题思路
在这里插入图片描述
16、打开后的界面
在这里插入图片描述

17、使用trace跟踪参数:-Xlog:gc+heap=debug:mygc.log ,将对应的堆日志打印出来
在这里插入图片描述

  • Java堆的参数
    1、-XX:NewRatio:老年代与新生代的比值;如果xms=xmx,且设置了xmn的情况下,该参数不用设置

例如:-XX:NewRatio=3;老年代占3成,3/4;新生代占1成 1/4

2、-XX:SurvivorRatio:Eden 区和 Survivor 区的大小比值,设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor占整个新生的1/10(eden占8,from和to各占1)

3、-XX:+HeapDumpOnOutOfMemoryError :OOM时导出堆到文件

4、-XX:+HeapDumpPath:导出OOM的路径

5、-XX:OnOutOfMemoryError:在OOM时,执行一个脚本

6、当存储的文件大小打过eden去的大小会直接存到老年代

  • Java栈的参数
    1、-Xss:通常只有几百K,决定了函数调用深度(Java栈中存放的是和我们运行期间相关的一些局部变量表的一些东西,深度即可以调多少次)

2、测试代码如下,解决方案就是要找好对应的退出方案,不要一直递归下去

public class Test2 {

    private int num = 0;

    private int callMe(int a, int b) {
        num++;
        return callMe(a + num, b);
    }

    public static void main(String[] args) {
        Test2 t = new Test2();
        try {
            t.callMe(1, 2);
        } catch (Throwable err) {
            System.out.println("num ==" + t.num);
            err.printStackTrace();
        }
    }
}

当我们设置-xss参数为 1m的时候,会发现执行次数为25179,就出现了StackOverflowError错误
在这里插入图片描述
在这里插入图片描述
当我们设置-xss参数为 2m的时候,会发现执行次数为98183,就出现了StackOverflowError错误
在这里插入图片描述

  • 元空间的参数
    1、-XX:MetaspaceSize:初始空间大小(设置了这个值之后,当达到这个之后就会触发垃圾回收机制进行类型卸载,同时GC也会对这个值进行调整,如果释放大量空间就需要降低该值,如果释放很少空间超过一个最大值的时候就需要适当提高该值)

2、-XX:MaxMetaspaceSize:最大空间,默认没有限制的

3、-XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比

4、-XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比

5、字节码执行引擎
  • 字节码执行引擎概述
    1、概述:JVM的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果

2、实现方式:可能有通过解释器直接解释执行字节码,或者是通过即使编译器产生本地代码,也就是编译执行(将字节码编译成机器码,然后直接执行机器码,这种方式在运行时动态编译,编译过后再去执行,对于性能来说有着数量级的提升,比解释执行快得多),当然也可能两者皆有

  • 栈帧、运行期操作数栈和局部变量表之间的交互关系
    1、栈帧概述:

1:栈帧是用于支持JVM进行方法调用和方法执行的数据结构
2:栈帧随着方法调用而创建,随着方法结束而销毁
3:栈帧里面存储了方法的局部变量、操作数栈、动态连接、方法返回地址等信息

在这里插入图片描述
2、局部变量:用来存放方法参数和方法内部定义的局部变量的存储空间

1:以变量槽slot为单位,目前一个slot存放32位以内的数据类型
2:对于64位的数据占2个slot
3:对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表
4:然后根据方法体内部定义的变量顺序和作用域来分配slot
5:slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为

实现代码 - 实例方法

public class Test1 {

    public int add(int a, int b) {
        int c = a + b;
        return a + b + c;

        // slot
        // 0--this
        // 1--a
        // 2--b
        // 3--c
    }
}

之后通过反编译target下的.class文件,得到下面的内容
在这里插入图片描述

 public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code: // 下面是执行的指令集(执行顺序)
      stack=2, locals=4, args_size=3
         0: iload_1 // 后面_1 指的是槽位得到索引,即加载局部变量表的1,即把 a 装载到操作出栈的栈顶
         1: iload_2 // 后面_2 指的是槽位得到索引,即加载局部变量表的2,即把 b 装载到操作出栈
         2: iadd	// 执行 add方法
         3: istore_3	// 将执行完的值复制到 _3中即 c
         4: iload_1
         5: iload_2
         6: iadd
         7: iload_3
         8: iadd
         9: ireturn
      LineNumberTable:
        line 10: 0
        line 11: 4
      LocalVariableTable: // 本地变量表(局部变量表)
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/capuda/jvm/execute/Test1; // Slot 0存放的是this(执行Test1的实例)
            0      10     1     a   I // Slot 1存放的是 a
            0      10     2     b   I // Slot 2存放的是 b
            4       6     3     c   I // Slot 3存放的是 c

实现代码 - 静态方法

public class Test2 {

    public static int add(int a, int b) {
        int c = a + b;
        return a + b + c;

        // slot
        // 0--a
        // 1--b
        // 2--c
    }
}

之后通过反编译target下的.class文件,得到下面的内容

 public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_0
         5: iload_1
         6: iadd
         7: iload_2
         8: iadd
         9: ireturn
      LineNumberTable:
        line 10: 0
        line 11: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0     a   I
            0      10     1     b   I
            4       6     2     c   I
}

slot 的复用,代码实现

public static void main(String[] args) {
        System.out.println("totalMemory ===" + Runtime.getRuntime().totalMemory()/1024.00/1024.00 + "M");
        System.out.println("freeMemory ===" + Runtime.getRuntime().freeMemory()/1024.00/1024.00 + "M");
        System.out.println("maxMemory ===" + Runtime.getRuntime().maxMemory()/1024.00/1024.00 + "M");
    }

// 内存的结果
totalMemory ===7.75M
freeMemory ===5.955513000488281M
maxMemory ===7.75M


// 创建一个局部变量,之后触发gc。代码如下
public static void main(String[] args) {
        {
            byte[] bs = new byte[2 * 1024 * 1024];
        }
        System.gc();

        System.out.println("totalMemory ===" + Runtime.getRuntime().totalMemory()/1024.00/1024.00 + "M");
        System.out.println("freeMemory ===" + Runtime.getRuntime().freeMemory()/1024.00/1024.00 + "M");
        System.out.println("maxMemory ===" + Runtime.getRuntime().maxMemory()/1024.00/1024.00 + "M");
    }

// 内存结果
totalMemory ===7.75M
freeMemory ===4.7498321533203125M
maxMemory ===7.75M

// 问题点:正常来说,当bs块钟执行完毕后应该是引用结束了,在gc后内存应该被回收掉
// 解析:上面的存储槽位关系:
// slot
// 0--args
// 1--bs --- 堆的空间,放着2M的数据  局部变量表中的bs指向者堆中,所以不会被gc清除(复用性,当执行完毕后,在局部变量表中不会被清除)
// 验证方式:我们可以在块文件的外面在创建一个局部变量,块文件中的局部变量使用完后,重新常见一个变量,该变量会替换掉 bs 在slot位置。实现代码如下,a 会覆盖bs的槽位,之后就没有bs指向堆了,之后就可以被清理掉了
    public static void main(String[] args) {
        {
            byte[] bs = new byte[2 * 1024 * 1024];
        }
        int a = 5;
		// 现在的slot槽位是
		// slot
		// 0--args
		// 1--a

        System.gc();

        System.out.println("totalMemory ===" + Runtime.getRuntime().totalMemory()/1024.00/1024.00 + "M");
        System.out.println("freeMemory ===" + Runtime.getRuntime().freeMemory()/1024.00/1024.00 + "M");
        System.out.println("maxMemory ===" + Runtime.getRuntime().maxMemory()/1024.00/1024.00 + "M");
    }

// 除了上面的槽位覆盖让其进行回收也可以使用下面的方法将值置为null,未将将值置为null他指向堆的指针也被清除了;代码如下
public static void main(String[] args) {
        {
            byte[] bs = new byte[2 * 1024 * 1024];
            bs = null;
        }
     

        System.gc();

        System.out.println("totalMemory ===" + Runtime.getRuntime().totalMemory()/1024.00/1024.00 + "M");
        System.out.println("freeMemory ===" + Runtime.getRuntime().freeMemory()/1024.00/1024.00 + "M");
        System.out.println("maxMemory ===" + Runtime.getRuntime().maxMemory()/1024.00/1024.00 + "M");
    }

2、操作数栈:用来存放方法运行期间,各个指令操作的数据(入栈和出栈的过程)

1:操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
2:虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠区域,以存放公用数据

public class Test2 {

    public int add(int a, int b) {
        int c = a + b;
        return a + b + c;

        // slot
        // 0--a
        // 1--b
        // 2--c
    }

    public static void main(String[] args) {
        Test2 t = new Test2();
        int ret = t.add(1, 2);
        System.out.println(ret);
    }
}

// main 方法
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #2   // 初始化类                // class com/capuda/jvm/execute/Test2
         3: dup
         4: invokespecial #3       // 调用构造方法          // Method "<init>":()V
         7: astore_1		       // 给 t 赋值
         8: aload_1				   // 将 t 装载进来
         9: iconst_1		       // 常量 1
        10: iconst_2			   // 常量 2
        11: invokevirtual #4      // 调用 add方法           // Method add:(II)I
        14: istore_2
        15: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        22: return
      LineNumberTable:
        line 21: 0
        line 22: 8
        line 23: 15
        line 35: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1     t   Lcom/capuda/jvm/execute/Test2;
           15       8     2   ret   I
}

// add方法
 public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1						
         1: iload_2					
         2: iadd
         3: istore_3
         4: iload_1
         5: iload_2
         6: iadd
         7: iload_3
         8: iadd
         9: ireturn
      LineNumberTable:
        line 10: 0
        line 11: 4
      LocalVariableTable: // 局部变量表
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/capuda/jvm/execute/Test2;
            0      10     1     a   I
            0      10     2     b   I
            4       6     3     c   I

		// 解析
		// 操作数栈
        6:a + b =3 --- c = c
        5:b = 2
        4:a = 1  // 从这里开始执行的是return中的加法
        3:a + b =3 --- c = c
        2:b = 2
        1:a = 1

        0: iload_1
        1: iload_2
        2: iadd
        3: istore_3
        4: iload_1
        5: iload_2
        6: iadd
        7: iload_3
        8: iadd
        9: ireturn

3、动态连接:每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程的动态连接

1:静态解析:类加载的时候,符号引用就转化成直接引用
2:动态连接:运行期间转换为直接引用

4、方法返回地址:方法执行后返回的地址;例如上面的字节码调用add方法后会返回往下执行 istore_2等指令

        11: invokevirtual #4      // 调用 add方法           // Method add:(II)I
        14: istore_2
        15: getstatic     #5       

4、方法调用:方法调用就是确定具体调用哪一个方法(即是调用A类中的t方法还是调用B类中的 t方法),并不涉及方法内部执行过程

1:部分方法是直接在类加载的解析阶段,就确定了直接引用关系(例如:静态方法、私有方法、实例构造器、父类方法)
2:但是对于实例方法,也称虚方法,因为重载和多态,需要运行期动态委派

  • 方法调用、静态分派和动态分派

1、分派:又分成静态分派和动态分派

1:静态分派:所有依赖静态类型来定位方法执行版本的分派方式,比如:重载方法
2:动态分派:根据运行期的实际类型来定位方法执行版本的分派方式,比如:覆盖方法(例如实现接口,覆盖对应接口的方法)

2、单分派和多分派:就是按照分派思考的纬度,多余一个的就算多分派,只有一个的称为单分派

当我们有一个Test父类,Test1继承Test,当Test和Test1 有两个重载方法,当我们调用的时候首先会判断是调用子类还是父类,如果是调用父类又要判断调用的是哪个方法(因为重载方法方法名一样,传入的参数类型不一样,所以这里两个维度;我们调用重载方法只有一个维度)

3、如何执行方法中的字节码指令:JVM通过基于栈的字节码解释执行引擎来执行指令,JVM的指令集也是基于栈的

6、垃圾回收
  • 垃圾回收概述

1、什么是垃圾:简单说就是内存中已经不再被使用到的内存空间就是垃圾

2、引用计数法:给对象添加一个引用计数器,有访问就加1,引用失效就减1

优点:实现简单、效果高
缺点:不能解决对象之间循环引用的问题

  • 根搜索算法(可达性分析算法)

1、从根(GC Roots)节点向下搜索对象节点,搜索走过的路径称为引用链,当一个对象到根之间没有连同的话,则该对象不可用
在这里插入图片描述
2、可作为GC Roots的对象包括:虚拟机栈(栈帧局部变量)中引用的对象、方法区类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象、所有被同步锁持有的对象(synchorinzed)、反应虚拟机内部情况的JMXbean,本地的代码缓存所持有的对象

3、由于我们在做的程序越来越大,我们的类和实例对象也会越来越多,如果虚拟机在运行期间都要根据这个根搜索算法从GC Roots 开始往下一棵树一棵树的找,判断对象是否可用的话,对性能消耗会十分巨大执行起来也会很缓慢,也需要额外的空间来构建根搜索算法;从实际出发是难以接受的;HotSpot使用了一组叫做OopMap(描述对象之间引用关系的一种数据结构,在类加载完成的时候虚拟机就会计算出当前对象在那个偏移位置上会有什么样的引用,都会记录在OopMap里面)的数据结构达到准确是GC的目的;如果需要GC需要知道哪些对象不再被引用了,哪些是垃圾就可以直接扫描OopMap就可以了

4、在OopMap的协助下,JVM可以很快的做完GC Roots枚举。但是JVM并没有为每一条指令生成一个OopMap(这个需要非常多的额外空间,而且会增加GC的成本)

5、记录OopMap的这些 “特定位置” (记录对象引用的信息)被称为安全点,即当前线程执行到安全点后才允许暂停进行GC

6、如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方开始GC都是安全的,那么这个区域称为安全区域

  • 引用分类

1、强引用:类似于Object a = new A()这样的,不会被回收

2、软引用:还有用但并不必须的对象,用SoftReference来实现软引用(如果在内存足够的情况下这些对象会存在对象中,当垃圾回收后内存还不够还需要释放对象的话,软引用这些可以被回收掉,软引用会被当做回收的第二梯队,回收后内存还不够则发生内存溢出)

3、弱引用:非必须对象,比软引用还要弱,垃圾回收时会回收掉。用WeakReference来实现弱引用

4、虚引用:也称为幽灵引用或幻影引用,是最弱的引用。垃圾回收时会回收掉,用PhantomReference来实现虚引用


// 这里的代码可以掩饰三个类型:SoftReference、WeakReference、PhantomReference三个类型的被gc的顺序,我们可以通过修改User类中的bs的参数例如:1 * 1024增加到 2 * 1024,将对应的大小的bs存入内存营造内存消耗完的场景来验证上面三个类型的gc顺序,参数修改有个临界值可以看到执行的顺序

public class User {

    private byte[] bs = new byte[1* 1024];

    private String userId;

    public User(String userId) {
        this.userId = userId;
    }

    @Override
    public String toString() {
        return "User{" +
                "userId='" + userId + '\'' +
                '}';
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("now finalize userId ===" + userId);
    }
}

public class ReferenceType {

    private static ReferenceQueue<User> rq = new ReferenceQueue<>();

    private static void printQueue(String str) {
        Reference<? extends User> obj = rq.poll();
        if (obj != null) {
            System.out.println("the gc Object reference" + str + "=" + obj.get());
        }
    }

    private static void testSoftReference() throws InterruptedException {
        List<SoftReference<User>> list = new ArrayList<SoftReference<User>>();

        for (int i = 0; i < 10; i++) {
            SoftReference<User> sr = new SoftReference<User>(new User("soft" + i), rq);
            System.out.println("now the soft user ====" + sr.get());
            list.add(sr);
        }
        System.gc();
        Thread.sleep(1000L);
        printQueue("soft");
    }

    private static void testWeakReference() throws InterruptedException {
        List<WeakReference<User>> list = new ArrayList<WeakReference<User>>();

        for (int i = 0; i < 10; i++) {
            WeakReference<User> sr = new WeakReference<User>(new User("weak" + i), rq);
            System.out.println("now the weak user ====" + sr.get());
            list.add(sr);
        }
        System.gc();
        Thread.sleep(1000L);
        printQueue("weak");
    }

    private static void testPhantomReference() throws InterruptedException {
        List<PhantomReference<User>> list = new ArrayList<PhantomReference<User>>();

        for (int i = 0; i < 10; i++) {
            PhantomReference<User> sr = new PhantomReference<User>(new User("weak" + i), rq);
            System.out.println("now the weak user ====" + sr.get());
            list.add(sr);
        }
        System.gc();
        Thread.sleep(1000L);
        printQueue("Phantom");
    }

    public static void main(String[] args) throws InterruptedException {
        testSoftReference();
        testWeakReference();
        testPhantomReference();
    }
}

  • 跨代引用

1、跨代引用:也就是一个代中的对象引用另外一个代中的对象(老年代引用新生代中的对象或者新生代引用了老年代的对象)
2、跨代引用假说:跨代引用相对于同代引用来说只是极少数
3、隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的

  • 记忆集
    1、记忆集(Remembered Set):一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构(建立了一个全局的数据结构,这个结构将老年代或新生代划分成若干个小块,然后标识出哪一块存在跨代引用)

2、字长精度:每个记录精确到一个机器字长,该字包含跨代指针

3、对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针

4、卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针

5、卡表(Card Table):是记忆集的一种具体实现,定义了记忆集的记录精度和与堆内存的映射关系等

6、卡表的每个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块称为 卡页(Card Page) (通常一个卡页的内存当中包含不止一个对象,只有这个里面任意一个对象的字段存在跨代指针的话,就将对应这个卡表的值就标识为 1 ,称之为 变章,在进行GC的时候讲变章的加入到GC Root中一并扫描,其他的就不用管了)

  • 写屏障

1、写屏障可以看成是JVM对 “引用类型字段赋值” 这个动作的AOP

2、通过写屏障来实现当对象状态改变后,维护卡表状态

  • 判断是否垃圾的步骤

1、根搜索算法判断不可用

2、看是否有必要执行finalize方法

// finalize自救的代码
public class HelpSelf {

    private static HelpSelf hs = new HelpSelf();

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("now in finalize ===");
        // 自救
        hs = this;
    }

    public static void main(String[] args) throws InterruptedException {
        hs = new HelpSelf();

        // 第一次
        hs = null;
        System.gc();
        Thread.sleep(1000L);
        System.out.println("first hs ====" + hs);

        // 第二次
        hs = null;

        System.gc();
        Thread.sleep(1000L);

        System.out.println("Sedond hs ===" + hs);

    }
}

3、两个步骤走完后对象仍然没有人使用,那就属于垃圾

  • GC类型

1、MinorGC/YoungGC:发生在新生代的收集动作

2、MajorGC/OldGC:发生在老年代的GC,目前CMS收集器会有单独收集老年代的行为

3、MixedGC:收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为

4、FullGC:收集整个Java堆和方法区的GC

  • Stop-THe-World

1、STW是Java中一种全局暂停的现象,多半由于GC引起。所谓全局停顿,就是所有Java代码停止运行,native代码可以执行,但不能和JVM交互

2、其危害是长时间服务停止,没有响应;对于HA系统,可能引起主备切换,严重危害生产环境

  • 垃圾收集类型

1、串行收集:GC单线程内存回收、会暂停所有的用户线程,如:Serial

2、并行收集:多个GC线程并发工作,此时用户线程是暂停的,如:Parallel

3、并发收集:用户线程和GC线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,如:CMS

  • 判断类无用的条件

1、JVM中该类的所有实例都已经被回收

2、加载该类的ClassLoader已经被回收

3、没有任何地方引用该类的Class对象

4、无法在任何地方通过反射访问这个类

垃圾回收算法
  • 标记清除法

1、标记清除法(Mark-Sweep)算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象

在这里插入图片描述
2、优点是简单

3、缺点是:

1:效率不高,标记和清除的效率都不高(这是因为Java中可能会存在很多的对象,他的标记和清除的分离的,这样意味着它会产生很多次的标记和清除的动作,他会一块块的去标记然后一块块的去清除)
2:标记清除后会产生大量不连续的内存碎片,从而导致在分配大对象时触发GC(如上图回收后的图片可使用的空间是不连续,如果存放大容量的队形例如要存放5个小块但是我们却找不到五个连续的小块,则会触发GC)

  • 复制算法

1、复制算法(Copying):把内存分成两块完全相同的区域,每次使用其中一块,当一块使用完了,就把这块上还存活的对象拷贝到另外一块,然后把这块清除掉

在这里插入图片描述2、优点是:实现简单,运行高效,不用考虑内存碎片问题

3、缺点是:内存有些浪费

4、JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden和一块Survivor,回收时,把存活的对象复制到另一块Survivor

5、HotSpot默认的Eden和Survivor比是8:1,也就是每次能用90%的新生代空间

6、如果Survivor空间不够,就要依赖老年代进行分配担保,把放不下的对象直接进入老年代

  • 分配担保

1、分配担保是:当新生代进行垃圾回收后,新生代的存活区放置不下(存在两种情况;1:新生代使用的时候使用的是Eden区和一块Survivor区,当垃圾回收在新生代发生之后,那么在Eden区和Survivor区中存活的对象还比较多的话,剩下这些对象的空间超过另外一块Survivor区的大小;2:垃圾回收过后,要为新的对象去分配空间,但是这个新的对象要求的空间比较大导致新生代找不到这么大的一块空间用于存放这个对象),那么需要把这些对象放置到老年代区的策略,也就是老年代为新生代的GC做空间分配担保,步骤如下:

1:在发生MinorGG前,JVM会检查老年代的最大可用的连续空间,是否大于新生代所有对象的总空间,如果大于,可以确保MinorGC是安全的
2:如果小于,那么JVM会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小
3:如果大于,则尝试进行一次MinorGC
4:如果不大于,则改做一次Full GC

  • 标记整理法

1、标记整理算法(Mark-Compact):由于复制算法在存活对象比较多的时候,效率较低,且有空间浪费,因此老年代一般不会选用复制算法,老年代多选用标记整理算法

2、标记过程跟标记清除一样,但后续不是直接清除可回收对象,而是让所有存活对象都向一端移动,然后直接清除边界以外的内存

在这里插入图片描述

垃圾收集器
  • 概述

1、前面讨论的垃圾收集算法只是内存回收的方法,垃圾收集器就来具体实现这些算法并实现内存回收

2、不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含收集器如下图所示:

3、新生代:

Serial
ParNew
Parallel Scavenge
G1

4、老年代

CMS
Serial old
Parallel old
G1

5、新生代的Serial和老年代的Serial old的串行收集器配合使用;新生代的ParNew和老年代的CMS的并行收集器配合使用;新生代的Parallel Scavenge可以和老年代的Serial old和Parallel old两个收集器配合使用;当我们启用CMS收集器的时候可以做一个备用收集器Serial old,当CMS不能正常去工作的时候就会把老年代收集器切换成Serial old
在这里插入图片描述

  • 串行收集器
    1、Serial(串行)收集器/Serial Old收集器,是一个单线程的收集器,在垃圾收集时;会Stop-the-World

2、当多线程执行,需要GC的时候新生代会首先暂停所有线程,通过一个GC线程去进行垃圾回收,使用的算法是复制算法,结束后多线程继续运行,老年代同理,只不过垃圾回收算法是标记整理算法
在这里插入图片描述
3、优点是简单,对于单cpu,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器

4、使用-XX:+UseSerialGC来开启,会使用:Serial + Serial Old 的收集器组合(和上文一样配置Idea的VM选项中可以得到下文的内容)

5、新生代使用复制算法,老年代使用标记-整理算法

在这里插入图片描述

  • 并行收集器

1、ParNew(并行)收集器:使用多线程进行垃圾回收,在垃圾收集时,会 Stop-the-World

2、当多线程执行,需要GC的时候新生代会首先暂停所有线程,通过多个GC线程去进行垃圾回收,使用的算法是复制算法,结束后多线程继续运行,老年代同理,只不过垃圾回收算法配合CMS以及是标记整理算法

在这里插入图片描述

3、在并发能力好的CPU环境里,他停顿的时间要比串行收集器短;但对于单CPU或并发能力较弱的CPU,由于多线程的交互开销,可能比串行回收器更差

4、是Server模式下首选的新生代收集器,且能和CMS收集器配合使用

5、不再使用-XX:+UsePARNewGC来单独开启(可以通过CMS来开启:-XX:+UseConcMarkSweepGC)(JDK13)

在这里插入图片描述

6、-XX:ParallelGCThreads:指定线程数,最好与CPU数量一致

7、新生代使用复制算法

  • 新生代Parallel Scavenge 收集器

1、新生代Parallel Scavenge收集器/老年代 Parallel Old收集器:是一个应用于新生代的、使用复制算法的、并行收集器

2、跟ParNew很类似,但更关注吞吐量,能最效率的利用CPU,适合运行后台应用

3、当多线程执行,需要GC的时候新生代会首先暂停所有线程,通过多个GC线程去进行垃圾回收,使用的算法是复制算法,结束后多线程继续运行,老年代同理,只不过垃圾回收算法配合CMS以及是标记整理算法

在这里插入图片描述
4、使用-XX:+UseParallelGC

5、使用-XX:+UseParallelOldGC来开启老年代使用ParallelOld收集器,使用Parallel Scavenge + Parallel Old 的收集器组合(不管使用那个一都是这种组合)

在这里插入图片描述
在这里插入图片描述
6、-XX:MaxGCPauseMillis:设置GC的最大停顿时间(如果设置过小,会导致垃圾收集不完,然后造成频率的增加,如从五分钟一次变成了五分钟五次)

7、新生代使用复制算法,老年代使用标记-整理算法

  • CMS收集器

1、CMS(Concurrent Mark and Sweep 并发标记清除:GC线程和工作线程并行执行)收集器分为:初始标记:指标记GC Roots能直接关联到的对象(从根节点往下找一层他能直接关联到的对象);并发标记:进行GC Roots Tracing 的过程(一级节点往下开始查找,一直到树的结尾及叶子节点,查看哪些对象是活着哪些对象是垃圾)

2、重新标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象(因为这个是GC线程和工作线程一起执行在执行GC的时候可能有些对象又重新被调用了,有些对象又不再被使用了,将这些变化标记出来)

3、并发清除:并发回收垃圾对象

4、用户线程进行工作,导出SafePoint点进行初始标记(从根节点往下找一层,查看哪些对象根节点能直接访问到,标记成可用,访问不到的对象就标记成垃圾),继续向下运行,到达下一个SafePoint,进行并发标记(启用多个线程从一级节点向下进行查找,查看哪些对象不再被引用了,因为是并发标记所以会和用户线程混在一起运行),到达下一个SafePoint对前一个阶段标记的对象进行重新标记,看看前面并发运行的时候某些对象状态发生改变(是垃圾的变成了不是垃圾的,不是垃圾的变成了是垃圾的),标记完成后,到达下一个SafePoint则进行真正的垃圾处理(并发处理和用户线程混在一起运行),处理完成后到达下一个SafePoint用户线程接着跑,用户CMS线程会有一个重置线程,将前面收集的数据清空,等待下一次的垃圾收集

在这里插入图片描述
5、在初始标记和重新标记两个阶段还是会发生Stop-the-World

6、使用标记清除算法,多线程并发收集的垃圾收集器

7、最后的重置线程,指的是清空跟收集相关的数据并重置,为下一次收集做准备

8、优点:低停顿、并发执行

9、缺点:

1:并发执行,对CPU资源压力大;
2:无法处理在处理过程中产生的垃圾,可能导致FullGC
3:采用的标记清除算法会导致大量碎片,从而在分配大对象是可能触发FullGC

10、开启:-XX:+UseConcMarkSweepGC:使用ParNew + CMS + Serial Old 的收集器组合,Seria Old 将作为CMS出错的后备收集器

11、-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发回收,默认80%

  • G1收集器

1、G1(Garbage-First)收集器:是一款面向服务端应用的收集器,与其他收集器相比,具有如下特点:

1:G1把内存划分成多个独立的区域(Region)
2:G1仍采用分代思想,保留了新生代和老年代,但他们不再是物理隔离的,而是一部分Region的集合,且不需要Region是连续的(下图对应颜色就是对应的代区域,灰色的是free区,还有一个专门存储大对象的区域,当对象的大小大于Region一半大小的区域就认为是大对象,对象存储可以横跨多个Region)
3:G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW
4:G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
5:G1停顿可预测,能明确指定在一个时间段内,消耗在垃圾收集上的时间不能超过多长时间(该参数不能设置果断会增加GC频率)
6:G1跟踪各个Region里面垃圾堆的价值大小(回收那些块释放的区域最大),在后台维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限时间内的高效收集

在这里插入图片描述

3、跟CMS类似,也分为四个阶段:初始阶段:只标记GC Roots 能直接关联到的对象(和CMS的过程一致)

4、并发标记:进行GC Roots Tracing的过程(和CMS的过程一致)

5、最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象(和CMS的过程一致)

6、筛选回收:根据时间来进行价值最大化的回收

7、用户线程进行工作,导出SafePoint点进行初始标记(从根节点往下找一层,查看哪些对象根节点能直接访问到,标记成可用,访问不到的对象就标记成垃圾),继续向下运行,到达下一个SafePoint,进行并发标记(启用多个线程从一级节点向下进行查找,查看哪些对象不再被引用了,因为是并发标记所以会和用户线程混在一起运行),到达下一个SafePoint对前一个阶段标记的对象进行重新标记,看看前面并发运行的时候某些对象状态发生改变(是垃圾的变成了不是垃圾的,不是垃圾的变成了是垃圾的),标记完成后,到达下一个SafePoint进行筛选回收,根据时间来进行操作,如果时间来不及的话他就不会将判断为垃圾的全部回收,而是筛选价值最大化回收(释放空间最大的对象进行回收),结束后用户线程继续运行

在这里插入图片描述
8、新生代回收示意图:绿色Region块的是新生代,蓝色的是老年代,灰色的没有分配的空闲区
在这里插入图片描述
9、对下面标记的红圈新生代进行回收:1、Region直接被回收掉;2、他还有用将其拷贝到survivor区;3、或者将其从survivor区拷贝到老年代。

在这里插入图片描述10、将新生代存活的放置到survivor区,从空间去新开一个region用来存放新生代存活的对象

在这里插入图片描述11、G1是没有FullGC只能叫MixGC,每次老年代回收都会将新生代进行回收,然后进行部分老年代回收

在这里插入图片描述
12、在并发标记阶段,发现对应的两块老年代对象为空了进行标记(可以直接被清除)

在这里插入图片描述
13、最终标记阶段,之前判断为空的老年代就会直接被清除,做好新生代和老年代最后的标记

在这里插入图片描述
14、将对应标记应该回收的新生代和老年代进行回收,然后将存活下来的通过复制清除算法进行操作

在这里插入图片描述
15、将存活下来的从空间区开辟一块新的Region存放这些存活的对象

在这里插入图片描述

16、使用和配置G1:-XX:+UseG1GC:开启G1,默认就是G1

17、-XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间

18、-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45

19、-XX:NewRatio=n:默认为2;-XX:SurvivorRatio=n:默认为8

20、-XX:MaxTenuringThreshold=n:新生代到老年代的岁数,默认是15(CMS默认是6)

21、-XX:ParallelGCThreads=n:并行GC的线程数,默认值会根据平台不同而不同

22、-XX:ConcGCThreads=n:并发GC使用的线程数

23、-XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%

24、-XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048区域

  • ZGC收集器

1、ZGC收集器:JDK11加入的具有实验性质的低延迟收集器

2、ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%

3、ZGC里面的新技术:着色指针 和 读屏障

  • GC性能指标

1、吞吐量 = 应用代码执行的时间/运行的总时间

2、GC负荷,与吞吐量相反,是 GC时间/运行的总时间

3、暂停时间,就是发生Stop-the-World的总时间

4、GC频率,就是GC在一个时间段发生的次数

5、反应速度,就是从对象成为垃圾到被回收的时间

6、交互式应用通常希望暂停时间越少越好越好

  • JVM内存配置原则

1、新生代尽可能设置大点,如果太小会导致(堆内存的3/8或者一半之间):

1:YGC次数更加频繁
2:可能导致YGC后的对象进入老年代,如果此时老年代满了,会触发FGC

2、对老年代,针对响应时间优先的应用:由于老年代通常采用并发收集器,因此其大小要综合考虑并发量和并发持续时间等参数

3、如果设置小了,可能会造成内存碎片,高回收频率会导致应用暂停

4、如果设置大了,会需要较长的回收时间

5、对老年代,针对吞吐量优先的应用:通常设置较大的新生代和较小的老年代,这样可以尽可能回收大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象

6、依据对象的存活周期进行分类,对象优先在新生代分配,长时间存活的对象进入老年代

7、根据不同代的特点,选取合适的收集算法:少量对象存活,适合复制算法;大量对象存活,适合标记清除或者标记整理

高效并发
  • Java内存模型

1、JCP定义了一种Java内存模型,以前是在JVM规范中,后来独立出来成为JSP-133(Java内存模型和线程规范修订)

2、内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象

3、Java内存模型主要关注JVM中把变量值存储到内存和从内存中取出变量值这样的底层细节

4、所有变量(共享的)都存储在主内存中,每个线程都有自己的工作内存;工作内存中保存该线程使用到变量的主内存副本拷贝(类似:堆内存为主内存,所有线程共享,栈就是工作内存为每个线程私有)

5、线程对变量的所有操作(读、写)都应该在工作内存中完成

6、不同线程不能相互访问工作内存,交互数据要通过主内存

在这里插入图片描述

  • 内存间的交互操作

1、Java内存模型规定了一些操作来实现内存间交互(指的是一个变量如何从主内存拷贝到工作内存,又如何从工作内存拷贝会主内存),JVM会保证它们是原子的

1:lock:锁定,把变量标识为线程独占,作用于主内存变量
2:unlock:解锁,把锁定的变量释放,别的线程才能使用,作用于主内存变量
3:read:读取,把变量从主内存读取到工作内存
4:load:载入,把read读取到的值放入工作内存的变量副本中(将对应的值赋给变量)
5:use:使用,把工作内存中一个变量的值传递给执行引擎(类似JVM中的字节码执行引擎)
6:assign:赋值,把从执行引擎接收到的值赋给工作内存里面的变量
7:store:存储,把工作内存中一个变量的值传递到主内存中
8:write:写入,把store进来的数据存放如主内存的变量中

2、执行顺序

1、当某个线程想使用主内存中的某个变量,对主内存中对应的变量进行Lock,标识这个变量为我某个线程独占了;
2、进行read将这个变量读到对应的工作内存中;
3、load将该变量赋值给工作把内存中对应的变量;
4、变量有值后就进入执行阶段(use)交于执行引擎执行;
5、使用完后当值发生改变将对应改变到的值赋给工作内存中的变量(assign);
6、将对应的值返回给主内存(store);
7、把变量真正复制给主内存中对应的变量;
8、当不在使用则unlock
在这里插入图片描述

  • 内存间交互操作的规则

1、不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但不保证连续执行,也就是说,read和load之间、store与write之间是可插入其他指令的

2、不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存

3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中

4、一个新的变量只能从主内存中 “诞生” ,不允许在工作内存中直接使用一个未被初始化的变量,也就是对一个变量实施use和store操作之前,必须执行过了assign和load操作

5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁

6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值

7、如果一个变量没有被lock操作锁定,则不允许对它执行unlock操作,也不能unlock一个被其他线程锁定的变量

8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)

  • 多线程中的可见性

1、可见性:就是一个线程修改了变量,其他线程可以知道

2、保证可见性的常见方法:volatile、synchronized、final(一旦初始化完成,其他线程就可见)

  • volatile

1、volatile基本上是JVM提供的最轻量级的同步机制,用volatile修饰的变量,对所有线程可见,即对volatile变量所做的写操作能立即反映到其它线程中

2、用volatile修饰的变量,在多线程环境下仍然是不安全的

// 示例代码
public class A {

    private int a;

    public void aPlus() {
        a++;
    }

    public int getA() {
        return this.a;
    }
}


public class A {

    private int a;

    public void aPlus() {
        a++;
    }

    public int getA() {
        return this.a;
    }
}
public class TestThread {

    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        Thread t1 = new Thread(new MyThread(a, "t11"));
        Thread t2 = new Thread(new MyThread(a, "t2"));

        t1.start();
        t2.start();

        // 主线程当t1和t2执行结束才会往下执行
        t1.join();
        t2.join();

        System.out.println("A.a ==" + a.getA());
    }
}
/**
正常来说结果应该是2000,但是实际运行结果如下,
所以证明使用volatile在多线程环境下仍然是不安全的,
解决方案还是在A类中的aPlus方法上加上synchorinized就可以了(这里不演示了)
*/

thread =t11is game over ========
thread =t2is game over ========
A.a ==1639

Process finished with exit code 0

3、volatile修饰的变量,是禁止指令重排优化的

4、适合使用volatile的场景:

1:运算结果不依赖变量的当前值
2:或者能确保只有一个线程修改变量的值

  • 指令重排

1、指令重排:指的是JVM为了优化,在条件允许的情况下,对指令进行一定的重新排列,直接运行当前能够立即执行的后续指令,避开获取下一条指令所需数据造成的等待

2、线程内串行语义,不考虑多线程间的语义

3、不是所有的指令都能重拍,比如:

写后读 a = 1;b = a; 写一个变量之后,再读这个位置
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量
读后写 a = b;b = 1;读一个变量之后,再写这个变量

4、以上语句不可重排,但是 a = 1;b = 2;是可以重排的

  • 指令重排的基本规则

1、程序顺序原则:一个线程内保证语义的串行性

2、volatile规则:volatile变量的写,先发生于读

3、锁规则:解锁(unlock)必然发生在随后的加锁(lock)前

4、传递性:A先于B,B先于C那么A必然先于C

5、线程的start方法先于它的每一个动作

6、线程的所有操作先于线程的终结(Thread.join())

7、线程的中断(interrupt())先于被中断线程的代码

8、对象的构造函数执行结束先于finalize()方法

// 指令重排的例子
public class TestThread1 {

    private static int x = 0;
    private static int y = 0;
    private static int a = 0;
    private static int b = 0;

    public static void main(String[] args) throws InterruptedException {
        // t1先运行完,然后t2再运行,a = 1, b = 2; x = 0, y = 1
        // t2先运行完,然后t1再运行,a = 1, b = 2; x = 2, y = 0
        // t1t2交叉,t1先部分运行,t2完整运行:a = 1, b = 2; x = 2, y = 1
        // t1t2交叉,t1先部分运行,t2部分运行:a = 1, b = 2; x = 2, y = 1
        // t1t2交叉,t2先部分运行,t1完整运行:a = 1, b = 2; x = 2, y = 1
        // t1t2交叉,t2先部分运行,t1部分运行:a = 1, b = 2; x = 2, y = 1

        // 重排的情况,可能会出现:x == 0 && y == 0

        for (int i = 0; i < 10000; i++) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 当停止后就t2先运行,我们加一个条件当过了8秒停止一次
                        if (System.currentTimeMillis() % 8 == 0) {
                            Thread.sleep(1);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 2;
                    y = a;
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            System.out.println(i + "x=" + x + ", y = " + y);

        }

    }
}

  • 多线程中的有序性

1、在本线程内,操作都是有序的

2、在线程外观察,操作都是无序的,因为存在指令重排 或 主内存同步延时

  • Java线程安全的处理方法

1、不可变是线程安全的(例如被Final修饰的变量)

2、互斥同步(阻塞同步):synchronized、java.util.concurrent.ReentrantLock。目前这两个方法性能已经差不多了,建议优先选用synchronized,ReentrantLock增加了如下特性:

1:等待可中断:当持有锁的线程长时间不释放锁,正在等待的线程可以选择放弃等待
2:公平锁:多个线程等待同一个锁时,须严格按照申请锁的时间顺序来获得锁(非公平锁则反之,默认使用公平锁)
3:锁绑定多个条件:一个ReentrantLock对象可以绑定多个condition对象,而synchronized是针对一个条件的,如果要多个,就得有多个锁
4:非阻塞同步:是一种基于冲突检查的乐观锁定策略,通常是先操作,如果没有冲突,操作就成功了,有冲突再采取其它方式进行补偿处理
5:无同步方案:其实就是在多线程中,方法并不涉及共享数据,自然也就无需同步了

  • 锁优化之自旋锁与自适应自旋

1、自旋:如果线程可以很快获得锁,那么可以不在OS(操作系统)层挂起线程,而是让线程做几个忙循环,这就是自旋

2、自适应自旋:自旋的时间不再固定,而是由前一次在同一个锁上的自旋锁时间和锁拥有者状态来决定

3、如果锁被占用时间很短,自旋成功,那么能节省线程挂起、以及切换时间,从而提升系统性能

4、如果锁被占用时间很长,自旋失败,会白白耗费处理器资源,降低系统性能

  • 锁优化之锁消除

1、在编译代码的时候,检测到根本不存在共享数据竞争,自然也就无需同步加锁了;通过-XX:+EliminateLocks来开启

2、同时要使用-XX:+DoEscapeAnalysis开启逃逸分析,所谓逃逸分析:

1:如果一个方法中定义的一个对象,可能被外部方法引用,称为方法逃逸
2:如果对象可能被其它外部线程访问,称为线程逃逸,比如赋值给类变量或者可以在其它线程中访问的实例变量(方法内存的变量被访问到了,正常来说一个类就是一个黑盒不会外部感知到,但是当我们创建了一些公共变量就会被外部线程访问到产生逃逸,如果一个类中都是局部变量所有的访问都是内部的就不能出现类似问题:例如实体类中的get,set方法;)
3:为什么在做锁消除的时候要开启逃逸分析:因为一旦出现逃逸,你就不能轻易将这个锁消除,因为逃逸了就意味着就能从外部访问这个线程中的资源和数据,在多线程的环境下我们不知道外部怎么来使用我们这些资源或者数据的我们该同步还是要同步不能轻易的将锁消除掉

  • 锁优化之锁粗化

1、通常我们都要求同步块要小,但是一系列的连续操作导致对一个对象反复的加锁和解锁,这会导致不必要的性能损耗。这种情况建议把锁同步的范围加大到整个操作序列

	// 示例:一系列的加锁和解锁
	public void t() {
        
        synchronized (this) {
            // 做一些操作
        }
        // 做其它的操作
        synchronized (this) {
            // 做一些操作
        }
        // 做其它的操作
        synchronized (this) {
            // 做一些操作
        }
        // 做其它的操作
        synchronized (this) {
            // 做一些操作
        }
        // 做其它的操作
    }
	// 示例:优化
	public void t() {

        synchronized (this) {
            // 做一些操作
        // 做其它的操作
            // 做一些操作
        // 做其它的操作
            // 做一些操作
        // 做其它的操作
            // 做一些操作
        }
        // 做其它的操作
    }

  • 锁优化之轻量级锁

1、轻量级是相对于传统锁机制而言,本意是没有多线程竞争的情况下,减少传统锁机制使用OS(操作系统层面)实现互斥所产生的性能损耗

2、其实现原理很简单,就是类似乐观锁的方式

3、如果轻量级锁失败,表示存在竞争,升级为重量级锁,导致性能下降(所以我们需要分析程序的环境如果是存在线程竞争的就直接使用重量级锁,而不用先使用轻量级锁当发生冲突再升级为重量级这样比直接使用重量级锁性能消耗的大;如果不存在线程竞争则直接使用轻量级锁)

  • 锁优化之偏向锁

1、偏向锁是在无竞争情况下,直接把整个同步消除,连乐观锁都不用,从而提高性能;所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程

2、只要没有竞争,获得偏向锁的线程,在将来进入同步块,也不需要做同步

3、当有其它线程请求相同的锁时,偏向模式结束(即当我们线程竞争所,当前线程就一直拿着所去只同步任务,不需要去获取锁和释放锁)

4、如果程序总大多数锁总是被多个线程访问的时候,也就是竞争比较激烈,偏向锁反而会降低性能(因为当一个线程持有锁,当运行结束也不会释放锁,直到出现竞争才会释放锁,但是竞争也不一定会真的释放,因为有多个线程抢占当出现真正的获取者才会真正的释放)

5、使用-XX:UseBiasedLocking来禁用偏向锁,默认开启

  • JVM中获取锁的步骤(锁的优化虚拟机会根据自己内部的算法选择在合适的锁优化手段)
    1、会先尝试偏向锁;然后尝试轻量级锁

2、再然后尝试自旋锁

3、最后尝试普通锁,使用OS(操作系统)互斥量在操作系统层挂起

  • 同步代码的基本规则

1、尽量减少锁持有的时间(这样可以尽快释放锁交给下一个线程,减少等待时间)

2、尽量减小锁的粒度(锁里面包含的内容最简的最必要的,更有利于功能的实现和减少锁持有的时间)

八、性能监控与故障处理工具

一、命令行工具:jps、jinfo、jstack、jmap、jstat、jstatd、jcmd
二、图形化工具:jconsole、jmc、visualvm

  • JVM监控工具的作用

1、对 jvm 运行期间的内部情况进行监控,比如:对jvm参数、CPU、内存、堆等信息的查看

2、辅助进行性能调优

3、辅助解决应用运行时的一些问题,比如:OutOfMemoryError、内存泄漏、线程死锁、锁争用、Java进程消耗CPU过高等等

  • jps

1、jps(JVM Process Status Tool):主要用来输出JVM中运行的进程状态信息,语法格式如下:jsp [options] [hostid]

2、hostid字符串的语法与URI的语法基本一致:[protocol:][//][:port][/servername],如果不指定hostid,默认为当前主机或服务器

在这里插入图片描述

  • jinfo
    1、打印给定进程或核心文件或远程调试服务器的配置信息。语法格式:jinfo [ option ] pid #指定进程号(pid)的进程

在这里插入图片描述

  • jstack
    1、jstack主要用来查看某个Java进程内的线程堆栈信息。语法格式如下:jstack [option] pid

在这里插入图片描述

  • jstat
    1、JVM统计监测工具,查看各个区内存和GC的情况
    2、语法格式如下:jstat [ generalOption | outputOptions vmid [interval[s|ms][count]] ]

在这里插入图片描述

  • jstated
    1、虚拟机的jstat守护线程,主要用于监控JVM的创建与终止,并提供一个接口,以允许远程监视工具附加在本地系统上运行JVM

  • jcmd
    1、JVM诊断命令工具,将诊断命令请求发送到正在运行的Java虚拟机,比如可以用来导出堆,查看Java进程,导出线程信息,执行GC等

  • jconsole
    一、一个用于监视Java虚拟机的符合JMX的图形工具。它可以监视本地和远程JVM,还可以监视和管理应用程序

二、使用jconsole
1:因为jconsole是JDK自带的所以我们只要在命令窗口输入jconsole即可

在这里插入图片描述

2:选择一个对应的进程,由于是本地选择不安全的连接即可

在这里插入图片描述
在这里插入图片描述
2:成功接入的界面,在概览中我们可以看到当前虚拟机的情况如:堆内存的使用量,线程的使用情况,装载类的这块情况,CPU的使用情况

在这里插入图片描述
3:线程这块可以检测是否存在死锁
在这里插入图片描述

  • jmc
    一、jmc(JDK Mission Control)Java任务控制(JMC)客户端包括用于监视和管理Java应用程序的工具,而不会引入通常与这些类型的工具相关联的性能开销
    二、下载地址

三、下载好后解压JMC在bin目录下打开
在这里插入图片描述四、MBean服务器

1:概览
在这里插入图片描述
2:MBean 浏览器
在这里插入图片描述
3:触发器

在这里插入图片描述
4:系统

在这里插入图片描述
5:内存

在这里插入图片描述
6、线程

在这里插入图片描述
7:诊断器

在这里插入图片描述
五、飞行记录器(它可以长时间的记录虚拟机的一个运行情况,非常有利于我们去监控服务端的应用情况)

1:开启飞行记录(这里我将idea的jdk设置成了12版本),直接点下一步,然后点击完成就好了,我这里使用的是默认配置

在这里插入图片描述
在这里插入图片描述

2:会出现一个倒计时记录服务端信息,同时我们操作下idea中的方法

在这里插入图片描述3、当计时结束后会以大纲的形式展示分析的结果,这个结果是他分析后给出的综合记录
在这里插入图片描述在这里插入图片描述

六、JFR需要JDK的商业证书,需要解锁jdk商业特性,例如:jcmd 1152 VM.unlock_commercial_features

七、可以直接使用命令行来启动JFR,例如:jcmd 41250(对应运行程序的PID) JFR.start delay = 10s duration = 1m filename=/Users/cc/Desktop/log.jfr(记录.jfr格式文件位置和名称)

  • visualVM

一、一个图形化工具,它提供有关在Java虚拟机中运行的基于Java技术的应用程序的详细信息

二、Java VisualVM提供内存和CPU分析,堆转储分析,内存泄漏检测,访问MBean和垃圾回收
三、visualVM下载地址

在这里插入图片描述
四、Local

1:Overview(概览)
在这里插入图片描述
2:vvm是一个插件话体系,有些功能我们需要通过安装插件才能使用,例如下图中的Visual GC就是一个插件,安装插件:Tools -> Plugins -> Available Plugins,安装对应的Pligun就好了

在这里插入图片描述
在这里插入图片描述
3:Monitor(监视器)
在这里插入图片描述
4:Thread(线程),还可以做线程的Dump

在这里插入图片描述
5、Sampler(采样器)

在这里插入图片描述6、Profiler

在这里插入图片描述

7、Visual GC

在这里插入图片描述
五、JFR Snapshots

1:打开对应的jfr文件
在这里插入图片描述
在这里插入图片描述

  • 两种远程连接方式

1、JMX连接可以查看:系统信息、CPU使用情况、线程多少、手动执行垃圾回收等比较偏于系统级层面的信息

2、jstatd连接方式可以提供:JVM内存分布详细信息、垃圾回收分布图、线程详细信息、甚至可以看到某个对象使用内存的大小

  • 远程连接Tomact

1、虚拟机上安装一个Tomcat,具体安装步骤可以自行查找对应博客

在这里插入图片描述
2、进入tmocat的bin目录下配置对应的catalina.sh文件,配置中的IP地址需要配置成你虚拟机对应的IP

// 配置内容如下
CATALINA_OPTS="-Xms800m -Xmx800m -Xmn350m -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError -Dcom.sun.management.jmxremote=true -Djava.rmi.server.hostname=192.168.1.206 -Docm.sun.management.jmxremote.port=6666 -Docm.sun.management.jmxremote.ssl=false -Docm.sun.managementote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"

2、配置jstatd:
①自定义一个statd.policy文件,添加如下配置(对应的JDK版本:jdk13):

grant codebase "jrt:/jdk.jstatd" {
   permission java.security.AllPermission;
};

grant codebase "jrt:/jdk.internal.jvmstat" {
   permission java.security.AllPermission;
};

②然后在JDK_HOME/bin(jdk的bin目录下)下面运行jstatd,示例如:

./jstatd -J-Djava.security.policy=/home/software/statd.policy(statd.policy文件的全路径) -J-Djava.rmi.server.hostname=192.168.1.205(你服务器上的IP)

运行成功
在这里插入图片描述
3、通过vvm的Remote连接对应的虚拟机IP,直接点击OK

在这里插入图片描述

4、可以看到对应的Jstatd已经虚拟机上正在运行的Tomcat
在这里插入图片描述
5、添加JMX的连接,端口号就是在catalina.sh配置的端口号
在这里插入图片描述

  • 监控与故障处理实战

一、内存泄漏分析、线程查看、热点方法查看、垃圾回收查看

二、实例代码
1、我们运行下面额代码

public class MemoryTest {

    public static void main(String[] args) {
        try {
            Thread.sleep(10000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List list = new ArrayList();
        for (int i = 0; i < 10000; i++) {
            list.add(new A());
            if (i % 20 == 0) {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.gc();
        System.out.println("over==================>");

        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        try {
            reader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class A {
    private byte[] bs = new byte[10 * 1024];
}

2、JMC监情况,我们点击我们所运行的方法类MemoryTest,并打开他的飞行记录,设置对应的时间,之后查看对应的分析结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3、VisualVM的情况和上面一致选中正在运行的类,查看对应的线等的一些信息

在这里插入图片描述
三、线程死锁

1、实例代码(实现死锁问题)

public class ThreadLockTest {
    public static void main(String[] args) {
        AModel am = new AModel("mys1", "mys2");

        for (int i = 0; i < 10; i++) {
            Thread t1 = new Thread(new MyThread1(am));
            t1.setName("MyThread1");
            t1.start();

            Thread t2 = new Thread(new MyThread2(am));
            t2.setName("MyThread2");
            t2.start();

        }
    }
}

class AModel {
    public String s1;
    public String s2;
    public AModel(String s1, String s2) {
        this.s1 = s1;
        this.s2 = s2;
    }
}

class MyThread1 implements Runnable{
    private AModel am = null;
    public MyThread1(AModel am) {
        this.am = am;
    }
    @Override
    public void run() {
        synchronized (am.s1) {
            System.out.println("now in mythread1111, has am.s1");

            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("now thread1 begin get am.s2");
            synchronized (am.s2) {
                System.out.println("Thread id =" + Thread.currentThread().getId()
                        + ", s1=" + am.s1 + " ,s2=" + am.s2);
            }
        }
    }
}

class MyThread2 implements Runnable{
    private AModel am = null;
    public MyThread2(AModel am) {
        this.am = am;
    }
    @Override
    public void run() {
        synchronized (am.s2) {
            System.out.println("now in mythread1111, has am.s2");

            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("now thread1 begin get am.s1");
            synchronized (am.s1) {
                System.out.println("22222222Thread id =" + Thread.currentThread().getId()
                        + ", s1=" + am.s1 + " ,s2=" + am.s2);
            }
        }
    }
}

2、VisualVM会自动检测到死锁问题
在这里插入图片描述
3、点击对应的线程会告知你线程的状态、行为、代码的行数、原因

在这里插入图片描述

8、JVM调优

一、JVM调优:调什么、如何调、调的目标是什么
一、调什么

  • 内存方面
    1、JVM需要的内存总大小
    2、各种内存分配,新生代、老年代、存活区
    3、选择合适的垃圾算法、控制GC停顿次数和时间
    4、解决内存泄漏的问题,辅助代码优化
    5、内存热点:检查哪些对象在系统中数量最大,辅助代码优化
  • 线程方面
    1、死锁检查,辅助代码优化
    2、Dump线程详细信息:查看线程内部运行情况,查找竞争线程,辅助代码优化
    3、CPU热点:检查系统哪些方法占用了大量CPU时间,辅助代码优化

二、如何调

  • 如何调优
    1、监控JVM的状态,主要是内存、线程、代码、I/O几部分
    2、分析结果,判断是否需要优化
    3、调整:垃圾回收算法和内存分配;修改并优化代码
    4、不断的重复监控、分析和调整,直至找到优化的平衡点

三、JVM调优的目标
1、GC 时间足够的小
2、GC的次数足够少
3、将转移到老年代的对象数量降到最小(因为老年代的对象存活时间比较长,占用大量的内存资源,如果不是十分有必要的话,尽量不要让对象进入老年代)
4、减少Full GC的执行时间
5、发生Full GC的间隔足够的长

二、JVM调优策略、调优冷思考、调优经验
一、常见调优策略
1、减少创建对象的数量
2、减少使用全局变量和大对象
3、调整新生代、老年代的大小到最合适
4、选择合适的GC收集器,并设置合理的参数

二、JVM调优冷思考
1、多数的Java应用不需要在服务器上进行GC优化
2、多数导致GC问题的Java应用,都不是因为参数设置错误,而是代码问题
3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合)
4、JVM优化是到最后不得已才采用的手段
5、在实际使用中,分析JVM情况优化代码比优化JVM本身要多得多
6、如下情况通常不用优化:

1:Minor GC执行时间不到50ms
2:Minor GC执行不频繁,约10秒一次
3:Full GC执行时间不到1s
4:Full GC执行频率不算频繁,不低于10分钟1次

三、JVM调优经验
1、要注意32位和64位的区别,通常32位仅支持2-3g左右的内存,64位没有什么限制
2、要注意client模式和Server模式的选择(可以将两种模式参数设置成一样去跑同一个应用进行性能监控来看有什么差别)

1、client模式特点:快速启动,最小的内存占用,快速代码生成的JAT编译器
2、server模式特点:更复杂的代码升级优化,作为服务性的应用更为靠谱一些

3、要想GC时间小必须要一个更小的堆;而要保证GC次数足够少,又必须保证一个更大的堆,这两个是有冲突的,只能取其平衡(记录一段时间的运行情况去分析新生代的峰值是多少,低谷是多少,平均值是多少,同样的参数区分析老年代,寻找一个最恰当的值,还要预留一定的空间来预防增长)

4、针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值

5、新生代和老年代将根据默认的比例(1 : 2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整,也可以通过-XX:newSize -XX:MaxNewSize来设置其绝对大小,同样,为了防止新生的堆收缩,通常会把-XX:newSize -XX:MaxNewSize设置同样大小

6、合理规划新生代和老年代的大小

7、如果应用存在大量的临时对象,应该选择更大的新生代;如果存在相对较多的持久对象,老年代应该适当增大。在抉择时应该本着Full GC尽量少的原则(Full GC的时间比较长,而且会导致应用停止),让老年代尽量缓存常用对象,JVM的默认比例1:2也是这个道理

8、通过观察应用一段时间,看其在峰值时老年代会占多少内存,在不影响Full GC的前提下,根据实际情况加大新生代,但应该给老年代至少预留1/3的增长空间

9、线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太大了,一般256K就足用了。在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程

三、分析和处理内存溢出

一、内存泄漏
1、内存泄漏导致系统崩溃前的一些现象,比如:

1:每次垃圾回收的时间越来越长,Full GC时间也延长到好几秒
2:Full GC的次数越来越多,最频繁时隔不到1分钟就进行一次Full GC
3:老年代的内存越来越大,并且每次Full GC后老年代没有内存释放

2、老年代堆空间被占满的情况

3、这种情况的解决方式:一般就是根据垃圾回收前后情况对比,同时根据对象引用情况分析,辅助去查找泄漏点

4、堆栈溢出的情况

5、通常抛出 java.lang.StackOverflowError例外

6、一般就是递归调用没退出,或者循环调用造成

四、调优实战

一、重点是调优的过程、方法和思路

二、内存调整、数据库连接调整、内存泄漏查找等

10、面试题

一、字节码部分

一、知道字节码吗?字节码都有哪些?Integer x = 5, int y = 5, 比较 x == y 都经过哪些步骤

二、简述Java的类加载机制,并回答一个JVM中可否存在两个相同的类

三、讲讲类加载机制,都有哪些类加载器,这些类加载器都加载哪些文件?

四、说说类加载、连接和初始化的过程

二、内存分配部分

一、谈谈JVM内存模型

二、JVM的数据区有哪些,作用是什么

三、Java堆内存一定是线程共享的吗

为对象分配内存的基本方法:指针碰撞法、空闲列表法
内存分配并发问题的解决:CAS、TLAB

四、JVM堆内存结构是怎样的?哪些情况会触发GC?会触发哪些GC?

三、垃圾回收方面

一、说一说JVM的垃圾回收

二、JVM四种引用类型

三、JVM回收算法和垃圾收集器

四、监控工具和实战方面

一、如何把Java内存的数据全部dump出来

二、Jstack是做什么的?Jstat呢?

三、如何定位问题?如何解决问题?说一下解决思路和处理方法

四、CPU使用率过高怎么办

五、线上应用频繁Full GC 如何处理

六、如果应用周期性地出现卡顿,你会怎么来排查这个问题?

七、你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?

八、StackOverFlow异常有没有遇到过?这个异常会在什么情况下被触发?如何制定线程堆栈的大小?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值