jvm学习

251 阅读35分钟

类加载子系统

image.png

内存结构概述

1627372781(1).png

类加载器和及类加载过程

类加载器

类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定文件标识

类加载器只负责class文件加载,是否可以运行由execution engine决定

加载的类信息一般存放于方法区,方法区还会存放运行时常量池信息,包括字符串,数字常量。

1627373731.png

类加载过程

1627374032(1).png

1627374156(1).png

1627374305(1).png

1627374380(1).png

类加载器的分类

1627375059(1).png

1627375149(1).png

1627375238(1).png

1627375297(1).png

1627375459(1).png

双亲委派

1627376128(1).png

优势:避免类的重复加载,保护程序安全,防止核心API被随意篡改

运行时数据区

image.png

红色部分一个进程一份,右边部分一个线程一份。

程序计数器(pc寄存器)

概述

对物理pc寄存器的抽象模拟,存储下一条指令的地址,由执行引擎读取下一条指令。 内存空间很小,运行速度快,每个线程有私有的程序计数器,生命周期和线程生命周期一致。

调用本地native方法指向null,因为c语言不会被编译成字节码文件,所以没有指向。

问题

使用pc寄存器存储字节码指令地址有什么用?

为什么使用pc寄存器记录当前线程的执行地址?

这两个问题是一个问题,因此cpu需要不停切换各个线程,切换后需要知道从哪个位置继续执行

为什么pc寄存器被设定为线程私有?

因为cpu会不停切换线程,导致不停中断或恢复,为了能准确记录各个线程正在执行的当前字节码指令地址。

虚拟机栈

概述

由于跨平台特性,java指令是根据栈来设计的,而不是基于寄存器,可以应对各个cpu

优点是跨平台,指令集小,容易编译,缺点是性能下降,同样功能需要更多指令

栈和堆比较 : 栈是运行时单位,堆时存储时单位。栈是解决程序的运行问题,即程序如何执行,如何处理数据,堆解决的是数据存储问题,即数据怎么放,放在哪儿。

作用:每个线程创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着java方法调用。生命周期和线程一致,管java的运行,它保存方法的局部变量(八种数据类型+对象引用地址),部分结果,参与方法的调用和返回。

1627441438(1).png

栈的特点: 快速有效分配存储,访问速度仅次于pc寄存器。jvm直接对栈的操作只有两个,每个方法执行,伴随着进栈(入栈,压栈),执行结束后的出栈工作。对于栈来说不存在垃圾回收问题。

开发中遇见的栈的异常?

java虚拟机规范允许java栈的大小是动态或者固定不变的。

如果固定不变,每个线程的栈容量在线程创建时独立选定,如果线程请求分配的栈容量超栈的最大容量,java虚拟机将抛出StackOverflowError异常

如果是动态的,并且在尝试扩展无法申请到足够内存,或者创建新线程时没有足够内存创建对应的栈,抛出oom异常。通过-Xss可以设置栈内存大小

栈的存储单位

  • 每个线程都有自己的栈,数据以栈帧格式存在
  • 这个线程上执行的每个方法都对应一个栈帧
  • 栈帧时一个内存区块,时一个数据集,维系着方法执行过程中的各种数据信息
  • 一条活动线程中,一个时间点上,只会有一个活动的栈帧,称为当前栈帧,对应的方法是当前方法,对应的类是当前类
  • 当前方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈顶称为新的当前栈帧
  • 不同线程中包含的栈帧不能互相引用,不同线程有不同的栈,只能共享堆和方法区。
  • 正常return返回和抛出异常返回都会使当前栈帧弹出。

栈帧内部结构

局部变量表操作数栈、动态链接、方法返回地址、一些附加信息。

1627454279(1).png

局部变量表

  • 也称为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。数据类型包括八大基本数据类型,和对象引用(地址),以及returnAddress类型
  • 建立在线程上,私有数据,不存在数据安全问题
  • 局部变量表所需要的容量大小是在编译期确定下来的,保存在方法的code属性的maximum local variables数据项中,在运行期间不会改变。

关于slot的理解

slot是局部变量表最基本的存储单元,32位以内的占用一个slot,64位的占用两个slot(long, double八个字节 8 * 8位),byte,short,char都会被转为int。 jvm为每一个slot分配一个访问索引,通过索引访问局部变量值。占用两个slot的用起始索引。

如果当前方法时由构造方法或者实例方法创建(非static方法),那么该对象引用this将会存放在index为0的slot处。

如下为test2方法的局部变量表: image.png

slot是可以重复利用的,如果一个局部变量过了其作用域,后面申请的新的变量可以继续用其slot。

image.png

成员变量与局部变量的对比

成员变量分为类变量(static修饰)和实例变量。类变量在linking的prepare阶段给类变量默认赋值initialization阶段显示赋值。实例变量随着对象的创建,在堆空间分配实例变量空间,并进行默认赋值。

而局部变量需进行显示赋值,否则编译不通过,因为局部变量表没存其值,没初始化。

class Person{
    String name;//成员变量,实例变量。随着对象的创建而存在于堆内存中
    static String country = "CN";//静态的成员变量,类变量。随着类的加载而存在于方法区中
    public static void show()
    {
        System.out.println("::::");
        //this.haha();//静态方法中不能出现this关键字,类没被实例化,static会提前初始化。
    }
    public void haha()
    {
        System.out.println("hahaha...");
    }
}
class  StaticDemo {
    public static void main(String[] args) {
        Person p = new Person();
        p.haha(); //对象调用成员方法
        Person.show();//类名调用静态方法,也可对象调用静态方法(不推荐)
    }
}

补充说明

在栈帧中,与性能调优最为密切的就是局部变量表。局部变量表中值不存在了,指针指向堆中的对象可以被回收了。并且在栈帧中,局部变量表越大,栈帧越大,可嵌套方法就会变少。

局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接、间接引用的对象都不会被回收。

操作数栈

数组形式存在,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据(入栈/出栈)

image.png

0,3都是存放在操作数栈中,2和5才是将其存放在局部变量表中。6和7是将操作数从局部变量表取出压入操作数栈中,最后iadd运算结果也存放在操作数栈中,9将add结果从操作数栈保存到局部变量表中。

  • 操作数栈在编译以后就确定一个栈的深度,保存在方法的Code属性中,为max_stack。
  • 32位占用一个栈深度,64位占用两个栈深度。如果被调用方法有返回值,返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令。

栈顶缓存技术

