类中赋初值时间

  • static修饰:

​ 准备阶段赋默认值,初始化阶段赋初值

  • final static/final修饰:

  • 准备阶段赋值

    编译阶段值已经确定,并且字面量和常量都会在class常量池出现,创建实例时在队中创建,如果可以将字面量存入缓存.eg:Integer缓存池.

  • 实例变量:

    创建实例时创建.

JVM类加载器

启动类加载器:

java、javax、sun等开头的类

扩展类加载器:

JDK的安装目录的jjre/lib子目录下的ext的子目录的类库

应用程序类加载器:

它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库

栈帧的返回值

invoke2()调用invoke1()

invoke1()的返回值压入invoke2()的操作数栈

image-20220319123007769

关于符号引用和直接引用

  1. 在类加载链接中的解析中,将符号引用转化为直接引用(运行时常量池中的地址)
  2. 动态连接:用来解决多态问题虚拟机栈中的栈帧指向运行时常量池的方法引用

动态连接

每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

解析与分派 · 语雀 (yuque.com)

  1. 执行invokevirtual/invokedynamic调用方法
  2. 去方法区中的虚方法表找方法真正的入口地址
  3. 创建栈帧
  4. 栈帧中的动态链接指向调用方法类中的运行时常量池

解析的目的

将符号引用转为直接引用。

类加载阶段的解析连接阶段的最后一步:将能确定了的东西合并到jvm运行环境中去

**运行期的解析:**将确定了多态使用子类还是父类方法后的方法地址合并到jvm运行环境中去

分派的目的

确定方法的接收者。

静态分派编译期间确定方法的接收者(没有多态的那些)。在类加载阶段解析

动态分派运行期间确定多态使用子类还是父类方法后的方法。在运行期第一次使用时解析

Runtime

每个JVM只有一个Runtime实例。即为运行时环境

局部变量表

定义为一个数字数组,存储方法参数和定义在方法体内的局部变量

Slot

  1. 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
  2. byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
  3. JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
  4. 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处

复用

1
2
3
4
5
6
7
8
public void localVar2() {
{
int a = 0;
System.out.println(a);
}
//此时的就会复用a的槽位
int b = 0;
}

image-20220323102838243

静态变量与局部变量的对比

局部变量必须显式初始化

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

操作数栈

所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值

  • 32bit的类型占用一个栈单位深度

  • 64bit的类型占用两个栈单位深度

方法重写

  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。

  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError 异常。

  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

  4. 如果始终没有找到合适的方法,则抛出java.1ang.AbstractMethodsrror异常。

虚方法表

虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕

堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续

  • 初始内存大小:物理电脑内存大小 / 64

  • 最大内存大小:物理电脑内存大小 / 4

  • 一般-Xms和-Xmx设为相同的

参数

-XX:NewRatio = 2 表示新生代1,老年代2

-XX:SurvivorRatio = 8 表示Eden区8

-XX:MaxTenuringThreshold = N 去养老区

-XX:UseTLAB:设置是否开启TLAB空间

-XX:TLABWasteTargetPercent:设置TLAB空间所占用Eden空间的百分比大小

-XX:+DoEscapeAnalysis:显式开启逃逸分析

-XX:+PrintEscapeAnalysis:查看逃逸分析的筛选结果

-XX:EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配到栈上

老年代GC

出现了Major Gc,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)

  • 也就是在老年代空间不足时,会先尝试触发Minor Gc。如果之后空间还不足,则触发Major GC

不同年龄段的对象分配原则

动态对象年龄判断:如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

Minor GC安全

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

  • 如果大于,则此次Minor GC是安全的

  • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允担保失败。

    • 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
      • 如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
      • 如果小于,则改为进行一次Full GC。
    • 如果HandlePromotionFailure=false,则改为进行一次Full Gc。

在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行FullGC。

逃逸分析:代码优化

一、栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配

二、同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

三、分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中

注意

普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上

方法区

栈、堆、方法区的交互关系

img

方法区的基本理解

  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

  • 关闭JVM就会释放这个区域的内存

参数

jdk7及以前

-XX:Permsize:永久代初始分配空间。默认值是20.75M

-XX:MaxPermsize:永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M

JDK8以后

1
-XX:MetaspaceSize` 和 `-XX:MaxMetaspaceSize

为避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值

直接内存大小可以通过MaxDirectMemorySize设置。如果不指定,默认与堆的最大值-Xmx参数值一致

解决OOM

  1. 要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
  2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
  3. 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

方法区的内部结构

类型信息

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  1. 这个类型的完整有效名称(全名=包名.类名)
  2. 这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
  3. 这个类型的修饰符(public,abstract,final的某个子集)
  4. 这个类型直接接口的一个有序列表

域(Field)信息(字段)

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

方法(Method)信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  1. 方法名称
  2. 方法的返回类型(或void)
  3. 方法参数的数量和类型(按顺序)
  4. 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
  5. 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
  6. 异常表(abstract和native方法除外)
    • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

non-final的类变量

  • 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分

  • 类变量被类的所有实例共享,即使没有类实例时,你也可以访问它

