微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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.学习路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oEjv0pzk-1612315051441)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20201223094909039.png)]

  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类对象;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RTmzCoh6-1612315051443)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210114205413950.png)]

  2. 验证

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

  3. 准备

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

  4. 解析

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

  5. 初始化

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

3.类加载器

类的唯一标识

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b6xUwhAU-1612315051451)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210114211415841.png)]

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.程序计数器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUNFxZst-1612315051452)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20201223100042440.png)]

作用:记住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.存储内容以及版本比较

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IgEcZq7b-1612315051453)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20201223120911532.png)]

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

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

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

2.方法区内存溢出

-XX:MaxMetaspaceSize元数据空间大小

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

javap -c *.class 反编译

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

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

1.常量池(class常量池)

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mOtFyOGB-1612315051453)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210108103226537.png)]

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代码可以直接操作,性能较高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-79POagnB-1612315051454)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20201225151812657.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WTNBN1qw-1612315051455)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20201225152100324.png)]

  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。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jqsV6CgS-1612315051456)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210106214230208.png)]

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.五种引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xR23MpwF-1612315051456)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20201225182512625.png)]

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.分代回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9UbEZFPD-1612315051457)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210108181458459.png)]

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

回收步骤:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZervM3LE-1612315051458)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210108182238638.png)]

4.垃圾回收器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aa8tcq2g-1612315051458)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210111113619402.png)]

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

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

新生代,老年代都是串行

新生代:复制算法

老年代:标记整理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Brko6wOc-1612315051459)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210111204458526.png)]

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核数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jp8gyQ0D-1612315051460)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210111204434299.png)]

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 重新标记之前对新生代进行垃圾回收,减少重新标记遍历对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8sJ8sj7W-1612315051460)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210111204349539.png)]

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)收集的方式。即每次- 收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合- 收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HLhSt5Wb-1612315051461)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210112213959421.png)]

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。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1rNJhYQ2-1612315051461)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210113211019791.png)]

3. 卡表(Card Table)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-od63XyGN-1612315051462)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210114102731832.png)]

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中放不下就只能放入老年代

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tl7gCjku-1612315051463)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210113212522097.png)]

对象分配规则

  • 对象优先分配在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调优

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BAGCdHHj-1612315051463)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210115114428525.png)]

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

调增老年代晋升阈值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCDmPwNY-1612315051464)(C:\Users\tangj\AppData\Roaming\Typora\typora-user-images\image-20210115115924459.png)]

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