java是零地址指令(栈式架构),按照顺序入栈出栈,不用记录指令地址,但是入栈出栈需要更多地指令分派以及内存读写次数。

这样会降低执行速度,因此开发者提出将所有栈顶元素缓存到物理cpu寄存器中,降低对内存的读写次数,提高执行速度。

动态链接

概述

保存常量池的指针,方便程序访问常量池的信息。 即指向运行时常量池的方法引用。 所有变量和方法会用会作为符号引用保存在class文件的常量池中。

下图中#2即为动态链接 image.png

常量池中#2即为调用的方法,图中仍为动态链接,继续向下寻找。直到找到直接引用的方法。 image.png

image.png

方法返回地址,动态链接,一些附加信息也称作帧数据区。

方法的调用

  • 静态链接:如果调用的目标方法在编译期就可知,且运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接
  • 动态链接:如果调用的目标方法在编译期不可知,运行期间才能确定,这种情况下将调用方法的符号引用转换为直接引用的过程称为动态链接

对应方法的绑定机制--早期绑定,晚期绑定。绑定是一个字段、方法或者类在符号引用被替换直接引用的过程,仅发生一次

  • 早期绑定:目标方法在编译期可知,运行期间不变(invokespecial),标记为final肯定是早期绑定。
  • 晚期绑定:目标方法在编译期不可知,只能在运行期根据实际类型绑定相关的方法。(字节码文件为invokevirtual),调用虚方法。通常传参为接口,父类的都是晚期绑定。
  • 非虚方法:编译期就确定具体调用版本,运行期间不变。--静态方法、私有方法、final方法、实例构造器、父类方法,其他方法都是虚方法。

静态链接--早期绑定--非虚方法 动态链接--晚期绑定--虚方法

image.png

动态语言和静态语言

二者区别是对于类型的检查是编译期间还是运行期间,编译期间是静态语言,运行期间是动态语言。静态语言是判断变量自身(String)的类型信息,动态语言是判断变量值("test",10)的类型信息,变量没有类型信息,变量值才有类型信息。

java:String info = "test" js:var name = "test";var age = 10 python: info = 1.1

java在编译时就知道类型,为静态类型语言,而js,python只有在运行期间才能知道类型,为动态类型语言。

java中lambda表达式为动态类型(invokedynamic),因此java具备动态语言的特点。

image.png 说明所有父类没有本方法,此时没有重写过方法,也没有实现这个方法,调用的是接口,而接口没被重写。但是如果每次都一直向上找父类,会降低性能,因此

image.png

image.png

image.png

图中son继承father继承object,如果son调用蓝色方法(没重写过的方法),不用去father中找,因为有虚方法表,直接指向object中的方法。而hardchoice object没有,直接指向father,而son中重写过这两个方法,也直接指向son自己。

方法返回地址

概述

  • 存放调用该方法pc寄存器的值,a方法内调用b方法,执行完b方法需要回到a方法正确位置,这个位置就是pc寄存器的值。
  • 无论是正常执行结束,或者非正常退出,在方法退出后都会返回到该方法被调用的位置,方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令地址,而通过异常退出的,返回地址要通过异常表确定,栈帧中一般不会保存这部分信息。
  • 通过异常退出不会给上层调用者产生任何返回值

image.png

image.png

一些附加信息

java虚拟机实现相关的附加信息,例如对程序调试提供支持的信息。

虚拟机栈的5个面试题

举例栈溢出的情况(StackOverFlowError)

通过-Xss设置栈的大小;如果都满了会出现oom异常

调整栈的大小能保证不出现溢出吗?

不能保证,还是有可能StackOverFlowError

分配栈内存越大越好吗?

不是,合理比较好,不然会占用过量内存,因为整体内存一定,其他空间会变小

垃圾回收是否会涉及到虚拟机栈

不会的。只有方法区和堆空间。局部变量表中值不存在了,指针指向堆中的对象可以被回收了,回收的还是堆空间的内容。

方法中定义的局部变量是否线程安全?

具体问题具体分析。如果方法传参形如StringBuilder线程不安全的对象,并且多线程操作,此时线程不安全。

或者: image.png 此时方法执行完毕,可能会被多个线程争抢到,因此线程不安全。

但是此时线程安全,因为s1没了,返回string,String可能会被多个线程调用不安全,但是StringBuilder安全。 image.png

总结就是内部产生,内部消亡就是安全的,不是内部产生的或者内部产生的返回给外面了,就是线程不安全的。

本地方法栈

管理本地方法的调用。线程私有,内存和java虚拟机栈相同,可以设置,也会内存溢出。需要在本地方法栈中登记native方法,在execution engine执行时加载本地方法库

堆空间

堆的核心概述

  • 一个jvm实例只存在一个堆内存,是java内存管理的核心区域,在jvm启动时就被创建了,空间大小确定,是jvm最大的内存空间,内存大小可以调节。
  • java堆处于物理上不连续,逻辑上视为连续的。
  • 所有线程共享java堆,在这里还可以划分线程私有的缓存区(TLAB),提高并发性。
  • 所有对象实例以及数组都应该在运行时分配到堆上,但是实际上是几乎所有,也有可能在栈上分配。
  • 方法结束以后,堆中的对象不会马上被移除,在垃圾收集的时候才会被移除。
  • 堆是gc执行垃圾回收的重点区域

堆的内存分配

java7及其以前逻辑上分为:新生区+养老区+永久区

java8及其以后逻辑上分为:新生区+养老区+元空间

约定:新生区=新生代=年轻代 养老区=老年区=老年代 永久区=永久代

设置堆内存大小与OOM

1627628119(1).png

  • -Xms = 新生代 + 老年代大小
  • -X jvm 运行参数
  • ms memory start 起始内存
  • -Xmx 设置最大内存大小

通过代码可以获取java RunTime信息 image.png

通常情况下Xms和Xmx设置相同,避免经常改变大小,造成系统额外的压力。

-XX:+PrintGCDetails可以打印细节

年轻代和老年代

java对象可以分为两类,一类是生命周期较短的瞬时对象,一类是生命周期长的。 因此java分为年轻代和老年代,年轻代又分为Eden空间、Survivor0和Survivor1空间,也叫from区,to区。

