JVM笔记

JVM笔记

一. JVM概览

1. JVM整体结构图

img

方法区和堆 共享

Java栈,本地方法栈,程序计数器独立

2. JVM 执行流程

img

执行器:解释执行字节码文件;

JIT编译器:编译字节码文件 为 机器指令,编译热点数据,放入缓存,方便重复使用,提高效率;

3.JVM 生命周期

虚拟机启动:通过虚拟机 引导类加载器 创建一个 初始类,这个类是由虚拟机的具体实现指定的。 例如:启动一个自定义类,jvm会使用 引导类加载器 创建一个初始类A,A中定义了 加载自定义类所需要的提前加载的父类 以及其他信息;

虚拟机运行:执行java代码时jvm就在运行状态

javap -c 文件名 反编译

jps查看jvm正在执行的进程

虚拟机退出

​ 出现异常或错误,退出

​ 程序执行结束退出

​ 主动调用Runtime类或System类的exit()方法,或Runtime类的halt()方法,并且Java安全管理器也允许这次 exit()或halt()操作

4.学习路径

在这里插入图片描述

  1. JVM 内存结构
  2. JVM的垃圾回收机制
  3. 字节码文件
  4. 类加载器
  5. JIT Compiler 运行时编译器

二. 类加载

1.类加载时机

  1. 第一次new 对象
  2. 第一次加载该类的子对象
  3. 第一次使用该类的静态变量和静态方法
  4. 通过反射显示类加载Class.forName(类全限定名)
  5. JVM启动时标明的启动类,即文件名和类名相同的那个类

注意: 对于一个final类型的静态变量,如果该变量编译时就能够确定,外界第一次调用就不会触发类加载(例如:static final int a = 1;)

否则,就会触发类加载(例如:static final Integer a = 1;)

2.类加载过程

加载->验证->准备->解析->初始化

其中验证,准备,解析三个阶归属于 链接阶段

  1. 加载

    jvm的类加载器会把class文件内容加载到JVM内存中,生成Class类对象;

在这里插入图片描述

  1. 验证

    用于检验被加载的类是否有正确的内部结构,是否符合JVM规范,并和其他类协调一致。

  2. 准备

    类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

  3. 解析

    将类的二进制数据中的符号引用替换成直接引用。符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

  4. 初始化

    初始化是为类的静态变量赋予正确的初始值。

3.类加载器

类的唯一标识

在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

在这里插入图片描述

1. 根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

2. 扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null

3. 系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它**负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader**。

4.类加载机制

双亲委派模型

原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

preview

优势

  • 避免类的重复加载,确保一个类的全局唯一性
    • Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 保护程序安全,防止核心API被随意篡改

三.JVM 运行时内存结构

1.字节码文件结构

字节码文件主要包含:类文件描述信息,class常量池,方法描述,JVM字节码程序等

package cn.itcast.jvm.t5;

public class HelloWorld {
    public HelloWorld() {
    }

    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

javap -v HelloWorld.class

Classfile /D:/workspace-idea/review/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
  Last modified 2021-1-8; size 567 bytes
  MD5 checksum 8efebdac91aa496515fa1c161184e354
  Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // cn/itcast/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t5/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               cn/itcast/jvm/t5/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public cn.itcast.jvm.t5.HelloWorld();
    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 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t5/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

2.程序计数器

在这里插入图片描述

作用:记住JVM下一条指令的地址;

特点: 线程私有

​ 永远不会发生内存溢出

3.虚拟机栈

虚拟机栈:又叫java线程栈,JVM会为每个线程开辟一个栈空间,线程栈之间互不影响;

栈帧:栈内部方法调用的的时候会产生一个栈帧压栈;

活跃栈帧:栈顶部的那一个栈帧

问题

  1. 垃圾回收是否涉及栈内存

    不会,栈由栈帧组成,用完即弹出,对应的内存也自动清除

  2. 栈内存是否越大越好?

    不是,栈内存越大,同时处理线程数越小

    -Xss表示栈内存大小,linux,mac默认1m

    假如:虚拟机栈空间一共500M,那么就能同时处理500个线程;若分配线程栈大小2M

    那么只能同时处理250个线程

