• JVM进阶


    文章目录

    1,JVM的定义

    1.1,概述

    Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java 最主要的特性之一。
    Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行,它的存在也是Java可以跨平台的关键。
    深入了解JVM有助于提高平台期解决问题的能力,优化并提高系统性能。

    1.2,作用

    在这里插入图片描述
    Java编译器
    JVM可以运行字节码文件,因为它有一个编译器,可以把Java文件编译成一个字节码文件.(java源代码通过java的编译器生成.class文件)
    Java的编译器分为两种: 静态编译器和动态编译器.
    静态编译器: 通过javac的方式,可以把Java源代码作为输入,并将其编译成字节码文件.
    动态编译器: 通过JIT的方式,动态的将一种编程语言编译成另一种语言,生成更好更有效的序列指令.

    1.3,Hotspot VM

    1.3.1 概述

    JVM是一种规范,基于这种规范不同公司就做了具体的实现,主流JVM产品:

    1. JRockit VM(BEA公司研发,后被Oracle收购)
    2. HotSpot VM(Sun公司研发,后被Oracle收购)

    Oracle公司收购了BEA公司和Sun公司, 就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM .
    Oracle公司宣布JDK8会完成这两款虚拟机的整合工作, 使之优势互补.
    HotSpot(热点代码探测技术)是Sun JDK和Open JDK中所带的默认虚拟机, 也是目前使用范围最广的Java虚拟机 .
    不管是现在仍在广泛使用的JDK8,还是使用比例较多的JDK版本中,默认的虚拟机都是HotSpot. 从服务器、桌面到移动端、嵌入式都有应用.

    1.3.2 作用

    在这里插入图片描述
    在HotSpot中,将class文件翻译成机器码执行时提供了两种方式,分别是:找出热点代码进行编译执行 和 全部代码次次解释执行.
    通常情况两种方式是会同时存在的
    其中:

    1. 热点代码: 一般泛指循环或高频使用的方法。
    2. 解释执行器:负责逐条将字节码翻译成机器码并执行。
    3. 编译执行器:负责即时编译(Just-In-Time compilation,简称JIT)执行,提升性能。
    1.3.3 结构

    在这里插入图片描述

    1.3.4 生命周期

    当一个程序启动,伴随的就是一个jvm实例的诞生,当这个程序关闭退出,这个jvm实例就随之消亡。
    类从被加载到虚拟机,内存开始到卸载出内存为止,整个声明周期的阶段包括7个阶段:
    加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。其中验证、准备、解析3个部分统称为链接(Linking)。
    在这里插入图片描述

    类加载:指的是类的生命周期中加载、连接、初始化三个阶段。就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口.
    加载:类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取.
    连接: 连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析.
    验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格
    式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行.
    准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值.
    jvm默认的初值是这样的:
    1. 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0.
    2. 引用类型的默认值为null.
    3. 常量的默认值为我们程序中设定的值.
    解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。 例如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,通过内存地址就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而内存地址就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址.
    连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化.
    初始化:如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有以下四种情况:
    1. 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法
    2. 通过反射方式执行以上三种行为
    3. 初始化子类的时候,会触发父类的初始化
    4. 作为程序入口直接运行时(也就是直接调用main方法).
    除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化.
    另外,初始化有三个阶段:
    1. 操作系统级别的初始化:
    确保只有一个JVM启动.
    初始化一些模块和系统属性.
    2. JVM级别的初始化:
    初始化一些命令行选项.
    初始化垃圾收集器,栈,本地存储,日至流记录器等.
    初始化lib库,线程库等.
    创建main线程,并与系统线程相关联.
    3. Java级别的初始化:
    初始化Java级别的线程.
    初始化类加载器,编译器,解释器,处理器.
    初始化并加载Java类和系统类等.
    卸载:执行shutdown,停止全部组建,全部线程,停止全部IO,关闭JVM.

    2,JVM内存管理JMM

    2.1,概述

    Java能够发展到如今,很大程度上取决于JVM(Java虚拟机),而内存管理又是JVM中的一个重要命题。
    JVM是如何管理各种资源的,这就需要有一套优秀的管理方案,能够了解JVM的内存结构,也可以让你编写高性能的应用程序。

    2.2,结构图

    2.2.1 从执行流程上

    JVM系统的三大主要部分: 类加载系统,运行时数据区,执行引擎
    类加载系统: ClassLoader System,负责把类加载进内存
    运行时数据区: Runtime Data Area, 负责存储数据信息
    执行引擎: Execute Engine,负责调用对象执行业务
    在这里插入图片描述

    2.2.2 从逻辑上

    分为两部分: 线程共享的内存区域 和线程独占的内存区域
    线程共享的内存区域 包括堆,方法区。
    线程独占的内存区域 包括虚拟机栈,本地方法栈,程序计数器。
    也就是说,当你在写程序时,需要判断当前数据读写的是存在于哪类内存区域。
    如果存在的是线程共享的内存区域,那么就要考虑是否存在线程安全问题,
    如果存在线程独占的内存区域,那么就可以打消这种顾虑。
    在这里插入图片描述
    在这里插入图片描述
    堆内存被分为两部分: 年轻代(young)和老年代(old).
    年轻代分为伊甸园区(eden)和幸存区(survivor),如上图幸存区又被分为两个区域(from和to).
    一般来讲新创建的对象,都会被分配在年轻代,并放在伊甸园区(eden). 如果伊甸园区内存不足时会启动GC回收,还是没有被回收的就会放入幸存区的from区,后面GC时可能也会把from里的内容回收一部分,from没有被回收的就会放进to里并被记录这个对象的年龄,如果多次未被回收的对象(年龄大的)就会被直接放入老年代了.所以老年代存的一般都是GC多次都没被回收的对象.当然,如果对象比较大伊甸园区内存不足的话也可以被直接分配到老年代(tenured).

    2.3,结构详解

    2.3.1 线程共享区

    堆区 :
    1.虚拟机启动时创建,用来存储对象实例,被所有线程共享.
    2.堆内存heap分为新生代(包括伊甸园eden区和幸存区survivor)和老年代(tenured)
    3.也是垃圾回收器(GC)主要的工作区域
    4.此时,如果你对堆内存又了一定了解,想做一些堆内存的优化的话,你需要调整以下参数来调整堆内存的大小:

    -Xmx设置堆的最大空间大小。
    -Xms设置堆的最小空间大小。
    -XX:MaxNewSize设置新生代最大空间大小。
    -XX:NewSize设置新生代最小空间大小。
    -XX:NewRatio 新生代和老年代的比值,值为4 则表示新生代:比老年代1:4
    -XX:SurivorRatio 表示Survivor和eden的比值,值为8表示两个survivor:eden=2:8.
    -Xss:设置每个线程的堆栈大小(值越大那么容纳的线程数就越少能处理的吞吐量就越少)。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    方法区:
    1.存储已被jvm加载了的数据(类的信息,常量,静态变量)
    2.存储运行时常量池(加载类文件的常量池,动态生成的常量)
    3.随着jdk版本的不同,实现方式也会有所改变
    4.如果你想优化方法区那么可以参考调整以下参数:

    -XX:MetaspaceSize 设置元数据区最小空间。 (JDK8)
    -XX:MaxMetaspaceSize 设置元数据区最大空间。(JDK8)
    
    • 1
    • 2
    2.3.2 线程私有区

    程序计数器 :
    1. 线程启动时创建,并设置为线程私有
    2. 用于记录当前正在执行的虚拟机字节码指令的地址
    3. Java虚拟机规范里唯一一个没有内存溢出的区域,只做计算
    :
    1.也叫栈帧stack,有一个先进后出的特点,分为栈顶和栈底,进栈的过程叫进栈/压栈,出栈的过程叫出栈/弹栈
    2.用于存储栈帧(stack frame)对象,保存方法里的局部变量,操作数栈,执行运行时常量池的引用等
    3.调用方法时会创建一个新的栈帧,方法执行完毕,栈帧出栈
    4.出现多次方法的递归调用时会出现栈内存溢出
    5. 虚拟机栈帧参数调整:

    -Xss128k: 设置每个线程的堆栈大小,原来是256K,JDK5后默认1M
    
    • 1

    本地方法栈:
    1.存放虚拟机使用到的native方法
    2.用于存储本地方法执行时的一些变量信息,操作数信息,结果信息等
    在这里插入图片描述

    2.4,JVM参数

    2.4.1 介绍

    当程序执行时出现了一些内存溢出的现象,如果你了解这个话题,那么可以合理调整JVM参数,从而达到延迟报错或者避免报错的发生.
    在这里插入图片描述

    2.4.2 分类

    三种参数类型

    1. 标准参数,jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变. 如 -version、-help
    2. X参数,默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容. 如 -Xms、-Xmx
    3. XX参数,此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用. 如-XX:+PrintGC

    XX参数数值类型

    1. 布尔类型,如-XX:+PrintGCDetails,其中 + 和 - 分别表示开启/关闭某个属性,-XX:+打开某属性 -XX:-关闭某属性,这里就是打开打印GC详情
    2. KV设值类型,如-XX:NewSize=256M,设置年轻代空间大小为256M
    2.4.3 堆内存JVM参数
    当代码在运行了一段时间以后会出现堆内存溢出( java.lang.OutOfMemoryError : Java heap space),可通过调整堆内存大小,延迟内存溢出的时间或者避免内存溢出现象。
    
    • 1
    -Xmx20m -Xms10m -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=e:/a.dump
    -Xmx20m    堆内存最大容量 
    -Xms10m    堆内存最小容量
    -XX:+HeapDumpOnOutOfMemoryError   设置如果出现指定的内存溢出现象
    -XX:HeapDumpPath=e:/a.dump        设置报错信息的导出的磁盘路径
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试实例:
    在这里插入图片描述

    2.4.4 元数据内存JVM参数
    当在有限的元数据内存区不断的加载新的类时会导致元数据区空间不足从而出现内存溢出现象(java.lang.OutOfMemoryError: Metaspace),例如:
    
    • 1
    -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
    -XX:MetaspaceSize=10M    元数据区最小容量
    -XX:MaxMetaspaceSize=10M   元数据区最大容量
    
    • 1
    • 2
    • 3
    2.4.5 对象GC的JVM参数

    大部分新创建的对象会分配在年轻代内存,进行参数调整查看不同内存大小何时GC的次数和耗时

    -XX:+PrintGCDetails -XX:+PrintGCTimeStamps  -Xmx1G -Xms1G -Xmn100M
    -XX:+PrintGCDetails  #打印GC的详细信息
    -XX:+PrintGCTimeStamps  #打印GC的时间戳
    -Xmx1G #最大堆内存
    -Xms1G  #最小堆内存
    -Xmn500M  #最大年轻代内存(包括伊甸园区和幸存区)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试实例:
    在这里插入图片描述
    调整参数,调成新生代最大4M时,暂观察GC情况
    在这里插入图片描述

    2.4.6 类加载的JVM参数
    -XX:+TraceClassLoading  用来观察类加载的顺序过程等
    
    • 1

    在这里插入图片描述

    2.4.7 JVM参数合集

    常用的X参数

    -Xms: 最小堆内存(初始值),如: -Xms512M
    -Xmx: 最大堆内存,如: -Xmx512M
    -Xmn: 新生代大小,如: -Xmx256M
    -Xss: 指定线程栈大小,如: -Xmx128K
    
    • 1
    • 2
    • 3
    • 4

    常用的XX参数

    -XX:NewSize:设置年轻代最小空间大小,如-XX:NewSize=256M
    -XX:MaxNewSize:设置年轻代最大空间大小,如-XX:MaxNewSize=256M
    -XX:PermSize:设置永久代最小空间大小
    -XX:MaxPermSize:设置永久代最大空间大小
    -XX:NewRatio:设置年轻代和老年代的比值。默认值-XX:NewRatio=2,表示年轻代与老年代比值为1:2,年轻代占整个堆大小的1/3
    -XX:SurvivorRatio:设置年轻代中Eden区Survivor区的容量比值。默认值-XX:SurvivorRatio=8,表示Eden : Survivor0 : Survivor1 = 8 : 1 : 1
    -XX:+HeapDumpOnOutOfMemoryError:表示当JVM发生OOM时,自动生成DUMP文件。
    -XX:HeapDumpPath=/usr/local/dump:dump文件路径或者名称。
    -XX:+PrintGCDetails 打印GC详情
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    友情提示: 你可以在命令行窗口中,尝试执行java -version / java -help / java -X 其中,你会发现一些jvm参数的含义…

    2.5,JVM监测工具

    2.5.1 jps命令

    用于查看有权访问的hotspot虚拟机的进程,当未指定hostid时,默认查看本机jvm进程

    jps -l 输出main的类名
    jps -v 输出传入JVM的参数
    
    • 1
    • 2

    在这里插入图片描述

    2.5.2 jmap命令

    用于打印指定Java进程的对象内存映射或堆内存细节

    jmap -heap 1844  进程的堆信息,如果查询时抛出了异常就需要修改运行时jre的版本和jdk的版本一致即可
    
    C:\Users\WuQiong>jmap -heap 1844
    Attaching to process ID 1844, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.45-b02
    
    using thread-local object allocation.
    Parallel GC with 4 thread(s)
    Heap Configuration:                                                                                                                      MinHeapFreeRatio         = 0     
    #GC后如果发现空闲堆内存小于整个预估堆内存的N%(百分比), 则JVM会增大堆内存,但不能超过-Xmx指定的最大堆内存限制   
    MaxHeapFreeRatio         = 100   
    #最大空闲堆内存比例,GC后如果发现空闲堆内存大于整个预估堆内存的N%(百分比),JVM则会收缩堆内存,但不能小于最小堆的限制                                                                                                    
    MaxHeapSize              = 2118123520 (2020.0MB) #即-Xmx, 堆内存大小的上限
    NewSize                  = 44564480 (42.5MB) #新生代预估堆内存占用的默认值
    MaxNewSize               = 705691648 (673.0MB)  #新生代占整个堆内存的最大值
    OldSize                  = 89653248 (85.5MB)    #老年代的默认大小
    NewRatio                 = 2 #老年代对比新生代的空间大小, 比如2代表老年代空间是新生代的两倍大小                                                                                                         
    SurvivorRatio            = 8 #Eden/Survivor的值. 例如8表示Survivor:Eden=1:8, 因为survivor区有2个, 所以Eden的占比为8/10. 
    MetaspaceSize            = 10485760 (10.0MB)
    #类元数据空间的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark ). 此值为估计值. MetaspaceSize设置得过大会延长垃圾回收时间. 垃圾回收过后, 引起下一次垃圾回收的类元数据空间的大小可能会变大
    CompressedClassSpaceSize = 1073741824 (1024.0MB) #类指针压缩空间大小, 默认为1G.
    MaxMetaspaceSize         = 10485760 (10.0MB)#是分配给类元数据空间的最大值, 超过此值就会触发Full GC. 此值仅受限于系统内存的大小, JVM会动态地改变此值
    G1HeapRegionSize         = 0 (0.0MB) #G1区块的大小, 取值为1M至32M. 其取值是要根据最小Heap大小划分出2048个区块.
    Heap Usage:                                                                                                                           PS Young Generation
    Eden Space:
       capacity = 439353344 (419.0MB)
       used     = 114234848 (108.94284057617188MB)
       free     = 325118496 (310.0571594238281MB)
       26.00067794180713% used
    From Space:
       capacity = 524288 (0.5MB)
       used     = 0 (0.0MB)
       free     = 524288 (0.5MB)
       0.0% used
    To Space:
       capacity = 524288 (0.5MB)
       used     = 0 (0.0MB)
       free     = 524288 (0.5MB)
       0.0% used
    PS Old Generation
       capacity = 89653248 (85.5MB)
       used     = 1204736 (1.14892578125MB)
       free     = 88448512 (84.35107421875MB)
       1.3437728435672514% used
    
    3048 interned Strings occupying 251608 bytes.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在这里插入图片描述

    2.5.3 jstack命令

    用于生成java虚拟机当前时刻的线程快照,主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待

    jstack -l 22345  查询指定线程的停顿原因,可以检测出死锁问题,定位到具体行号
    
    • 1
    2.5.4 GUI可视化工具

    jconsole命令 :用图形化界面展示线程的内存情况

    C:\Users\Administrator>jconsole
    #直接输入命令后,选择你想要查看的程序--点击连接--点击不安全连接 即可查看
    
    • 1
    • 2

    在这里插入图片描述
    另外,JVM可视化工具市面上有一些,MAT/VisualVM,他们集成了一些JVM命令行的可视化界面的工具,分析能力强,也可以方便快捷的查看Java程序的相关内存信息.,效果同上.

    3,类加载的机制

    3.1 概述

    Class 文件需要加载到Java虚拟机中后才能运行和使用,那么,这些 Class 文件被Java虚拟机加载的过程就是所谓的类加载的过程.
    如果你还不知道你的类是何时被加载的,类中的成员何时被加载的初始化的和何时被使用的等等,那么你就该认真了解以下内容,方便以后的系统调优,减少类的加载过程来提高应用的性能.
    在这里插入图片描述

    3.2 类的生命周期

    从我们开始使用类,到类被使用完卸载类的过程,就是类的生命周期.整个周期主要有三步: 加载 -> 连接 -> 初始化,
    连接过程又可分为三步: 验证 -> 准备 -> 解析, 所以一共分为5大阶段分别是: 加载 -> 验证 -> 准备 -> 解析 -> 初始化.
    通过以上5大阶段就可以把类从磁盘或网络读到JVM内存,然后交给执行引擎执行并使用了.
    如果说还有其他过程,那就是,使用阶段和卸载阶段了.
    在这里插入图片描述

    3.3 生命周期-加载阶段loading

    3.3.1 概述

    我们知道类的加载过程中可分为加载、验证、准备、解析、初始化 5大阶段,那他们的执行顺序是什么?
    JVM规范中是这样说的:

    1. 加载、验证、准备和初始化发生的顺序是确定的,而解析阶段则不一定.
    2. 加载、验证、准备和初始化这四个阶段按顺序开始不一定按顺序完成.
    3.3.2 类加载的流程
    1. 通过给定的类的全名(包名.类名)来获取其定义的二进制字节流
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    3. 在Java的堆内存中生成一个代表这个类的java.lang.Class对象(方便反射),作为对方法区中这些数据的访问入口
      在这里插入图片描述
    3.3.3 类加载的方式

    通过类加载的机制,可以把JDK类库里定义的类,第三方类库的类,和你自己定义的类,全都加载进JVM内存中.但是,这些类是用什么方式进行加载的呢? 你可以配置JVM参数 -XX:+TraceClassLoading 来查看.
    隐式加载

    1. 访问类的静态成员(例如类变量,静态方法)
    2. 构建类的实例对象(例如使用new 关键字构建对象)
    3. 构建子类实例对象(构建类的对象时首先会加载父类类型)
      显式加载
    4. 使用类加载器对象加载指定的类: ClassLoader.loadClass(…)
    5. 使用反射机制反射指定的类: Class.forName(…)

    3.4 生命周期-连接阶段linking

    3.4.1 概述

    连接阶段主要分为三部分: 验证 准备 解析.

    3.4.2 验证

    主要就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全.主要完成4个检验:

    1. 文件格式的验证
    2. 元数据验证
    3. 字节码合法性验证
    4. 符号引用验证(Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info等常量形式出现)
      说明:验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,想要减少验证缩短虚拟机类加载的时间,那么可以考虑采用JVM参数 -Xverify:none 来关闭大部分的类验证措施.
    3.4.3 准备

    准备阶段是正式为 类变量分配内存并设置类变量默认值的阶段,这些内存都将在方法区中分配.

    1. 给类变量(static的)内存分配
    2. 按类型进行初始分配默认值(如0、0.0、null、false等)
      思考: 如果有这样一段代码 public static int val= 3;那么此时变量val的值到底会设置成什么呢?
      答案: 当前阶段会被设置为0,而不是3,下一个初始化阶段才会val赋值为3(也可以理解成一个隐式赋值的动作).
    3. 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段val就会被直接初始化为ConstValue属性所指定的值。
      思考: 如果有这样一段代码 public static final int val = 3;那么此时变量val的值到底会设置成什么呢?
      编译时Javac将会为val生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将val直接赋值为3.
    3.4.4 解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,其中:

    1. 符号引用:就是一组符号(例如CONSTANT_Fieldref_info)来描述目标,可以是任何字面量
    2. 直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
      说明:相同的符号引用,不同JVM机器上对应的直接引用可能不同,直接引用一般对应已加载到内存中的一个具体对象.

    3.5 生命周期-初始化阶段initialization

    3.5.1 概述

    类加载的最后一个阶段,其他阶段都是JVM来操作主导的,在这个阶段我们可以让自己定义的类加载器参与进来.
    所谓类的初始化阶段,主要是 初始化类变量,初始化类的静态资源(静态域)等 ,类变量初始化值有两种方式:

    1. 声明类变量时直接指定初始值
    2. 使用静态代码块为类变量指定初始值
      注意:只有当对类主动使用的时候,才会导致类的初始化.
    3.5.2 关于类的主动使用和被动使用

    Java程序中,对类的使用方式可以分为两种:
    主动使用:会执行加载、连接、并初始化静态域
    被动使用:只执行加载、连接,不初始化类静态域
    类的主动引用

    1. 虚拟机启动时,main方法所在类
    2. new一个对象
    3. 调用类变量(final常量除外)和静态方法(可以是final方法)
    4. 反射调用

    类的被动引用(不会发生类的初始化)
    5. 当访问一个静态域时,只有真正声明这个域的类才会被初始化
    6. 当通过子类引用父类的静态变量时,不会导致子类初始化
    7. 通过数组定义类引用,不会导致类的初始化
    8. 通过static final修饰的常量, 不会导致类的初始化
    9. 使用类加载器时,也不会导致类的初始化
    思考: 如果 通过子类引用父类的静态字段,此时,自雷会被初始化吗?
    答案: 属于子类的被动使用,不会导致子类初始化

    3.5.3 测试案例
    public class jvm {
        public static void main(String[] args) throws Exception {
            //类的主动调用: new/反射/调用类成员或方法
    //        new Son();//输出啥呢?父类static{}被加载,子类static{}被加载
    //        Class.forName("cn.tedu.test.Son");//输出啥呢?父类static{}被加载,子类static{}被加载
    //        System.out.println(Son.s_static_field);//输出啥呢?父类static{}被加载,子类static{}被加载,857
    //        Son.s_static_method();//输出啥呢?父类static{}被加载,子类static{}被加载
            //类的被动调用: 访问静态域(谁声明的才初始化谁)
    //        System.out.println(Son.f_static_field);//输出啥呢?父类static{}被加载 ,666
    //        Son.f_static_method();//输出啥呢?父类static{}被加载 ,方法被执行
            Son[] sons = new Son[3];//输出啥呢?啥也木有,都是被动加载的
        }
    }
    class Father {
        static int f_static_field = 666;
        static {
            System.out.println("父类static{}被加载");
        }
        static  void f_static_method(){
            System.out.println("f_static_method()");
        }
    }
    class Son extends Father {
        static {
            System.out.println("子类static{}被加载");
        }
        static int s_static_field = 857;
        public static void s_static_method() {  }
        public static final void s_static_final_method() { }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    3.6 自定义类加载器

    3.6.1 概述

    类加载器是指一个对象,它在类运行时把class文件读到Java虚拟机里.其类型为ClassLoader类型,此类型为抽象类型,通常以继承形式出现.
    类加载器对象常用方法说明:

    1. getParent() 返回类加载器的父类加载器
    2. loadClass(String name) 加载名称为 name的类
    3. findClass(String name) 查找名称为 name的类
    4. findLoadedClass(String name) 查找名称为 name的已经被加载过的类
    5. defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类
    3.6.2 分类

    Java中,类加载器大致可分为两种: 一类是系统提供的,一类是自己定义的.
    系统提供的有四种:

    1. BootstrapClassLoader: c/c++代码实现的加载器,用于加载JDK的核心类库rt.jar里的类
    2. ExtClassLoader: 用于加载Java的扩展类,提供除系统类之外的功能
    3. AppClassLoader: 应用程序类加载器,加载当前程序的类库
    4. CustomClassLoader: 自定义加载器,扩展加载器的功能
      在这里插入图片描述
    3.6.3 加载类的过程
    1. 首先会查看自己是否已加载过此类,有则返回. 如果没有加载,则不是自身去查找而是委托父类类加载器进行查找,依次递归
    2. 直到委托到BootstrapClassLoader,如果BootstrapClassLoader找到了就会直接返回,如果没有找到,则继续向下交给子类类加载器查找
    3. 如果还没找到最后交由自身查找. 如果谁都没有完成加载时,就会抛出异常
      说明:类加载时,首先委托父类加载的这种机制称之为双亲委派机制.
      基于这种机制,实现了类加载在查找时的优先级关系,比较安全,同时也可以保证一个类只会被一个加载器加载(例如Object类只会被BootstrapClassLoader加载),避免重复加载,这样更有利于java程序的稳定高效的运行.
    3.6.4 自定义类加载器
    3.6.4.1 作用

    JVM自带的类加载器,只能加载默认classpath下的类,如果我们需要加载应用程序路径之外的类文件 或者 是网络中的一些文件呢?这就要使用自定义加载器了.
    因为自定义类加载器可以自己指定类的路径,也更加个性化,所以框架等等一些技术中都有自定义的类加载器.
    将来还可以实现动态读取指定目录下的新类,来更新原来类的内容,这个过程就是系统在线升级技术也叫热替换.

    3.6.4.2 继承ClassLoader

    准备源文件
    在这里插入图片描述
    MyClassLoader类
    核心类,核心功能就是,可以读取用户自己指定的路径下的类文件,并变成Class对象,方便后续反射操作

    package cn.tedu.test;
    
    import java.io.*;
    
    //自定义类加载器:
    //1. 继承ClassLoader
    //2. 重写findClass(),加载自定义路径下的类文件
    public class MyClassLoader extends ClassLoader{
    
        //指定要加载的文件的基础路径
        private String baseDir;//比如:E:\workspace
        public MyClassLoader(String baseDir) {
            this.baseDir=baseDir;
        }
    
        /**
         * 读取指定路径下的类文件,并变成Class对象(你现在只有class文件,只能通过Class反射操作了)
         * param: name要加载的类的全路径(包名.类名)
         */
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            String fileName = baseDir + name.replace('.', File.separatorChar) + ".class";
            System.out.println("fileName=" + fileName);//文件的完整磁盘路径
            try(
                InputStream ins= new FileInputStream(fileName);//读取流
                ByteArrayOutputStream baos = new ByteArrayOutputStream();//写出流
                ) {
                int bufferSize = 1024;
                byte[] buffer = new byte[bufferSize];//缓冲数组
                int length = 0;
                while ((length = ins.read(buffer)) != -1) {//每次读1024字节
                    baos.write(buffer, 0, length);  //把读到的写出去
                }
                byte[] datas = baos.toByteArray(); //把数据存入字节数组中
    
                System.out.println("操作成功!");
                //使用父类方法,把数组里的数据转成Class对象
                return super.defineClass(name, datas, 0, datas.length);
            } catch (IOException e) {
                System.out.println("操作异常!");
            }
            return null;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    3.6.4.3 准备目标类,用来测试

    目的是测测这个类,能不能成功使用自定义类加载器
    HelloClassLoader类

    package cn.tedu.test;
    //TODO: 去你的工作空间里,找到这个类的字节码文件HelloClassLoader.class,拷贝到其他路径去
    // 注意:目录新路径的结构要和现在的结构一致(cn.tedu.test)
    public class HelloClassLoader {
        static {
            System.out.println("I'm cn.tedu.test.HelloClassLoader static{}");
        }
    
        public HelloClassLoader() {
            System.out.println("I'm cn.tedu.test.HelloClassLoader constructor()");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    3.6.4.4 准备测试类

    测试自定义类加载器的使用
    TestMyClassLoader类

    package cn.tedu.test;
    //测试加载指定的类时,使用的是自定义的类加载器还是JVM提供的.
    public class TestMyClassLoader {
        public static void main(String[] args) throws Exception, InstantiationException {
            //指定一个基础路径,注意,这个路径,不能在当前路径下,否则,当前路径下有这个文件时,就会使用JVM默认的类加载器AppClassLoader了..
            MyClassLoader my = new MyClassLoader("E:\\testworkspace\\");//自定义的新路径
            //拼接类路径(包名.类名)
            Class<?> myclass = my.findClass("cn.tedu.test.HelloClassLoader");//要和复制时的路径保持一致
            Object instance = myclass.newInstance();
            //查看当前实例的class对象HelloClassLoader
            System.out.println(instance.getClass());
            //查看当前实例用的哪个类加载器?MyClassLoader
            System.out.println(instance.getClass().getClassLoader());
    
    //        问题:自定义类加载器成功了,那你还会用默认的类加载器吗?
    //        new HelloClassLoader().getClass().getClassLoader()
    //       你会发现这样写也可以成功加载类并执行,此时使用的类加载器呢?是默认的AppClassLoader
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    3.6.4.5 继承URLClassLoader

    URLClassLoader已经继承了ClassLoader,可以从指定目录,jar包,网络中加载指定的类资源。
    MyClassLoader2

    package cn.tedu.test;
    
    import java.net.URL;
    import java.net.URLClassLoader;
    
    //自定义类加载器,这种方式很简单,直接提供含参构造就行
    public class MyClassLoader2 extends URLClassLoader {
        public MyClassLoader2(URL[] urls) {
            super(urls);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    TestMyClassLoader2 测试新的类加载器

    package cn.tedu.test;
    
    import java.io.File;
    import java.net.URI;
    import java.net.URL;
    
    public class TestMyClassLoader2 {
        public static void main(String[] args) throws Exception {
            File file=new File("f:\\testworkspace\\");
            //File to URI
            URI uri=file.toURI();
            URL[] urls={uri.toURL()};
            ClassLoader classLoader = new MyClassLoader2(urls);//利用新的类加载器加载指定位置文件
            Class<?> cls = classLoader.loadClass("cn.tedu.test.HelloClassLoader");
            Object obj = cls.newInstance();
            System.out.println(obj);
            System.out.println(classLoader);//MyClassLoader2
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4,垃圾收集器GC

    4.1 概述

    基于正在使用的对象进行遍历,对存活的对象进行标记,未标记对象可认为是垃圾对象,然后基于特定算法进行回收,这个过程就叫GC.
    GC全称Garbage Collection,所有的GC系统都从如下几个方面进行实现:
    1.GC判断策略(例如引用计数,对象可达性分析)
    2.GC收集算法(标记-清除,标记-清除-整理,标记-复制-清除)
    3.GC收集器(例如Serial,Parallel,CMS,G1)
    深入理解GC的工作机制,可以帮你写出更好的Java应用程序,更好的优化应用程序等.

    4.2 分类

    4.2.1 手动GC

    即显式地进行内存分配(allocate)和内存释放(free).
    如果忘记释放, 则对应的那块内存不能再次使用.
    如果内存一直被占着,却不能使用,这种情况就会产生内存泄漏(memory leak).
    比如:c/c++程序中就需要程序员,自己完成对象的回收动作.回收晚了(或者忘了)会导致内存泄漏,回收早了对象还没用完呢,就会报错.

    4.2.2 自动GC

    在Java程序中,对于对象的回收问题得到了很好的解决.
    Java会自动GC,一般是在JVM系统内存不足时,由JVM系统启动GC对象,自动对内存进行垃圾回收及时释放内存.
    JVM会对程序中,已产生的对象进行引用计数, 比如: a对象被引用了2次,b对象被引用了1次,c对象被引用了0次,JVM就会标记这个c对象是需要被清除的对象,并启动GC把c对象清除掉.

    注意: JVM包含着多种GC算法收集器.但是无论采用哪种算法,GC一般包含两个步骤: 标记和清除,这也是GC经常使用的标记-清除算法.
    标记清除就是对可达对象进行标记,不可达对象即认为是垃圾,然后进行清除.

    1. 标记所有可到达的对象
    2. 清除不可到达对象占用的内存地址

    4.3 GC常用概念

    4.3.1 碎片整理

    你整理过操作系统的磁盘碎片么?
    在这里插入图片描述
    那么Java也需要,碎片的整理:
    系统GC时,每次执行清除操作,JVM 都必须保证“不可达对象“占用的内存能被回收然后重用.在回收内存这个过程中有可能会产生大量的内存碎片(类似于磁盘碎片), 进而引发两个问题:

    1. 对象创建时,执行写入操作越来越耗时, 因为很难找到一块足够大的空闲.
    2. 对象创建时,JVM需要在连续的内存块中为对象分配内存. 如果碎片问题很严重, 甚至没有空闲片段能存放新创建的对象,就会发生内存分配错误(allocation error).
    3. 为了解决碎片问题,JVM在启动GC执行垃圾收集的过程中不仅仅是标记和清除, 还需要执行 内存碎片整理.这个过程会让所有可达对象(reachable objects)进行依次移动,进而可以消除或减少内存碎片,并为新对象提供更大更多的连续的内存空间.
      在这里插入图片描述
    4.3.2 分代设想

    我们知道垃圾收集时要停止整个应用程序的运行,那么假如这个收集过程需要的时间很长,就会对应用程序产生很大性能问题.如何解决这个问题呢?通过科学家实验发现,内存中的对象通常可以将其分为两大类:

    1. 存活时间较短的(这样的对象比较多)
    2. 存活时间较长的(这样的对象比较少)
      基于对如上问题的分析,科学家提出了分代回收思路,将JVM中内存分为年轻代(Young Generation)和老年代(Old Generation).
      分代设想将内存拆分为两个可单独清理的区域,允许采用不同的算法来大幅提高GC的性能.
      但这种方法也不是没有问题的,例如: 在不同分代中,可能存在互相引用的对象, 这样也是难以回收.
      在这里插入图片描述
    4.3.3 对象分配

    观察JAVA中,堆内存的内存结构图:
    在这里插入图片描述
    基于此内存架构,对象内存分配过程如下:

    1. 编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配
    2. 如果是在堆上分配,则首先检测是否可在TLAB上直接分配
      TLAB :全称 Thread Local Allocation Buffer,即线程本地分配缓存.是一块 线程专用 的内存分配区域
    3. 如果TLAB上无法直接分配,则在Eden加锁区(线程共享区),进行分配
    4. 如果Eden区无法存储对象,则执行Young GC
    5. 如果Young GC之后Eden区仍然不足以存储对象,则直接分配在老年代
      说明:在对象创建时可能会触发Young GC,此GC过程的简易原理图分析如下:
      在这里插入图片描述
      其中:
    6. 新生代由Eden 区和两个幸存区构成(假定为s1,s2), 任意时刻至少有一个幸存区是空的(empty),用于存放下次GC时未被收集的对象
    7. GC触发时Eden区所有”可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代
    8. GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2,同时清空eden区和s1幸存区
    9. GC再次触发时Eden区和s2幸存区中的”可达对象”会被复制到另一个幸存区s1,同时清空eden区和s2幸存区.依次类推
    10. 当多次GC过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参数 -XX:+MaxTenuringThreshold 来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代.
    4.3.4 GC模式分析

    垃圾收集事件(Garbage Collection events)通常分为:

    1. Minor GC (小型GC):年轻代GC事件,(新对象)分配频率越高, Minor GC 的频率就越高
    2. Major GC (大型GC): 老年代GC事件
    3. Full GC (完全GC):整个堆的GC事件
      说明:一般情况下可以将Major GC与Full GC看成是同一种GC

    4.4 GC算法

    4.4.1 标记为可达对象

    现在的GC算法,基本都是要从标记可达对象开始,标记为可达的对象即为存活对象.
    同时,我们可以将 查找可达对象时 的起始位置对象 认为是根对象(Garbage Collection Roots).
    基于根对象标记可访问或可达对象,对于不可达对象,GC会认为是垃圾对象.
    在这里插入图片描述
    例如: 上图中,绿色云朵为根对象,蓝色圆圈为可达对象,灰色圆圈为垃圾对象
    首先,GC遍历内存中整体的对象关系图(object graph)确定根对象,那什么样的对象可作为根对象呢?
    GC规范中指出根对象可以是:

    1. 栈中变量直接引用的对象
    2. 常量池中引用的对象
      其次,确定了根对象以后,进而从根对象开始进行依赖查找,所有可访问到的对象都认为是存活对象,然后进行标记
      说明:标记可达对象需要暂停所有应用线程, 以确定对象的引用关系。其暂停的时间, 与堆内存大小、对象的总数 没有直接关系, 而是由存活对象的数量来决定。
    4.4.2 移除不可达对象

    移除不可达对象时,会因GC算法的不同而不同,但是大部分的GC操作一般都可大致分为三步:清除(Mark-Sweep),标记清除整理(Mark-Sweep-Compact),标记复制(Mark-Copy).
    标记-清除(Mark-Sweep)
    对于标记清除算法而言,应用相对简单,但会产生大量的内存碎片,这样再创建大对象时,内存没有足够连续的内存空间可能会出现OutOfMemoryError.
    在这里插入图片描述
    标记-清除-整理(Mark-Sweep-Compact)
    标记清除整理算法中,在清除垃圾对象以后,会移动可用对象,对碎片进行压缩,这样会在内存中构建相对比较大的连续空间便于大对象的直接存储,但是会增加GC暂停时间.
    在这里插入图片描述
    标记-复制 (Mark and Copy)
    标记复制算法会基于标记清除整理算法,但是会创建新的内存空间用于存储幸存对象,同时复制与标记可以同时并发执行,这样可以较少GC时系统的暂停时间,提高系统性能.
    在这里插入图片描述

    4.5 算法分析

    4.5.1 GC常见的几种算法

    JVM系统在运行时,因新对象的创建可能会触发GC事件.无论哪种GC都可能会暂停应用程序的执行,但如何将暂停时间降到最小,这要看我们使用的GC算法.
    现在对于JVM中的GC算法无非两大类: 一类负责收集年轻代,一类负责收集老年代.
    假如没有显式的指定垃圾回收算法,一般会采用系统平台默认算法(当然也可以自己指定),例如JDK8中基于特定垃圾回收算法的垃圾收集器应用组合如下:
    在这里插入图片描述
    其中:

    1. 年轻代和老年代的串行收集器: Serial GC
    2. 年轻代和老年代的并行收集器: Parallel GC
    3. 年轻代的并行收集器 Parallel New + 老年代的并发收集器CMS(Concurrent Mark and Sweep)
    4. 年轻代和老年代的G1收集器
      说明: 除了以上几种组合方式外,其它的组合方式要么现在已经不支持,要么不推荐.那么,到底要如何对这些组合进行选择,要结合系统的特点.
      例如: 系统是追求高吞吐量还是响应时间,还是两者都要兼顾.总之,对于GC的组合没有最好,只有更好.结合当前系统的环境配置,性能指标以及GC器特点,不断进行GC日志分析,定位系统问题,才是一般是选择哪种GC的关键.
    4.5.2 串行收集器Serial GC

    Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5 和 JAVA SE6 中客户端虚拟机采用的默认配置.
    JVM也提供了其应用的参数配置:java -XX:+UseSerialGC

    特点有两个:

    1. 内部只使用一个线程执行垃圾回收,不能充分利用CPU的多核特性,无法并行化.
    2. GC时所有正在执行的用户线程暂停,并且可能会产生较长时间的停顿,用户的响应时间长
      应用场景:
    3. 一般可工作在JVM的客户端模式
    4. 适用于CPU个数或核数较少且内存空间较小的场景,因为内存大的话响应更慢了
      采用的算法
    5. 新生代使用 标记-复制 算法 (因为新生代存活对象较少,也就是复制的需求少,这样相对高效)
    6. 老年代使用 标记-清除-整理 算法 (因为GC容易产生碎片,而老年代对象回收较少,这样相对高效)
      实践应用
      在这里插入图片描述总之,Serial GC是一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程。
      适合单CPU小应用,实时性要求不是那么高场景。
    4.5.3 并发收集器CMS

    CMS的英文名称为 “Mostly Concurrent Mark and Sweep Garbage Collector”,其设计目标是追求更快的响应时间。
    特点:

    1. 使用空闲列表(free-lists)管理内存空间的回收,不对老年代进行碎片整理,减少用户线程的暂停时间
    2. 在 标记-清除 阶段的大部分工作和用户线程一起并发执行,提高效率
    3. 最大优点是 可减少停顿时间(提高响应速度),最大缺陷是 会产生老年代的内存碎片
      应用场景:
    4. 应用于多个或多核处理器,目标降低延迟,缩短停顿时间,提高响应时间
    5. CPU受限场景下,CMS收集器的线程 会和 用户线程 竞争CPU,导致吞吐量减少
      算法应用:
    6. 年轻代采用并行方式的 标记-复制 算法
    7. 老年代主要使用并发的 标记-清除 算法
      关键步骤分析:
    8. 初始标记, 此阶段标记一下GC Roots能直接关联到的对象,这个过程很快
    9. 并发标记, 此阶段就是进行GC Roots Tracing的过程,从直接关联对象遍历所有可达对象,然后进行标记
    10. 重新标记, 此阶段要修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的记录
    11. 并发清除, 此阶段与应用程序并发执行,不需要STW停顿. 目的是删除未使用的对象,并收回他们所占用的空间
    12. 并发重置, 此阶段与应用程序并发执行,重置CMS算法相关的内部数据,同时GC线程切换到用户线程
      实践应用:
      在这里插入图片描述
      也可以使用JVM提供的观察CMS的参数配置: java -XX:+UseConcMarkSweepGC ,默认开启-XX:+UseParNewGC
      其它参数配置:
    13. -XX:+UseCMSCompactAtFullCollection 执行Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
    14. -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
    15. -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
      总之, CMS垃圾收集器在减少停顿时间上做了很多给力的工作, 大量并发执行的工作并不需要暂停应用线程。
      如果服务器是多核CPU,并且主要调优目标是降低延迟, 那么使用CMS是个很明智的选择。
      CMS垃圾收集可减少每一次GC停顿的时间,提高系统的灵敏度和响应速度,但是因为多数时候都有部分CPU资源被GC消耗, 所以在CPU资源受限的情况下,CMS会比并行GC的吞吐量差一些。还有老年代内存碎片问题, 在某些情况下GC会造成不可预测的暂停时间, 特别是堆内存较大的情况下。
    4.5.4 并行收集器Parallel GC

    并行收集器,它可利用多个或多核CPU优势实现多线程并行GC操作,其目标是减少停顿时间,以实现更高的吞吐量.
    特点:

    1. 可利用CPU的多核特性执行多线程下的并行化GC操作
    2. GC期间, 所有CPU内核都在 并行 清理垃圾, 所以暂停时间较短
    3. 最大优势是 可实现可控的吞吐量与停顿时间
      应用场景:
    4. GC操作仍需暂停应用程序(也有可能暂停时间比较长,因为GC阶段不能被打断),所以不适合要求低延迟的场景
    5. 因其高吞吐GC量的特性,适用于后台计算、后台处理的弱交互场景而不是web交互场景
      算法应用:
    6. 在年轻代, 使用 标记-复制 算法,对应的是Parallel Scavenge收集器
    7. 在老年代, 使用 标记-清除-整理 算法,对应的是Parallel Old收集器
      实践应用:
      在这里插入图片描述
      也可以使用JVM提供的参数,观察Parallel GC:-XX:+UseParallelGC ,默认开启-XX:+UseParallelOldGC
      其它参数配置:
    8. -XX:ParallelGCThreads=20 设置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收.此值最好配置与处理器数目相等
    9. -XX:MaxGCPauseMillis=100 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
    10. -XX:+UseAdaptiveSizePolicy 设置并行收集器自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开
    11. -XX:GCTimeRatio=99,设置吞吐量大小,默认值就是99,也就是将垃圾回收的时间设置成了总时间的1%。它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集
      总之,Parallel GC是一种并行收集器,可利用多CPU优势,执行并行GC操作,提高了吞吐量.而且可以提供JVM参数来控制吞吐量.
      并可有效降低工作线程暂停时长。但是因为垃圾收集的所有阶段都不能被打断,所以Parallel GC还是有可能导致长时间的应用暂停。
      所以Parallel GC适合于需要高吞吐量而对暂停时间不敏感的场合,比如批处理任务
      说明:所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    4.5.5 G1收集器

    G1(Garbage-First )收集器是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器.
    G1 在实现高吞吐的同时,也最大限度满足了GC 停顿时间可控的.
    在JDK7 update 4 后续的版本中已全面支持G1回收器功能.
    特点:

    1. 可以像CMS 收集器一样能同时和应用线程一起并发的执行
    2. 减少了整理内存空间时的停顿时间
    3. 满足了可控的GC停顿时间需求
    4. 不能牺牲太多的吞吐性能
      未来 G1 计划要全面取代CMS.G1相比CMS有更多的优势:
    5. G1是压缩型收集器,可以实现更有效的空间压缩,消除大部分潜在的内存碎片问题
    6. G1提供了更精准的可预测的垃圾停顿时间设置,可满足用户在指定垃圾回收时间上的需求
      在G1中,堆不再分成连续的年轻代和老年代空间,而是划分为多个(通常是2048个)可以存放对象的小堆区.
      每个小堆区都可能是Eden区, Survivor区或者Old区. 在逻辑上, 所有的Eden区和Survivor区合起来就是年轻代, 所有的Old区拼在一起那就是老年代,如下图所示:
      在这里插入图片描述
      这样的划分使得GC不必每次都去收集整个堆空间, 而是以增量的方式来处理.
      GC时每次只处理一部分小堆区,称为此次的回收集.
      GC事件的每次暂停都会收集所有年轻代的小堆区, 同时也可能包含一部分老年代小堆区,如下图所示:
      在这里插入图片描述
      G1以一种和CMS 相似的方式执行垃圾回收.
      G1在并发阶段估算每个小堆区存活对象的总数,垃圾最多的小堆区会被优先收集,这也是G1名称的由来.
      顾名思义,G1将其收集和压缩活动集中在堆中可能充满可回收对象的区域上,G1通过用停顿预测模型来满足用户自定义的停顿时间目标,它基于设定的停顿时间来选择要回收的regions数量.
      G1基于标记,清理对应的regions时,会将对象从一个或多个region里复制到另一个region里,在这个过程中会伴随着压缩和释放内存.
      清理过程在多核机器上都采用并行执行,来降低停顿时间,增加吞吐量.
      因此,G1在持续的运行中能减少碎片,满足用户自定义停顿时间需求。
      这种能力是以往的回收器所不具备的(例如 CMS回收器不能进行碎片压缩,ParallelOld 只能进行整堆的压缩并且会导致较长的停顿时间).

    再次强调:G1不是一个实时的收集器,它只是最大可能的来满足设定的停顿时间。G1会基于以往的收集数据,来评估用户指定的停顿时间可以回收多少regions,需要花费的时间,然后确定停顿时间内可以回收多少个regions.
    特点:
    7. 将java堆均分成大小相同的多个区域(region,1M-32M,最多2000个,最大支持堆内存64G)
    8. 内存应用具备极大地弹性(一个或多个不连续的区域共同组成eden、survivor或old区,但大小不再固定)
    9. 相对CMS有着更加可控的暂停时间(pause time) 和 更大的吞吐量(throughput)以及更少的碎片(标记整理)
    10. 支持并行与并发,可充分利用多CPU,多核优势,降低延迟,提高响应速度
    场景应用分析:

    1. FullGC发生相对比较频繁或消耗的总时长过长
    2. 对象分配率或对象升级至老年代的比例波动较大
    3. 较长时间的内存整理停顿
      说明:如果你现在用CMS 或者 ParallelOldGC ,并且你的程序运行很好,没有经历长时间垃圾回收停顿,建议就不用迁移
      应用场景:
    4. 年轻代标记复制算法
    5. 老年代标记清除整理算法
      关键步骤应用分析:
    6. 初始标记(Initial Mark): 属于Young GC范畴,是停止活动。对持有老年代对象引用的Survivor区(Root区)进行标记
    7. 根区扫描(Root Region Scan):并发执行,扫描那些对old区有引用的Survivor区,在youngGC 发生之前该阶段必须完成
    8. 并发标记(Concurrent Mark):并发执行,找出整个堆中存活的对象,将空区域标记为”X”,此阶段也可能会被Young GC中断
    9. 再次标记(Remark):完全完成对heap存活对象的标记。采用SATB (snapshot-at-the-beginning) 算法完成,比CMS用的算法更快
    10. 清理(cleanup):并发执行,统计小堆区中所有存活的对象, 并对小堆区进行排序,优先清理垃圾多的小堆区,释放内存
    11. 复制/清理(copy/clean):对小堆区未被清理对象对象进行复制,然后再清理。
      实践应用分析:
      在这里插入图片描述
      也可以配置JVM参数来观察G1收集器: java-XX:+UseG1GC 表示启用GC收集器
      其它参数配置:
    12. -XX:MaxGCPauseMillis=200 - 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽力去达成这个目标. 所以有时候这个目标并不能达成. 默认值为 200 毫秒.
    13. -XX:InitiatingHeapOccupancyPercent=45 - 启动并发GC时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (表示堆使用了45%).

    总之:
    G1是HotSpot中最先进的准产品级(production-ready)垃圾收集器.
    重要的是, HotSpot 工程师的主要精力都放在不断改进G1上面, 在新的java版本中,都用到了G1收集器.在将来会带来新的功能.

  • 相关阅读:
    [量化投资-学习笔记007]Python+TDengine从零开始搭建量化分析平台-布林带
    [论文阅读] Generative Adversarial Networks for Video-to-Video Domain Adaptation
    【无标题】
    源代码层面分析Appium-inspector工作原理
    基本算法:二分
    system函数实践1:system函数进程的爸爸是谁?
    [学习笔记] VFX Silhouette
    手把手教你如何自制目标检测框架(从理论到实现)
    【C语言】文件输入输出操作
    在线会议中人脸面部轮廓图像提取(三)——Dlib库人脸面部轮廓图像特征提取
  • 原文地址:https://blog.csdn.net/u012932876/article/details/127498269