image.png

  • -XX:NewRatio=2,系统默认2,新生代1,老年代2,新生代占总堆的1/3,命令行查看比例:jinfo -flag newRatio <pidNum>
  • Eden和两个Suvivor空间缺省所占比例是8:1:1,通过-XX:SuvivorRatio调整查看这个比例。命令行查看:jinfo -flag SuvivorRatio <pidNum>
  • 注意:在查询时,实际比例和设置比例不相同,这是由于打开了自适应内存分配策略,参数调整为-XX:-UseAdaptiveSizePolicy (+使用,-不用)
  • 几乎所有对象是Eden被new出来的,绝大多数对象在新生代就被销毁了。
  • -Xmn设置新生代最大内存大小,通常默认值即可
  • -XX:SurvivorRatio设置新生代中Eden和Survivor区比例

对象分配过程

在Eden创建对象,Eden满后,YGC对Eden进行垃圾回收,将不被引用的对象销毁,新对象加载到Eden,本来在Eden中的对象移动到Survivor0区,同时年龄设置为1。 往后Eden再满,触发垃圾回收,将不被引用的对象销毁,新对象加载到Eden,本来在Eden中的对象移动到Survivor1区,年龄设置1,同时将Survivor0区中不被引用的销毁,仍在引用的移入Suvivor1区,年龄+1。 后续操作相同,因此Suvivor仅有一个区有对象。当年龄增长到15(默认值),再次GC触发晋升,存储到老年代中。通过 -XX:MaxTenuringThreshold=<N>设置晋升到老年代的阈值。

注意:

  • 仅仅在Eden满了的时候会触发YGC
  • 针对幸存者s0,s1区,复制之后有交换,谁空谁是to
  • 垃圾回收频繁在新生区收集,很少在老年代收集,几乎不在永久区/元空间收集
  • YGC一定会将Eden清理干净,是垃圾清理,不是垃圾放入s区
  • 动态年龄判断:如果s区同龄的对象总和大于s区空间一半,则年龄大于等于该年龄的对象可以直接进入老年代。

image.png

MinorGC、MajorGC和FullGC

1、部分收集

  • 新生代收集(MinorGC/YoungGC):只是新生代(Eden、s0、s1)的垃圾收集
  • 老年代收集(MajorGC/OldGC):只是老年代的垃圾收集
    • 目前,只有CMS GC会有单独收集老年代的行为
    • 注意,很多时候MajorGC会和FullGC混淆使用,需要具体分辨是老年代回收还是整堆回收
  • 混合回收(Mixed GC):收集整个新生代以及部分老年代的垃圾收集 2、整堆收集
  • 收集整个java堆和方法区的垃圾收集

年轻代GC(MinorGC)触发机制:

  • 年轻代空间不足就会触发(指的是Eden满触发,而Survivor满不会触发)
  • java大多对象朝生夕灭,因此MinorGC非常频繁,回收速度比较快
  • MinorGC会引发STW,暂停其他用户的线程,等待垃圾回收的结束,用户线程才恢复运行。

老年代GC(MajorGC/FullGC)触发机制:

  • 指发生在老年代的GC,对象从老年代消失时,就可以说MajorGC/FullGC发生了
  • 出现了MajorGC,经常会伴随至少一次MinorGC,但非绝对,在parallelScavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程
    • 也就是在老年代空间不足时,会先尝试触发MinorGC,如果空间还是不足,则触发MajorGC
  • MajorGC速度比MinorGC慢十倍以上,STW时间更长。
  • 如果MajorGC后内存好还不足,oom

FullGC触发机制:

  • 调用System.gc(),系统建议执行FullGC,但是不是一定执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过MinorGC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden和from 向to复制时,对象大小大于to可用区域,则把对象转存到老年代,且老年代的可用内存小于该对象大小
  • FullGC在开发或调优尽量避免。涉及STW

堆分代主要是为了优化GC性能,许多对象寿命短,放在特定地方,每次优先堆这种地方回收。

TLAB(Thread Local Allocation Buffer)

由于堆是共享区域,如果多个线程同时在jvm创建对象会造成线程不安全。为避免多个线程操作同一个地址,需要加锁等机制,进而影响分配速度

什么是TLAB?

  • 从内存模型而非垃圾收集的角度,对Eden继续进行划分,JVM为每个线程分配了一个私有缓存区域。
  • 多线程同时分配内存时使用TLAB可以避免一系列非线程安全问题,同时还能提升内存分配的吞吐量,称之为快速分配策略。
  • 不是所有对象实例都能在TLAB中成功分配内存,但是是jvm首选
  • -XX:UseTLAB设置是否开启TLAB空间,默认开启
  • 默认情况下TLAB空间小,为Eden的1%,通过-XX:TLABWasteTargetPercent设置TLAB占用Eden的百分比大小。
  • 对象在TLAB分配内存失败,就会通过加锁机制确保原子性,在Eden直接分配内存。

1627959588(1).png

更改堆设置指令

1627960375(1).png

在发生MinorGC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象的总空间。

  • 大于:MinorGC安全
  • 小于: 虚拟机查看-XX:HandlePromotionFailure设置值是否允许担保失败
    • 如果为true,会继续检查老年代最大可用连续空间是否大于历次紧急老年代的对象的平均大小
      • 大于:尝试进行MinorGC,但这次MinorGC仍然有风险
      • 小于:改为进行一个FullGC
    • 如果为false,进行一次FullGC

注意:jdk7后HandlePromotionFailure失效,只要老年代的连续空间大于新生代总大小或者历次晋升空间的平均大小就进行MinorGC,否则FullGC

堆是分配对象存储的唯一选择吗?

随着jit编译期的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换技术会导致一些微妙的变化,所有对象分配到堆上也渐渐变得不那么绝对了。

j如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。

TaoBaoVM,创新GCIH(GC invisible heap)实现off-heap,生命周期较长的对象从heap中移到heap外,并且GC不能管理GCIH内部的java对象,降低GC回收频率提升GC效率

逃逸分析

  • 如何将堆上的对象分配到栈,需要使用逃逸分析手段
  • 有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
  • java编译器能分析出一个新的对象的引用的使用范围从而绝对是否要将这个对象分配到堆上
  • 一个对象在方法中定义以后,对象只在方法内使用,则认为没有发生逃逸
  • 反之,如果被外部方法引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

1627975341(1).png

jdk6u23后默认开始逃逸分析了,能使用局部变量,就不要再方法外定义。

-XX:+DoEscapeAnalysis 开启逃逸分析

image.png

标量替换 image.png

方法区

栈堆方法区交互关系

image.png

方法区的理解