  3. 线程安全问题

    局部变量线程安全,非局部变量线程内不安全

  4. 栈溢出

    调用栈帧过多 :例如 递归调用,以及对象关系之间循环引用

    栈帧过大

  5. 线上问题解决流程

    1. 线上cpu使用过高

      1. top 查看运行进程,找到cpu使用过高进程,例如 : 32600
      2. ps -H -eo pid,tid,%cpu | grep 32600 查看该进程哪一个线程 引起cpu使用率过高,例如:32655
      3. jstack 32655(线程ID) ,会打印该线程相关信息,通过信息具体定位哪一行出错 (需要32655转换为16进制,应为只会显示线程名,且只显示线程id的16进制
    2. 线程运行很长时间没有结果(例如:死锁)

      ps 查看程序进程

      jstack 进程号 查看死锁线程

4. 本地方法栈

JAVA其实有部分页使用C或C++写的,这些 native关键字修饰的方法就是本地方法,这些方法的实现一般使用C或C++来实现的,在java中只是做一个调用;

例如:Object 类中的 clone(),hashCode()方法等;

5.堆

-Xmx 指定堆空间大小 ,默认4G

堆内存溢出:outOfMemeryError

堆内存溢出诊断:

jps 查看当前系统有哪些java进程,显示进程id,进程名

jmap -heap 进程id 显示进程占用堆内存情况

jconsole 图形界面方式显示java各种内存变化情况

案例:堆内存GC之后,仍然有大部分内存占用

jvirsualvm 命令 图形化查看,里面有个 堆dump查看那些对象占用内存过多

6.方法区

1.存储内容以及版本比较

在这里插入图片描述

1.6以及之前:方法区是一个概念,被存储在永久代存储类的元数据信息,类加载器信息,运行时常量池以及stringtable(串池)

1.7:运行时常量池还是在方法区,永久代中,但是 字符串常量池放在了堆中

1.8:方法区为一个概念,存储在操作系统的本地内存,在本地内存划分了一个 元数据空间,不再划到JVM ;类信息,类加载器,运行时常量池储存在 元数据空间,另外串池数据存在堆中

2.方法区内存溢出

-XX:MaxMetaspaceSize元数据空间大小

字节码文件包含信息:类基本信息 常量池 类方法定义 ,虚拟机指令

javap -c *.class 反编译

javap -v *.class 类反编译后详细信息

7.常量池(class常量池)以及运行时常量池

1.常量池(class常量池)

java被编译为 class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池;

常量池又叫class常量池,它是被JVM加载到内存方法区内部的字面量以及符号应用的集合

常量池:常量池是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名。参数类型,字面量等一些信息;

在这里插入图片描述

2.运行时常量池

运行时常量池时JVM加载class文件后 将class常量池内容转移到 运行时常量池(所以每一个class文件都会有一个运行时常量池),它是动态的,内容包含了编译class文件的常量池和 运行时新增的常量信息

在这里插入图片描述

8.StringTable

1.intern()方法

参考:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html

intern()方法把字符串强制放入串池中,并返回串池中的对象:

  • 1.6版本: 串池中有则不添加返回串池中对象;没有则复制堆中对象,生成另一个对象放入串池,返回串池对象,此时两个对象不同;
  • 1.7以后版本: 串池中有则不添加返回串池中对象;没有则把堆中对象的引用放入串池,返回串池对象,此时两个对象相同;
2.在Java中有两种创建字符串对象的方式:

参考:https://blog.csdn.net/qq_45737068/article/details/107149922

  1. 采用字面值的方式赋值
  2. 采用new关键字新建一个字符串对象(会在堆和字符串常量池中个创建一个)
3.面试题
  1. 面试题:

    public class Demo1_21 {
        public static void main(String[] args) {
            //StringTable[]
            String s1 = "a"; //如果串池中没有"a",把"a"放入StringTable串池
            String s2 = "b";  //"b"放入串池
            String s3 = "a" + "b"; // 由于是固定的结果,编译器直接优化为"ab",把"ab"放入串池
            /**执行步骤:
             * 1. StringBuilder sb = new StringBuilder()
             * 2.sb.append("a").append("b")
             *3.sb.toString();  注意toString()方法内部 会在堆中new String()一个新对象
             */
            String s4 = s1 + s2;  //此时s4为堆中对象
            String s5 = "ab";//s5指向串池
            /**intern()方法把字符串强制放入串池中,并返回串池中的对象:
             * 1.6版本: 串池中有则不添加返回串池中对象;没有则复制堆中对象,生成另一个对象放入串池,返回串池对象,此时两个对象不同;
             *1.7以后版本: 串池中有则不添加返回串池中对象;没有则把堆中对象的引用放入串池,返回串池对象,此时两个对象相同;
             */
            String s6 = s4.intern();
    
            System.out.println(s3 == s4); // false
            System.out.println(s3 == s5); // true
            System.out.println(s3 == s6); // true
    		//new String()产生的匿名对象会很快清除
            String x2 = new String("c") + new String("d"); // new String("cd")
            x2.intern();
            String x1 = "cd";
    
    // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
            System.out.println(x1 == x2);
        }
    }
    
  2. StringTable 调优

    StringTable底层数据结构为hashtable ,使用数组+链表的形式,所以我们只要改变buket的数量就能优化程序;

    –XX:StringTableSize=60086 设置buket个数

  3. 优化场景

    当程序中有海量的字符串存储,且有大量重复的数据可以考虑将堆对象intern()入池,减少内存占用

9.直接内存

  1. 什么是直接内存?

    直接内存是操作系统中的缓冲内存,不贵JVM管理,所以垃垃圾回收的时候JVM无法回收;

    但是直接内存可以手动分配,手动回收;

    java代码调用allocateDirect(size)分配直接内存,通过Unsafe类的freeMemory()方法手动回收;

    当然也可以JVM回收调用直接内存的对象,通过回收该对象,触发直接内存的回收机制

  2. 特点

    1. 分配和回收艰难,读写效率高
    2. 不收JVM管理
    3. 常见于NIO操作,用作数据缓冲
  3. 直接内存优化?

    public class Demo1_9 {
        static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
        static final String TO = "E:\\a.mp4";
        static final int _1Mb = 1024 * 1024;
    
        public static void main(String[] args) {
            io(); // io 用时:1535.586957 1766.963399 1359.240226
            directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
        }
    
        private static void directBuffer() {
            long start = System.nanoTime();
            try (FileChannel from = new FileInputStream(FROM).getChannel();
                 FileChannel to = new FileOutputStream(TO).getChannel();
            ) {
                ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
                while (true) {
                    int len = from.read(bb);
                    if (len == -1) {
                        break;
                    }
                    bb.flip();
                    to.write(bb);
                    bb.clear();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            long end = System.nanoTime();
            System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
        }
    
        private static void io() {
            long start = System.nanoTime();
            try (FileInputStream from = new FileInputStream(FROM);
                 FileOutputStream to = new FileOutputStream(TO);
            ) {
                byte[] buf = new byte[_1Mb];
                while (true) {
                    int len = from.read(buf);
                    if (len == -1) {
                        break;
                    }
                    to.write(buf, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            long end = System.nanoTime();
            System.out.println("io 用时:" + (end - start) / 1000_000.0);
        }
    }
    

    上面代码是 读取操作系统文件时 使用直接内存和不使用 的对比,可以发现使用直接内存读取效率高

    为什么呢?

    java代码读取文件时 由用户态转换为内核态,读取文件放入操作系统的内存缓冲区,然后转换为用户态,

    复制操作系统缓冲区到 JVM堆内存,这样需要两个缓冲区,耗时且性能不好;

    而直接内存 一种 JVM和操作系统共用的缓冲区,使用java代码可以直接操作,性能较高

在这里插入图片描述

在这里插入图片描述

  1. 直接内存分配释放原理

    分配:调用allocateDirect()方法会生成一个 DirectByteBuffer对象,DirectByteBuffer的构造方法里调用Unsafe类的setMemory()方法设置分配 直接内存大小,还生成Cleaner对象,方便直接内存回收;

    回收:生成Cleaner有个回调方法会创建一个Deallocator 对象,它实现Runnable接口,Cleaner(弱引用对象)弱引用于ByteBUffer对象,当ByteBUffer被回收时,Cleaner会被放入引用队列中,ReferenceHandler守护线程会从引用队列获取Clener对象,通过Cleaner的clean方法调用freeMemory本地方法释放内存;

四.垃圾回收

GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。

1.如何判断垃圾是否可以回收?

1.引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。

但是,如果出现循环引用的情况,对象无法回收,如下图所示

在这里插入图片描述

2.可达性分析法
所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。

Tracing GC的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡。注意再注意:tracing GC的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。

1.可以被当做GC ROOT的对象:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.方法区中的类静态属性引用的对象
3.方法区中的常量引用的对象
4.原生方法栈(Native Method Stack)中 JNI 中引用的对象。

5.处于激活状态的线程

6.正在被用于同步的各种锁对象

7.JVM自身持有的对象,比如系统类加载器等

8.通过System Class Loader或者Boot Class Loader加载的class对象(通过自定义类加载器加载的class不一定是GC Root)

参考:

https://bbs.csdn.net/topics/390669860

https://zhuanlan.zhihu.com/p/181694184

2.被GC判断为”垃圾”的对象一定会回收吗?

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)

如果这个对象被判定为有必要执行finalize()方法,那么**这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。**这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

参考:

https://blog.csdn.net/mine_song/article/details/63251367?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

2.五种引用

在这里插入图片描述

1.五种引用的区别:
  1. 强引用 :只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  2. 软引用(SoftReference): 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身 (内存紧张时,一些不重要的文件,图片信息用软引用存放)
  3. 弱引用(WeakReference) :仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用引用的对象 可以配合引用队列来释放弱引用对象自身 (ThreadLocal本地线程原理中使用)
  4. 虚引用(PhantomReference)以NIO为例,在创建ByteBuffer的时候,会创建一个名为Cleaner的虚引用对象,ByteBuffer会分配一个块直接内存,并把内存地址传递给Cleaner;这样做的目的是当ByteBuffer没有被强引用时,会被垃圾回收掉,但是直接内存并不能java的垃圾回收管理,此时Cleaner会进入引用队列,由Reference Handler线程调用Unsafe.freeMemory方法把直接内存释放掉。
  5. 终结器引用(FinalReference):Java中所有的类都继承自Object类,在Object类中有一个finalize()方法,如果某个类A重新覆盖了这个方法,那么当没有强引用引用时,虚拟机会创建一个终结器引用指向这个对象,把终结器引用加入到引用队列,再由一个优先级很低的线程Finalizer去调用类A的finalize()方法。
2.finalize()方法

finalize流程:

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

垃圾回收器准备释放内存的时候,会先调用finalize()。

之所以使用finalize()方法是为了释放一些就GC不会管理的特殊区域;

特殊区域:

  1. GC一般管理显示new出来的java对象,但是有一些内存空间是有本地方法(Native method)J(C,C++等语言)创建出来;这些内存不归JVM管理,需要手动调用对应这些C或C++的方法释放内存;

    所以,通常涉及到这些内存的释放,需要覆盖finalize()方法,在覆盖方法里执行C或C++方法

  2. 打开的文件资源,这些资源也不属于垃圾回收器的回收范围。

一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize()方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。

3.软引用和引用队列

软引用使用场景:当内存紧张时,一些不重要的资源可以用软引用关联,内存不足,直接回收不重要的资源

// list --> SoftReference —-> byte[]
// list对SoftReference是强引用,但对SoftReference对byte[]是软引用
List<SoftReference<Byte[]>> list=new ArrayList<>();
ReferenceQueue<byte[]> queue=new ReferenceQueue<>();//引用队列
for(int i=0;i<5;++i){
	//关联了引用队列,当软引用所关联的byte[]回收时,软引用自己也会加入到queue中去
		SoftReference<Byte[]> ref=new SoftReference<>(new Byte[_4MB]);
		list.add(ref);
}
//从list中删除掉无效的引用
Reference<? Extends byte[]> poll=queue.poll();
while(poll!=null){
		list.remove(poll);
		poll=queue.poll();
}

3.垃圾回收算法

1.标记清除算法(适用于老年代)

先标记后清除

缺点: 1. 标记和清除效率都不高

2.容易产生空间碎片

2.标记整理(适用于老年代)

标记,清除,整理

清楚后会把存活对象压缩整理到一片区域

优点:无空间碎片

3.复制算法(适用于新生代)

内存分割为两块,把一块中的存活对象复制到另一块,清除第一块空间

特性: 不会有空间碎片

占用双倍内存

4.分代回收

在这里插入图片描述

新生代一般使用 复制算法,老年代一般使用标记清除或标记整理算法

回收步骤:

  1. 新生对象首先进入Eden
  2. 新生代空间不足时触发MinorGC,把Eden和from的存活数据复制到to,然后清除Eden和from,并发存活对象年龄+1,最后交换from和to区域(MinorGC会触发Stop The World 时,应用程序线程会被阻塞,直到GC线程结束)
  3. 当对象年龄超过阈值(最大寿命15),把对象从新生代放入老年代
  4. 当老年代触发GC,会先尝试进行MinorGC,空间仍然不足会触发FullGC,非常耗时

在这里插入图片描述

4.垃圾回收器

在这里插入图片描述

1.串行垃圾回收器(Serial/serial Old)

GC线程执行时,用户线程阻塞

新生代,老年代都是串行

新生代:复制算法

老年代:标记整理

在这里插入图片描述

2.并行垃圾回收器

多个GC线程间并发执行,GC线程和与用户线程并行执行,GC执行时用户线程阻塞

ParNew: Serial的并行模式,新生代并行,老年代串行;新生代复制算法、老年代标记-压缩;

Parallel Old:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法;

Parallel Scavenge:类似ParNew,更注重吞吐量

参数详解:

-XX:+UseAdaptiveSizePolicy 自适应对大小策略

-XX:GCTimeRatio GC时间占运行时间比例,公式1/(1+Ratio),例如Ratio为99,则单位时间内要求GC时间为1/100(程序运行100分钟,GC时间不操作1分钟),当超过1/100时,会缩小堆空间

-XX:MaxGCPauseMillis=ms GC导致程序最大暂停毫秒数

-XX:ParallelGCThreads=n GC线程数,一般为cpu核数

在这里插入图片描述

3.响应时间优先

CMS(Concurrent Mark Sweep)并发式标记清理垃圾回收器(主要用于老年代

  1. 初始标记 需要Stop The World 仅仅标记GC Roots对象
  2. 并发标记 GC线程与用户线程并发执行,沿着GC Roots遍历引用链,并发标记阶段就是进行GC Roots Tracing的过程,时间较长
  3. 重复标记 因为并发标记时用户线程在执行过程中,可能会产生新的垃圾对象,需要STW
  4. 并发清除 GC线程与用户线程并发执行

由于CMS是标记清除算法,会有空间碎片,当老年代满时,可以选择退化为标记整理的垃圾回收器,例如:Serial Old

缺点:

  1. 标记整理算法,会有大量内存碎步,但是可以通过XX:CMSFullGCsBeForeCompaction 设置几次CMS回收后,使用Full GC进行一次碎片整理
  2. CMS并发清理时,与用户线程并发执行,并发清理阶段用户线程可能产生新的垃圾对象,所以GC必须在堆内存占满前完成

参数详解:

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次CMS回收后,使用Full GC进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

-XX:ConcGCThreads:=threads CMS并发线程数

-XX:CMSInitiatingOccupancyFraction=percent 代表老年代空间占用达到percent%进行一次GC,由于并发清理时,用户线程也在执行,所以可能会产生新的垃圾对象,不能等老年代空间占满后才进行GC

-XX:+CMSScavengeBeforeRemark 重新标记之前对新生代进行垃圾回收,减少重新标记遍历对象

在这里插入图片描述

4.常见垃圾回收期组合
新生代GC策略老年老代GC策略说明
1SerialSerial OldSerial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
2SerialCMS+Serial OldCMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。
3ParNewCMS使用 -XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项 -XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
4ParNewSerial Old使用 -XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
5Parallel ScavengeSerial OldParallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
6Parallel ScavengeParallel OldParallel Old是Serial Old的并行版本
7G1GCG1GC-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启; -XX:MaxGCPauseMillis=50 #暂停时间目标; -XX:GCPauseIntervalMillis=200 #暂停间隔目标; -XX:+G1YoungGenSize=512m #年轻代大小; -XX:SurvivorRatio=6 #幸存区比例

5.G1垃圾回收器

参考:

https://blog.csdn.net/coderlius/article/details/79272773

https://blog.csdn.net/shlgyzl/article/details/95041113?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.control

1.G1的特点
  • G1的设计原则是"首先收集尽可能多的垃圾(Garbage - First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部- 采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时- 间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
  • G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进- 行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天- 然就是一种压缩方案(局部压缩);
  • G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的- survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不- 同代之间前后切换;
  • G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次- 收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合- 收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿

在这里插入图片描述

2.G1内存模型

G1垃圾回收器取消了新生代,老年代物理内存的划分,而是把整个堆内存划分成多个region区域,切每个region区域大小一致;

region又分为四类,分别是Eden,Survivor,Old,以及Humongous 巨大对象区域

3.垃圾回收阶段

大致分为三个阶段:新生代会后Young GC, 并发标记Concurrent mark阶段和Mixed GC混合回收阶段

1.Young GC

新生代的会后与之前的垃圾回收相同,新生代空间占满,进入Young GC阶段,会把存活对象放入 Survivor幸存区,如果survior区也满了就直接放入老年代

Young GC 阶段:

  • 阶段1:根扫描
    静态和本地对象被扫描
  • 阶段2:更新RS
    处理dirty card队列更新RS
  • 阶段3:处理RS
    检测从年轻代指向年老代的对象
  • 阶段4:对象拷贝
    拷贝存活的对象到survivor/old区域
  • 阶段5:处理引用队列
    软引用,弱引用,虚引用处理
2.Mixed GC阶段1- 全局并发标记
  • 初始标记(initial mark,STW)
    在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
  • 根区域扫描(root region scan)
    G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
  • 并发标记(Concurrent Marking)
    G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
  • 最终标记(Remark,STW)
    该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
  • 清除垃圾(Cleanup,STW)
    在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
3. Mixed GC阶段2- 拷贝存活对象

不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区

4.问题
1.Young GC时的跨代引用问题

当YoungGC时,回收新生代,那么怎么获取老年代GCRoots呢?

采用Remembered Set 和 Card Table 的形式

2.Remembered Set

CMS中:在老年代中划分了一块区域 用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。(记录老年代的对象引用了哪些新生代对象,及记录在老年代的一块区域

G1使用point-in:意思是哪些分区引用了当前分区中的对象;新生代的Remembered Set 会记录老年代到新生代之间的引用;(记录 了引用该区域的 region区的对象,记录的是新生代对象被哪些老年代对象引用)

但是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)

一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

在这里插入图片描述

3. 卡表(Card Table)

一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。

在这里插入图片描述

4.如何保证应用程序在运行的时候,GC标记的对象不丢失呢?

有如下2中可行的方式:

  1. 在插入的时候记录对象
  2. 在删除的时候记录对象

刚好这对应CMS和G1的2种不同实现方式:

在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

1,在开始标记的时候生成一个快照图标记存活对象

2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)

3,可能存在游离的垃圾,将在下次被收集

5.巨大对象的内存分配与回收

超过region区域50%会被当做为Humongous对象

这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

对象分配策略:

TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。

对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。

  1. TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
  2. TLAB无法分配的对象,尝试放在Eden中
  3. 当Eden中放不下就只能放入老年代

在这里插入图片描述

对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

五.GC调优

参考:https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247483966&idx=1&sn=dfa3375d36aa2c0c25a775522e381e62&chksm=ebf6da41dc815357e0d53c73865a23f41219e75bac5a4d510bfa31cc51594b59a20e2e4f6cb8&cur_album_id=1326602114365276164&scene=189#rd

https://www.cnblogs.com/shanheyongmu/p/5775003.html

新生代GC调优

在这里插入图片描述

新生代空间大小一占总堆内存的25%~50%,空间小,容易频繁MinorGC,空间大

调增老年代晋升阈值

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值