补充说明:全局常量(static final)

被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

JIT代码缓存放在方法区哪里

转->12.9

几种常量池内存储的数据类型

  • 数量值

  • 字符串值

  • 类引用

  • 字段引用

  • 方法引用

运行时常量池

  • 池中的数据项像数组项一样,是通过索引访问的。
  • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常。

静态变量和字符串常量池位置

JDK1.6及之前 有永久代(permanet),静态变量存储在永久代上
JDK1.7 有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中
JDK1.8 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。

img

img

img

方法区的垃圾收集

主要回收两部分内容:常量池中废弃的常量和不再使用的类型

StringTable为什么要调整位置

StringTable会存放方法名,包名之类的字符串字面量

方法区不怎么进行垃圾回收

判定一个类型是否属于“不再被使用的类”

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。

  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。

  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

对象实例化及直接内存

创建对象的方式

  • new:最常见的方式、Xxx的静态方法,XxxBuilder/XxxFactory的静态方法

  • Class的newInstance方法:反射的方式,只能调用空参的构造器,权限必须是public

    1
    2
    3
    4
    //先使用加载器加载类
    Class c = Class.forName(“A”);
    //只能调用无参构造
    factory = (AInterface)c.newInstance();
  • Constructor的newInstance(XXX):反射的方式,可以调用空参、带参的构造器,权限没有要求

    1
    2
    3
    4
    //能够调用无参有参构造
    Constructor c1=c.getDeclaredConstructor(new Class[]{int.class,int.class});
    c1.setAccessible(true);
    A a1=(A)c1.newInstance(new Object[]{5,6});
  • 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口,实现clone()

  • 使用序列化:从文件中、从网络中获取一个对象的二进制流

  • 第三方库 Objenesis

创建对象的步骤

img

https://www.yuque.com/u21195183/jvm/rdng01#3ea7094c

堆里分配引用变量大小

4个字节

什么时候将分配的空间给引用

初始化执行(包括静态代码块和构造方法)以后

给对象属性赋值的操作

  • 属性的默认初始化


  • 显式初始化

  • 代码块中初始化

  • 构造器中初始化

对象内存布局

img

img

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

句柄访问

img

直接访问(hotspot采用)

img

阈值参数

-XX:CompileThreshold

方法调用计数器就用于统计方法被调用的次数,它的默认阀值在Client模式下是1500次,在Server模式下是10000次。超过这个阈值,就会触发JIT编译

HotSpotVM 可以设置程序执行方法

缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:

  • -Xint:完全采用解释器模式执行程序;

  • -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行

  • -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。

StringTable

String的基本特性

  • String在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]

  • String声明为final的,不可被继承

字符串拼接操作

  • 常量与常量的拼接结果在常量池,原理是编译期优化
  • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
  • 在实际开发中,能够使用final的,尽量使用
  • 那么,在实际开发中,对于需要多次或大量拼接的操作,在不考虑线程安全问题时,我们就应该尽可能使用StringBuilder进行append操作
  • StringBuilder空参构造器的初始化大小为16。那么,如果提前知道需要拼接String的个数,就应该直接使用带参构造器指定capacity,以减少扩容的次数

intern

intern是一个native方法,调用的是底层C的方法

img

intern的使用:JDK6 vs JDK7/82

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
/**
* ① String s = new String("1")
* 创建了两个对象
* 堆空间中一个new对象
* 字符串常量池中一个字符串常量"1"(注意:此时字符串常量池中已有"1")
* ② s.intern()由于字符串常量池中已存在"1"
*
* s 指向的是堆空间中的对象地址
* s2 指向的是堆空间中常量池中"1"的地址
* 所以不相等
*/
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s==s2); // jdk1.6 false jdk7/8 false

/*
* ① String s3 = new String("1") + new String("1")
* 等价于new String("11"),但是,常量池中并不生成字符串"11";
*
* ② s3.intern()
* 由于此时常量池中并无"11",所以把s3中记录的对象的地址存入常量池
* 所以s3 和 s4 指向的都是一个地址
*/
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3==s4); //jdk1.6 false jdk7/8 true

img

img

总结String的intern()的使用:

JDK1.6中,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址

  • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址

JDK1.7起,将这个字符串对象尝试放入串池。

  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址

  • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址

img

s2中直接把0x1122复制过来

G1中的String去重操作

实现

  1. 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象

  2. 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的string对象。

  3. 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。

  4. 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。

  5. 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。

命令行选项

1
2
3
4
5
6
# 开启String去重,默认是不开启的,需要手动开启。 
UseStringDeduplication(bool)
# 打印详细的去重统计信息
PrintStringDeduplicationStatistics(bool)
# 达到这个年龄的String对象被认为是去重的候选对象
StringpeDuplicationAgeThreshold(uintx)

垃圾回收

垃圾回收相关算法

https://www.yuque.com/u21195183/jvm/fkl9x7#0dc8ebb2

垃圾回收相关概念

内存溢出

没有空闲内存,并且垃圾收集器也无法提供更多内存