尽管所有方法区在逻辑上属于堆得一部分,但是一些简单的实现可能不会选择区进行垃圾收集或者压缩。但是对于HotSpotJVM而言,方法区有一个别名叫Non-Heap非堆,目的就是要和堆分开,因此方法区看做是一块独立于堆得内存空间。

  • 方法区也是线程共享内存区域
  • 方法区在jvm启动时创建,实际物理内存和堆一样可以是不连续的
  • 大小可以选择固定或可扩展,和堆一样
  • 大小决定了系统可以保存多少个类,如果太多会溢出oom:metasapce
    • 加载大量的第三方包
    • tomcat部署工程过多
    • 大量动态反射类
  • 关闭jvm会释放内存
  • 方法区和元空间并不等价,元空间是方法区的一种实现
  • 元空间不在虚拟机设置的的内存中,而是使用本地内存,如果方法区无法满足新的内存分配需求也会oom。

设置的方法区大小与oom

jdk8以后

  • 初始值:-XX:MetaspaceSize=size
  • 最大值:-XX:MaxMetaspaceSize=size

以前:

  • 初始值:-XX:PermSize=size

  • 最大值:-XX:MaxPermSize=size

  • 默认metaSpace21M,最大值默认是-1,即没有限制。一旦比21M高,会触发FullGC,卸载没用的类,即这些类对应的类加载器不在存活,然后这个高水位线重置,新的高水位线取决于GC后释放了多少元空间,如果释放空间不足,那么在不超过最大值时,适当提高该值,反之则降低该值。

  • 如果不指定大小,默认情况下虚拟机会好近所有系统内存。元数据发生溢出则会抛出oom:Metaspace

  • 如果初始化高水位线过低,则水位线调整会发生多次,FullGC会多次调用,避免频繁GC,建议将MetaspaceSize设置一个较高的值

如何解决oom

1628066099(1).png

方法区的内部结构

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

  • 类型信息:对每个加载的类型(类class、接口interface、枚举enum、注解annotation)jvm必须在方法区存储以下信息
    • 这个类型完整的有效名称(包名+类名)
    • 这个类型的直接父类的有效名称(interface和object没有父类)
    • 这个类型的修饰符(public,abstract,final的某个子集)
    • 这个类型直接接口的一个有序列表
  • 域信息(成员变量,属性)
    • jvm必须在方法区中保存类型所有域的相关信息,以及域的生命瞬息
    • 域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
  • 方法信息
    • 方法名称、返回类型,参数数量类型(按顺序)
    • 修饰符(public,private,protected,static,final,synchronized,native,abstract)
    • 异常表(除abstract和native)
      • 每个异常处理的开始位置,结束位置,代码处理在程序计数器的偏移地址,被捕获的异常类的常量池索引。

运行时常量池

常量池:

  • 常量池包括各种字面量,对类型、域和方法的引用。
  • java字节码文件需要数据支持,但是很大,因此字节码指向常量池的引用,在动态链接的时候用到运行时常量池。
  • 常量池存储的数据类型包括:数量值,字符串值,类引用,方法引用,字段引用。
  • 常量池可以看成一张表,虚拟机根据这张表找到要执行的类名、方法名、参数类型、字面量等类型。

运行时常量池:

  • 字节码文件中的常量池加载到方法区中的运行时常量池。
  • 此时不是常量池的符号地址了,而是真实地址
  • 具备动态性,可能和常量池内容有出入。
  • 超过常量池最大值会oom
  • 虚拟机栈中动态链接链接到常量池中

永久代什么被元空间替代

元空间不用额外设置内存,永久代需要自己划分内存,容易oom,分配大了容易内存浪费。元空间使用本地内存,默认情况下仅受本地内存限制。对永久代调优困难

静态对象存放

  • 静态引用对应的对象在jdk678中都是存放在堆空间中。
  • 引用名称6中在永久代,7中移到了堆中。
  • 只要是new的对象都在堆空间中,除非没有发生逃逸。

方法区的垃圾回收

这个区域回收效果不好,尤其是类型的卸载,条件苛刻。但是回收有必要。主要回收两部分内容:常量池中废弃的常量和不再使用的类型。常量池中的不用,没地方引用就回收了

类的回收:同时满足如下三个条件

  • 该类所有实例已经被回收,堆中不存在该类及其任何派生子类的实例
  • 加载该类的加载器已经被回收,这个条件除非是经过精心设计的可替换类的加载器的常见,如OSGI、JSP的冲加载等,否则很难达成。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

对象创建

对象实例化

  • 判断对象对应的类是否加载、链接、初始化
    • 遇到new指令时,先检查这个指令的参数能否在元空间常量池中定位到一个类的符号引用,并检查这个类是否已经被加载解析初始化(判断这个类元信息是否存在),如果没,双亲委派,使用当前类加载器以ClassLoader+包名+类名为key进行查找对应的.class文件,如果没有,抛出classNotFoundException的异常,找到进行加载,并且声称对应的Class类对象
  • 为对象分配内存(计算对象占用空间大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间空间即可,即4字节,32bit)
    • 内存规整--指针碰撞(整齐的往栈中放东西,放完了移动指针,此时GC使用Serial,parNew这种基于压缩算法内存规整,使用指针碰撞)
    • 不规整 虚拟机需要维护一个列表或空闲列表分配(已使用和未使用内存交错,空闲列表法,列表记录哪些内存块可用,再分配的时候找到一块足够打的空间划给对象实例,更新表上内容)
  • 处理并发安全问题
    • 采用cas配上失败重试保证更新原子性
    • 每个线程预先分配一块TLAB(Eden区) -XX:+/-UseTLAB设定 默认使用
  • 初始化分配到的空间,所有属性值设置默认,保证对象实例字段在不赋值阶段可以直接使用
  • 设置对象的对象头(将对象所属的类,对象的hashcode和对象的GC信息,锁信息等数据存储在对象的对象头中,这个过程具体设置方式取决于jvm的实现)
  • 执行init方法进行初始化
    • java程序视角,这个阶段初始化才开始,初始化成员变量,执行实例化代码块,调用类的构造方法,把堆中的对象的首地址值赋值给引用变量。

对象内存布局

jvm是如何通过栈帧中的对象引用访问到内部的对象实例

image.png

image.png

执行引擎

概述

执行引擎是将字节码指令解释/编译(后端编译,非运行时的前端编译)称为对应平台上本地机器指令。高级语言翻译为机器语言 image.png

image.png

执行引擎就是执行一行一行的字节码指令,从程序计数器代表的地址取指令执行,主要就对操作数栈操作,不停入栈出栈。

  • 执行引擎执行什么字节码指令完全依赖pc寄存器
  • 每当执行完一条指令,pc寄存器会更新下一条指令地址
  • 执行引擎可能通过存储在局部变量表中的对象引用准确定位到堆中的对象实例信息,通过对象头的元数据指针定位到目标对象的类型信息。

执行过程

java是半编译半解释型语言,因此有两条路走。 image.png

  • 解释器是java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方法执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行(边翻译边执行)
  • jit编译器是虚拟机将源代码直接编译和本地机器平台相关的机器语言(一起编译好再执行)

String

简介

String底层1.8是char数组,1.9为byte数组,因为char是2byte,但是存储的大部分都是拉丁文,1byte就能实现,因此换为byte数组,但是如果是汉字又没办法表示了,因此补上一个字符编码集的标识。因此StringBuffer、StringBuilder和固有的一些字符也做了相应的修改。

  • String具有不可变性,只要修改,就会造新的String字符串。
  • String常量池不会存储相同的字符串的,底层是一个固定大小的Hashtable,默认长度1009(jdk6为1009,7中60013,8中60013,1009为可设置的最小值),如果放进的String Pool的String非常多,就会造成Hash冲突严重,导致链表很长,造成调用String.intern时性能大幅下降。-XX:StringTableSize=size

内存分配

  • jdk6及其以前字符串常量池放在永久代

  • jdk7调整到了堆中

    • 所有字符串都在堆中,在调优的时候仅需调整堆得大小即可
    • 字符串常量池概念远比使用的比较多,但是这个改动使我们重新考虑在7中使用intern
  • jdk6(静态变量,StringTable) image.png

  • jdk7 image.png

  • 调整到堆中一个是因为permSize空间较小,一个是因为垃圾回收频率较低

String基本操作

class Memory {
    public static void main(String[] args) {
        int i = 1;
        Object obj = new Object();
        Memory mem = new Memory();
        mem.foo(obj);
    }
    private void foo(Object param) {
        String str = param.toString();
        System.out.println(str);
    }
}

image.png

字符串拼接操作

  • 常量和常量拼接结果在常量池,原理是编译期优化
String s1 = "a" + "b" + "c";
String s2 = "abc";
s1 == s2 //true 在编译生成的class文件中s1已经是abc了
  • 常量池中不会存在相同的内容
  • 只要其中有一个是变量,结果就在堆(非常量池部分的堆中,但是常量池也在堆中)中,变量拼接的原理是StringBuilder
String s1 = "javaEE";
String s2 = "Hadoop";
String s3 = "javaEEHadoop";
String s4 = "javaEE" + "Hadoop";
String s5 = s1 + "Hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
String s8 = s6.intern();
s3 == s4 // true
s3 == s5 // false
s3 == s6 // false
s3 == s7 // false
s5 == s6 // false
s5 == s7 // false
s6 == s7 // false
s3 == s8 // true
  • 如果拼接的结果调用intern方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

字符串拼接底层

形如String s = s1 + s2这种的(jdk5之后)

  1. StringBuilder s = new StringBuilder();
  2. s.append("javaEE");
  3. s.append("Hadoop");
  4. s.toString(); //类似于new String("javaEEHadoop");
 0 ldc #2 <javaEE>
 2 astore_1
 3 ldc #3 <Hadoop>
 5 astore_2
 6 new #4 <java/lang/StringBuilder>
 9 dup
10 invokespecial #5 <java/lang/StringBuilder.<init> : ()V>
13 aload_1
14 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
17 aload_2
18 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
21 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
24 astore_3
25 getstatic #8 <java/lang/System.out : Ljava/io/PrintStream;>
28 aload_3

注意:加上final相当于不再是变量,都是常量引用或者字符串常量,因此相当于都是直接从常量池中取,编译期优化了已经, 而不用StringBuilder

final String s1 = "javaEE";
final String s2 = "Hadoop";
String s3 = s1 + s2;
String s4 = "javaEE" + "Hadoop";
System.out.println(s3 == s4); //true

因此,对于基本数据类型和String能用final就用,编译期间已经被确定值。而类加载classloader的linking的prepare期间类变量(static修饰)才赋默认值,此时对于static final已经被初始化值(在常量池中)。final修饰new的对象依然等到初始化后才能赋值。

class Solution {
    public static void main(String[] args)  {
        Solution solution = new Solution();
        int num = 100000;
        long start = System.currentTimeMillis();
        solution.method1(num);
        long end = System.currentTimeMillis();
        solution.method2(num);
        long end2 = System.currentTimeMillis();
        System.out.println(end - start); //5755
        System.out.println(end2 - end);  //4
    }
    void method1(int num) {
        String src = "";
        for (int i = 0; i < num; i++) {
            src += "a";
        }
    }
    void method2(int num) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < num; i++) {
            sb.append("a");
        }
    }
}

如果频繁添拼接字符串,一定使用StringBuilder,自始至终只创建了一个对象,而对于String字符串拼接,创建了多个StringBuilder对象,还会产生GC

还可以继续优化,StringBuilder有默认长度,超过长度会进行扩容,将原有的拷贝到新的中,因此可以初始化默认大小,跑上述代码结果为3.

String.intern()

如果字符串常量池中没有对应的字符串,则在常量池中生成,如过有,直接指向。

  • new String("1")创建了几个对象
    • 如果常量池中没有1,则两个分别是String和1
    • 如果常量池中有1,则不用在常量池中在造一个1,就创建了一个String对象
  • new String("a") + new String("b")创建了几个对象
    • StringBuilder,new String(),常量池中的a,new String(),常量池中的b,StringBuilder.toString()中的new String();
    • 深入剖析StringBuilder的toString()方法
    • new String(char value[],int offset,int count)
    • 在字符串常量池中没有生成ab,因为是char数组生成的(jdk1.9以前是char,后是byte)

intern面试题

public static void main(String[] args)  {
    // 常量池中存在"1",s指向new String(),而不是常量池
    String s = new String("1");
    // 没啥用,已经存在了
    s.intern();
    // s2指向常量池中的"11"
    String s2 = "1";
    System.out.println(s == s2); //false

    //s3记录的是堆中new String的地址 并且由于StringBuilder特性,常量池中不存在"11","11"直接存在String对象中
    String s3 = new String("1") + new String("1");
    //intern是在常量池中生成"11"
    //jdk6中,常量池中创建了新的"11"对象
    //jdk7中,常量池没创建"11"对象,而是在常量池中生成了指向堆中new String("11")的指针
    s3.intern();
    // 因此s4的"11"实际上是常量池中指向s3对象的指针
    String s4 = "11";
    System.out.println(s3 == s4); //jdk6:false jdk7+:true
}