在抛出OutOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间

也不是在任何情况下垃圾收集器都会被触发的

  • 比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError。

内存泄漏

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,也可以叫做宽泛意义上的“内存泄漏”

举例

  1. 单例模式
    单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。

  2. 一些提供close的资源未关闭导致内存泄漏
    数据库连接(dataSourse.getConnection() ),网络连接(socket)和io连接必须手动close,否则是不能被回收的。

Stop The World

可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。

  • 分析工作必须在一个能确保一致性的快照中进行

  • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上

  • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

开发中不要用System.gc() 会导致Stop-the-World的发生。

安全点与安全区域

安全点主动式中断

设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。(有轮询的机制)

https://www.yuque.com/u21195183/jvm/nwkhey#68cf3419

安全区域

在安全区域内(例如线程睡眠)的对象引用关系不会发生变化,因此在GC时忽略这些线程(进安全区域时会给它一个标识),

当这些线程要出安全区域的时候看GC是否结束,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止;

软引用

高速缓存就有用到软引用

弱引用

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

软引用、弱引用都非常适合来保存那些可有可无的缓存数据

虚引用

虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

终结器引用

https://www.yuque.com/u21195183/jvm/nwkhey#9e3135f0

垃圾回收器

7种经典的垃圾收集器

  • 串行回收器:Serial、Serial Old

  • 并行回收器:ParNew、Parallel Scavenge、Parallel old

  • 并发回收器:CMS、G1

img

如何查看默认垃圾收集器

-XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)

使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

Serial回收器:串行回收

Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器。

Serial收集器采用复制算法、串行回收和”stop-the-World”机制的方式执行内存回收。

Serial Old收集器采用了标记-压缩算法,串行回收和”Stop the World”机制,只不过内存回收算法使用的是。

在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial Old GC

ParNew回收器:并行回收

ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器

Par是Parallel的缩写,New:只能处理的是新生代

ParNew 收集器:复制算法,并行运行,stw机制

可以搭配serial old(废弃)和cms

在程序中,开发人员可以通过选项”-XX:+UseParNewGC“手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。

Parallel回收器:吞吐量优先

多用于交互不多的应用场景

复制算法、并行回收和”Stop the World”机制

那么Parallel 收集器的出现是否多此一举?

  • 和ParNew收集器不同,ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。

  • 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。

2Parallel Old收集器

标记压缩,并行回收和stw

参数配置

  • -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。

  • -XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。

    • 分别适用于新生代和老年代。默认jdk8是开启的。
    • 上面两个参数,默认开启一个,另一个也会被开启。(互相激活)

https://www.yuque.com/u21195183/jvm/kpugvm#86eb8b14

-XX:+UseAdaptivesizePolicy 设置Parallel Scavenge收集器具有自适应调节策略

    • 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
    • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

CMS回收器:低延迟

标记清除算法,并行并发,stw

无法和parllel 搭配

收集过程

整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

img

  • 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。

  • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

  • 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

  • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

什么时候收集

由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用.因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收(如果留的内存不够,启用serial old收集器进行收集)

设置的参数

https://www.yuque.com/u21195183/jvm/kpugvm#5052fd67

G1回收器:区域化分代式(并行和并发)

为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。

在延迟可控的情况下尽可能高的提高吞吐量

延迟可控:在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大(垃圾占比最多,所以叫garbage-first)的Region

空间整合

  • CMS:“标记-清除”算法、内存碎片、若干次Gc后进行一次碎片整理

  • 将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法

G1垃圾收集器的缺点

G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。

从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。

参数

  • -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务

  • -XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。

  • -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms(人的平均反应速度)

  • -XX:+ParallelGCThread 设置STW工作线程数的值。最多设置为8(上面说过Parallel回收器的线程计算公式,当CPU_Count > 8时,ParallelGCThreads 也会大于8)

  • -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。

  • -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。

调优

  1. 开启G1垃圾回收器
  2. 设置堆的最大内存
  3. 设置最大的停顿时间

三种垃圾回收模式

Young GC、Mixed GC和Full GC

G1收集器的适用场景

面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)

最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)。

用来替换掉JDK1.5中的CMS收集器;在下面的情况时,使用G1可能比CMS好:

  • 超过50%的Java堆被活动数据占用;

  • 对象分配频率或年代提升频率变化很大;

  • GC停顿时间过长(长于0.5至1秒)

HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

H区

设置H的原因:对于堆中的对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。

每个Region都是通过指针碰撞来分配空间

img

回收过程

当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程

标记完成后开始四到五次的混合回收

混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

img

1)G1执行的第一阶段:初始标记(Initial Marking )

这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象。

2)G1执行的第二阶段:并发标记

从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。当并发标记完成后,开始最终标记(Final Marking )阶段

3)最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)

4)筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)

https://www.yuque.com/u21195183/jvm/kpugvm#8038353d

7种经典垃圾回收器总结

https://www.yuque.com/u21195183/jvm/kpugvm#db9e4939

eden区不够时

先进行minorGC

class文件结构

常量池计数器

为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。