image.png

image.png

总结:

  • jdk1.6中,将字符串尝试放入常量池
    • 如果常量池有,不会放入,返回常量池对象地址
    • 如果没有,把对象复制一份,放入常量池,返回常量池地址
  • jdk1.7起,将字符串尝试放入常量池
    • 如果常量池有,同上
    • 如果没有,则会把对象引用地址复制一份,放入常量池,返回常量池引用的对象地址

StringTable垃圾回收

-XX:+PrintStringTableStatistics 显示StringTable垃圾回收行为

垃圾回收

概述

  • 什么是垃圾:运行程序中没有任何指针指向的对象(只设计引用类型),否则内存消耗完了无法运行oom。
  • pc寄存器不存在GC与内存溢出
  • 本地方法栈与虚拟机栈没有GC但是有stackOverFlow
  • 方法区于堆有GC和OOM
  • 频繁收集新生代,较少收集养老带,基本不动永久代/元空间,因此主要针对堆

标记阶段:引用计数算法

  • 标记阶段:对象存活判断,哪些死了哪些活着。
  • 每个对象保存一个整形的引用计数属性,记录对象被引用的情况
  • 对于A对象,只要有一个对象引用了A,A的引用计数器+1,失效-1,如果为0,则可以被回收
  • 优点:实现简单,垃圾对象便于辨识,效率高,延迟低
  • 缺点:
    • 需要单独的引用计数器属性,增加存储空间开销
    • 每次复制需要更新计数器,伴随加减法,增加时间开销
    • 严重问题:无法处理循环引用,因此java没用这个。P指向A指向B指向C指向A,此时P用完了不指向A了,但是ABC谁也无法被回收。这就是内存泄漏,但是java不会出现这种内存泄漏。
    • python使用,解决方案:1、手动解除,合适时机手动解除引用关系 2、使用弱引用Weakref,是python标准库,解决循环引用。(只要GC就回收)

标记阶段:可达性分析算法

  • GCRoots是一组必须活跃的引用。通过一系列名为GCRoots的对象作为起始点,从起始点向下搜索。如果一个对象到GCRoots没有任何引用链相连时,此对象不可用。

以下四种可以作为GCRoots对象

  • 虚拟机栈(栈帧中局部变量区)中引用的对象
  • 方法区中类静态属性的引用对象
  • 方法区中常量引用的对象(如字符串常量池中的引用)
  • 本地方法栈中JNI引用的对象
  • 以上四条比较重要
  • 同步锁Synchronized持有的对象
  • 虚拟机内部的引用:基本数据类型对应的Class对象,一些常驻的对象如NullPointerException、OunOfMemoryError,系统类加载器
  • 反应java虚拟机内部情况的JMXBean,JVmTI中注册的回调,本地代码缓存等。

小技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,他保存了堆内存里的对象、但是自己又不存放在堆内存里,他就是一个root。(除了堆以外的周边的结构内的引用基本都可以看做是GCRoots)

除了固定的GCRoots集合外,根据用户选用的垃圾回收器以及当前回收的内存区域不通,还有其他对象可以临时性加入,共同构成GCRoots集合。如分代收集和局部回收

  • 如果只针对堆中的某块区域进行GC(典型就是针对新生代),必须考虑内存整体性,此时诸如老年代就要被加入GCRoots范畴,保证准确定。

  • 可达性分析算法判断内存是否可以被回收,判断工作需在一个一致性的快照中进行(不能改变值),否则准确性无法保证,因此GC时候需要STW(Stop The World),即使号称不会发生停顿的CMS收集器,在枚举根节点也需要停顿。

对象的finalization机制

  • 允许开发人员提供对象被销毁之前自定义处理逻辑。即当GC在回收没有被引用的对象前,会调用这个对象的finalize方法。
  • 这个方法允许被子类重写,用于在对象被回收时对资源的释放,如关闭文件,套接字,和数据库连接等。
  • 不要主动调用对象的本方法
    • 可能会导致对象复活
    • 执行时间没保障,完全由GC线程决定,极端情况下,不发生GC则不会调用此方法
    • 一个糟糕的此方法严重影响GC性能
  • 由于本方法存在,对象一般存在三种状态(当所有根节点都无法访问到A对象,说明A不在使用,一般来说会被回收,但是不是必须回收,而是处于缓刑,可能被复活)
    • 可触及的:从根节点开始,可以达到这个对象
    • 可复活的:所有引用都释放,但是可能在本方法中复活
    • 不可触及的:本方法调用后,对象并没有复活,进入不可触及状态,并且本方法只可能被调用一次。只有这种状态的对象才会被回收。

具体流程

当对象A到GCRoots没有引用链:

  • 进行筛选,判断对象是否有必要执行本方法
    • 如果A没有重写本方法(Obj对象中本方法只定义了,方法体没东西),或者本方法已经被调用过,则被认为没有必要执行,A被判定不可及
    • 如果重写了本方法,且没有被执行,A会被插入到F-Queue队列中,由一个JVM自动创建的,低优先级Finalizer线程触发其finalize()方法执行
    • 本方法是对象最后一个免死的机会,GC会对队列中的对象进行二次标记,如果A在本方法中与引用链上的GCRoots建立了联系,二次标记时,A会被溢出即将回收集合,之后A再次出现没有引用存在的状态,本方法不会被调用,直接变为不可及状态。

MAT与Jprofiler的GC Roots溯源

使用MAT,Jprofiler分析GCRoots

P144-146

清除阶段:标记-清除算法(Mark-Sweep算法)

  • 清除阶段:当成功区分区对象的存活对象和死亡对象以后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。
  • 执行过程:堆内有效空间耗尽的时候,停止整个程序(STW),开始2个工作-标记和清除
    • 标记:Collector从引用根节点开始遍历,参考深入理解java虚拟机第三版本3.3.2 P77右下角。标记所有需要被回收的对象,在标记完成后统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象,标记属于垃圾判定的过程。官网写的是标记可达对象。
    • 清除:Collector对堆内从从头到尾进行线性遍历(所有对象),如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

image.png 优点:

  • 易于理解。。 缺点:
  • 效率不高
  • 进行GC的时候,需要STW,用户体验差
  • 清理出来的空间内存不连续,产生碎片,需要维护一个空闲列表 注意:
  • 这里所谓的清除不是置空,而是把需要清除的对象地址保存在空闲的地址列表中,下次有新对象要加载时,判断垃圾的位置空间是否够,如果够就存放。

清除阶段:复制算法

  • 解决标记-清除算法在垃圾收集效率方面的缺陷。
  • 核心思想:将活着的内存空间分为两块,每次只使用其中的一块,在垃圾回收时将正在使用的内存中存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存角色,完成GC。新生代的s0和s1区用的就是这个算法。

优点:

  • 没有标记清除过程(直接通过GCRoots遍历每个对象复制过去即可),实现简单运行高效(最显著特征)
  • 复制后保证空间的连续性,不会出现碎片问题。 缺点:
  • 需要两倍内存空间
  • 对于G1这种拆分成大量region的GC(分区),复制而不是移动,意味着GC需要维护region之间对象的引用关系,不管是内存占用还是时间开销也不小(即下图中的reference需要改变,也需要进行维护) 1628780628(1).png

特别的:

  • 如果系统中GCRoots可达对象很多,本算法不太理想,复制算法仅仅在垃圾多或者说是GCRoots可达对象不算多的时候效率更高。恰恰在新生代中,对象死亡率非常高,活着的对象不多,因此在s区使用复制算法。

清除阶段:标记-压缩算法

概述

对于老年代来说,大部分对象都是存活的对象,复制算法是建立在存活对象少,垃圾多的前提下,因此如果老年代也是用复制算法,效果不理想,是用其他算法,并且老年代空间大,如果再砍一半,浪费过多。

标记清除算法可以使用在老年代中,但是效率低,产生碎片,在此基础上产生了标记压缩算法。

执行过程

  • 第一阶段和标记清除算法一样,从根节点开始标记所有的引用对象
  • 第二阶段将所有存活的对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间,最终效果相当于标记压缩清除。

image.png

  • 优点:消除了标记清除算法中,内存分散的缺点,不用维护空闲列表,消除了复制算法中内存减半的代价
  • 缺点:效率低于复制算法,移动对象的同时,如果被其他对象引用,还需要调整引用的地址,移动过程中stw。

image.png

分代收集算法

由于没有最优的GC算法,只有最合适的,所以分代回收

不同对象生命周期不通,采取不同的的收集方式,提高效率

  • 年轻代 区域较老年代相比小,对象生命周期短,存活率低,收集频繁,此时使用复制算法,速度快,回收效率和对象存活数量有关,越少效率越高,而内存效率不高问题,通过设置两个s区设计缓解

  • 老年代 老年代区域大,对象生命周期长,存活率高,回收不频繁。采用标记清除,或者标记压缩算法混合实现。

  • Mark阶段开销和存活数量成正比

  • sweep阶段开销和管理区域大小成正比

  • compact阶段开销与存活对象数据成正比

以HotSpot中的CMS GC为例,基于Mark-Sweep实现,对于对象回收效率高,对于碎片问题,CMS采用Mark-Compact算法的Serial Old回收器作为补偿,当内存回收不佳,碎片导致的Concurrent Mode Failure时,将采用Serial Old执行FullGC达到堆老年代内存的整理。

增量收集算法、分区算法

增量收集概述

主要优化stw,优化用户体验,系统稳定性。

思想:一次性将系统所有垃圾进行处理,会导致系统长时间停顿,可以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到GC完成

  • 缺点:简短执行程序代码,会增加线程切换,上下文转换的消耗,使得垃圾回收整体成本上升,造成系统吞吐量的下降

分区算法概述

主要针对于G1,一般来说堆空间越大,一次GC时间越长,stw时间越长。为了控制,将一块大的内存分割成若干个小块,根据目标停顿的时间,每次合理地回收若干个小区间,而不是整个堆空间。从而减少一次GC的停顿。

image.png

垃圾回收相关概念

System.gc()的理解

概述

显式触发FullGC同时堆老年代和新生代GC,附带的免责声明:无法保证堆垃圾收集器的调用(不能确保什么时候GC)。一般不用手动写,自动触发即可。

public void test0() {
    {
        byte[] buffer = new byte[10 * 1024 * 1024];
    }
    System.gc();
}
public void test1() {
    {
        byte[] buffer = new byte[10 * 1024 * 1024];
    }
    int a = 10;
    System.gc();
}

test0中,局部方法表0中是this,索引1中在表中看不到,但是实际这个slot指向buffer,所以gc无法回收,而test1中,由于有a的缘故,此时索引1位置是a,而buffer无人引用,因此直接被回收了。(局部变量表槽位复用)

内存溢出与内存泄漏

  • 内存溢出OOM
    • 垃圾回收速度大于内存消耗速度不太容易出现oom
    • 大多数情况下GC会在各个阶段垃圾回收,实在不行就FullGC
    • 定义是没有空闲内存,GC也无法提供更多内存。
    • 可能的原因:
      • java虚拟机堆内存设置不够,不合理
      • 创建了大量大对象,并且不能被GC(存在引用)
  • 内存泄漏
    • 对象不被程序用到了,GC又不能回收的情况。
    • 有一些不太好的实践或者疏忽,导致对象生命周期变得很长甚至导致oom,宽泛意义上的内存泄漏,比如一些局部变量就可以,定义成了成员变量甚至是类变量。
    • 举例
      • 单例模式生命周期和应用程序一样长,所以单例程序中,如果持有对外部对象的引用的话,这个外部对象不能被回收,导致内存泄漏
      • 一些提供close的资源未关闭导致内存泄漏如,数据库连接,网络连接。

stw

GC发生过程中发生的停顿

  • 可达性分析算法,枚举根节点GCRoots会导致所有java执行线程停顿
    • 分析工作需要在一个保证一致性的快照进行
    • 一致性指整个分析期间整个系统看起来像被冻结在一点
    • 如果分析过程中引用发生变化,分析结果准确性无法保证
  • 所有GC都会STW
  • 开中不要中system.gc()会导致stw

垃圾回收的并行与并发

  • 并发:同一个处理器在一个时间段中不停快速切换多个线程进行执行。

image.png

  • 并行:有1个以上cpu时,n个cpu执行n个线程,互补抢占cpu资源为并行。

  • 垃圾回收器的并行与并发

    • 并行:parallel,多条垃圾收集线程并行工作,用户线程仍处于等待状态,如ParNew、Parallel Scavenge、Parallel Old
    • 串行:相较于并行的概念,单线程执行,如果内存不够,则程序暂停,启动JVM垃圾回收期回收,之后再启动程序线程
    • 并发:用户线程和垃圾收集线程同时执行,不一定是并行的,可能交替执行,垃圾回收线程在执行时不会停顿用户的程序的运行

安全点与安全区域

  • 安全点:程序只有在特定位置才能停下来GC,称为安全点
    • 安全点少可能导致GC等待时间长,多可能导致太频繁性能降低
    • 通常根据是否具有让程序长时间执行的特征为标准选择为安全点,如方法调用,循环跳转和异常跳转等。
    • 如何GC发生时,检查所有线程都跑到最近的安全点停顿下来
      • 抢断式中断(目前没有虚拟机采用):首先中断所有线程,如果有线程不在安全点,就将其回复,让线程跑到安全点
      • 主动式中断:设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果为真,将自己中断挂起。
  • 安全区域:如果线程处于sleep或者bolcked状态,无法响应jvm的中断请求,运行到安全点中断挂起不太可能,jvm也不可能等线程被唤醒,就需要安全区域。指的一段代码块片段中,对象引用关系不会发生变化,在这个区域任何位置开始GC都是安全的,相当于扩展了安全点
    • 执行时:当线程执行到saferegion代码时,首先表示已经进入了安全区,如果这段时间GC,jvm会忽略标识为safe region的线程
    • 当线程即将离开安全区时,会检查jvm是否完成gc如果完成继续运行,否在直到收到可以安全离开安全区的信号位置。

强引用

  • 最传统的定义,Object obj = new Object(),只要引用关系还存在,就不会被回收

软引用

  • 将要oom之前,把这些对象列入回收范围内进行二次回收,如果还没有足够内存才会抛出oom,内存不足即回收
Object obj = new Object(); // 声明强引用
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; // 销毁强引用

弱引用

  • 只能存活到下次GC前,只要GC就回收

虚引用

  • 虚引用,不会决定对象的生命周期,如果一个对象仅有虚引用,那么它和没有引用一样,任何时候都可能被GC掉,需要和引用队列ReferenceQueue联合使用。get方法总是返回null,其意义在于说明一个对象已经进入finalization阶段,可以被GC回收,用来实现比finalization机制更灵活的回收操作。死之前在引用队列有个通知机制,一般用不到

垃圾回收器

1、GC分类与性能指标

  • 按照线程数分可以分为串行垃圾回收器和并行垃圾回收器
    • 串行默认被应用在客户端Client模式下的jvm中(适合单核处理器,应用内存较小,硬件不流畅场合使用)
    • 并行运用多个cpu同时垃圾回收,提升应用吞吐量,与串行一样,也是独占式,需要stw
  • 按照工作模式分,可以分为并发和独占
    • 独占需要stw
    • 并发可以让gc和应用程序交替执行,减少应用程序停顿时间。
  • 按照碎片处理方式,分为压缩式和非压缩式
  • 按照工作内存区分,年轻代和老年代 性能指标:
  • 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
  • 暂停时间: 暂停的时间

注意低延迟,则每段暂停时间相对较小,但是会降低吞吐量,因为会切换线程。

2、不同的垃圾回收器概述

7种经典垃圾回收器:

  • 串行回收器:Serial,Serial Old
  • 并发回收器:ParNew,Parallel Scavenge,Parallel Old
  • 并发回收器:CMS,G1

垃圾回收器组合关系

image.png

image.png

image.png

  • 相当于只考虑黑色实线

3、Serial回收器:串行回收

新生代用Serial GC 老年代用Serial Old GC

串行回收,最基本,历史最悠久。Client模式下默认的垃圾回收器。

  • 新生代:复制算法 stw
  • 老年代:标记整理算法 stw

image.png

  • 优势:简单高效(针对其他GC的单线程对比),但是基本已经不用了。

4、ParNew回收器:并行回收

新生代垃圾回收器,采用复制算法,stw机制,除了是并行以外和串行没有区别

  • -XX:+UseParNewGc 新生代用parNewGC

5、Parallel回收器:吞吐量优先

parallel scavenge也是新生代并行垃圾回收器。采用复制算法,并行回收,和stw机制,性能差距也不大,但是可控制吞吐量throughput,吞吐量优先垃圾回收器。自适应调节机制也是该回收器和parnew的一个重要区别。

适用于后台运算,不需要太多交互的任务,如执行批量处理,订单处理,工资支付,科学计算的应用程序。

parallel收集器在1.6提供了适用于老年代的paralle Old收集器,用来代替老年代的Serial Old收集器。采用了标记压缩算法,并行回收,stw。

二者搭配使用是1.8默认GC

参数设置

  • -XX:+UseParallelGC手动指定年轻代时候该GC
  • -XX:+UseParallelOldGC手动指定老年代使用该GC,和上面的互相激活,设置一个即可,jdk8默认启用。
  • -XX:ParallelGCThreads 设置年轻代并行收集器的线程数,一般和cpu数量相等,避免过多线程数影响垃圾收集性能
    • 默认当cpu小于8个时候,设置为cpu数量
    • 如果大于8个,设置为3 + [5 * CPU_Count] / 8;
  • -XX:MaxGCPauseMillis stw时间设定,尽可能吧停顿时间控制在其中,GC会在工作时调整堆大小或者其他参数,使用需要谨慎
  • -XX:GCTimeRatio 垃圾收集时间占总时间比例 1/(N + 1)
    • 取值范围 0-100 默认99 垃圾回收时间不超过1%
    • 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性,暂停时间越长Ratio参数越容易超过设定比例
  • -XX:UseAdaptiveSizePolicy 设置parallel scavenge收集器具有自实行调节策略

6、CMS回收器(低延迟,并发 Concurrent-Mark-Sweep)

  • 第一次实现了垃圾回收线程和用户线程可以同时工作
  • java8默认垃圾回收器
  • 尽可能减少stw,适合低延迟与用户交互的程序
  • 采用了标记-清除算法
  • 作为老年代GC,无法与ParScavenge配合工作,只能和ParNew或者Serial之一配合

image.png

  • 初始标记:stw,所有线程都会暂停,标记出GCRoots能直接关联到的对象,直接关联到的对象比较小,所以速度很快
  • 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长但是不需要停顿用户线程
  • 重复标记:在并发标记阶段,程序工作线程和GC线程会同时/交叉运行,修正那部分期间,因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这阶段比并发标记时间段,比初始标记时间长。也发生stw,并发好几个重复标记线程,
  • 并发清除:清除掉标记阶段判定死亡的对象,释放内存空间,由于不需要移动存活对象,所以这个阶段可以与用户线程并发