一. JVM概览
1. JVM整体结构图
方法区和堆 共享
Java栈,本地方法栈,程序计数器独立
2. JVM 执行流程
执行器:解释执行字节码文件;
JIT编译器:编译字节码文件 为 机器指令,编译热点数据,放入缓存,方便重复使用,提高效率;
3.JVM 生命周期
虚拟机启动:通过虚拟机 引导类加载器 创建一个 初始类,这个类是由虚拟机的具体实现指定的。 例如:启动一个自定义类,jvm会使用 引导类加载器 创建一个初始类A,A中定义了 加载自定义类所需要的提前加载的父类 以及其他信息;
虚拟机运行:执行java代码时jvm就在运行状态
javap -c 文件名 反编译
jps查看jvm正在执行的进程
虚拟机退出:
出现异常或错误,退出
程序执行结束退出
主动调用Runtime类或System类的exit()方法,或Runtime类的halt()方法,并且Java安全管理器也允许这次 exit()或halt()操作
4.学习路径
- JVM 内存结构
- JVM的垃圾回收机制
- 字节码文件
- 类加载器
- JIT Compiler 运行时编译器
二. 类加载
1.类加载时机
- 第一次new 对象
- 第一次加载该类的子对象
- 第一次使用该类的静态变量和静态方法
- 通过反射显示类加载Class.forName(类全限定名)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
注意: 对于一个final类型的静态变量,如果该变量编译时就能够确定,外界第一次调用就不会触发类加载(例如:static final int a = 1;)
否则,就会触发类加载(例如:static final Integer a = 1;)
2.类加载过程
加载->验证->准备->解析->初始化
其中验证,准备,解析三个阶归属于 链接阶段
-
加载
jvm的类加载器会把class文件内容加载到JVM内存中,生成Class类对象;
-
验证
用于检验被加载的类是否有正确的内部结构,是否符合JVM规范,并和其他类协调一致。
-
准备
类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
-
解析
将类的二进制数据中的符号引用替换成直接引用。符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
-
初始化
初始化是为类的静态变量赋予正确的初始值。
3.类加载器
类的唯一标识
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
1. 根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
2. 扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
3. 系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它**负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader**。
4.类加载机制
双亲委派模型
原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势
- 避免类的重复加载,确保一个类的全局唯一性
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 保护程序安全,防止核心API被随意篡改
三.JVM 运行时内存结构
1.字节码文件结构
字节码文件主要包含:类文件描述信息,class常量池,方法描述,JVM字节码程序等
package cn.itcast.jvm.t5;
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("hello world");
}
}
javap -v HelloWorld.class
Classfile /D:/workspace-idea/review/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
Last modified 2021-1-8; size 567 bytes
MD5 checksum 8efebdac91aa496515fa1c161184e354
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 cn/itcast/jvm/t5/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public cn.itcast.jvm.t5.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
2.程序计数器
作用:记住JVM下一条指令的地址;
特点: 线程私有
永远不会发生内存溢出
3.虚拟机栈
虚拟机栈:又叫java线程栈,JVM会为每个线程开辟一个栈空间,线程栈之间互不影响;
栈帧:栈内部方法调用的的时候会产生一个栈帧压栈;
活跃栈帧:栈顶部的那一个栈帧
问题
-
垃圾回收是否涉及栈内存
不会,栈由栈帧组成,用完即弹出,对应的内存也自动清除
-
栈内存是否越大越好?
不是,栈内存越大,同时处理线程数越小
-Xss表示栈内存大小,linux,mac默认1m
假如:虚拟机栈空间一共500M,那么就能同时处理500个线程;若分配线程栈大小2M
那么只能同时处理250个线程
-
线程安全问题
局部变量线程安全,非局部变量线程内不安全
-
栈溢出
调用栈帧过多 :例如 递归调用,以及对象关系之间循环引用
栈帧过大
-
线上问题解决流程
-
线上cpu使用过高
- top 查看运行进程,找到cpu使用过高进程,例如 : 32600
- ps -H -eo pid,tid,%cpu | grep 32600 查看该进程哪一个线程 引起cpu使用率过高,例如:32655
- jstack 32655(线程ID) ,会打印该线程相关信息,通过信息具体定位哪一行出错 (需要32655转换为16进制,应为只会显示线程名,且只显示线程id的16进制)
-
线程运行很长时间没有结果(例如:死锁)
ps 查看程序进程
jstack 进程号 查看死锁线程
-
4. 本地方法栈
JAVA其实有部分页使用C或C++写的,这些 native关键字修饰的方法就是本地方法,这些方法的实现一般使用C或C++来实现的,在java中只是做一个调用;
例如:Object 类中的 clone(),hashCode()方法等;
5.堆
-Xmx 指定堆空间大小 ,默认4G
堆内存溢出:outOfMemeryError
堆内存溢出诊断:
jps 查看当前系统有哪些java进程,显示进程id,进程名
jmap -heap 进程id 显示进程占用堆内存情况
jconsole 图形界面方式显示java各种内存变化情况
案例:堆内存GC之后,仍然有大部分内存占用
jvirsualvm 命令 图形化查看,里面有个 堆dump查看那些对象占用内存过多
6.方法区
1.存储内容以及版本比较
1.6以及之前:方法区是一个概念,被存储在永久代 ,存储类的元数据信息,类加载器信息,运行时常量池以及stringtable(串池)
1.7:运行时常量池还是在方法区,永久代中,但是 字符串常量池放在了堆中
1.8:方法区为一个概念,存储在操作系统的本地内存,在本地内存划分了一个 元数据空间,不再划到JVM ;类信息,类加载器,运行时常量池储存在 元数据空间,另外串池数据存在堆中
2.方法区内存溢出
-XX:MaxMetaspaceSize元数据空间大小
字节码文件包含信息:类基本信息 常量池 类方法定义 ,虚拟机指令
javap -c *.class 反编译
javap -v *.class 类反编译后详细信息
7.常量池(class常量池)以及运行时常量池
1.常量池(class常量池)
java被编译为 class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池;
常量池又叫class常量池,它是被JVM加载到内存方法区内部的字面量以及符号应用的集合
常量池:常量池是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名。参数类型,字面量等一些信息;
2.运行时常量池
运行时常量池时JVM加载class文件后 将class常量池内容转移到 运行时常量池(所以每一个class文件都会有一个运行时常量池),它是动态的,内容包含了编译class文件的常量池和 运行时新增的常量信息
8.StringTable
1.intern()方法
参考:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
intern()方法把字符串强制放入串池中,并返回串池中的对象:
- 1.6版本: 串池中有则不添加返回串池中对象;没有则复制堆中对象,生成另一个对象放入串池,返回串池对象,此时两个对象不同;
- 1.7以后版本: 串池中有则不添加返回串池中对象;没有则把堆中对象的引用放入串池,返回串池对象,此时两个对象相同;
2.在Java中有两种创建字符串对象的方式:
参考:https://blog.csdn.net/qq_45737068/article/details/107149922
- 采用字面值的方式赋值
- 采用new关键字新建一个字符串对象(会在堆和字符串常量池中个创建一个)
3.面试题
-
面试题:
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); } }
-
StringTable 调优
StringTable底层数据结构为hashtable ,使用数组+链表的形式,所以我们只要改变buket的数量就能优化程序;
–XX:StringTableSize=60086 设置buket个数
-
优化场景
当程序中有海量的字符串存储,且有大量重复的数据可以考虑将堆对象intern()入池,减少内存占用
9.直接内存
-
什么是直接内存?
直接内存是操作系统中的缓冲内存,不贵JVM管理,所以垃垃圾回收的时候JVM无法回收;
但是直接内存可以手动分配,手动回收;
java代码调用allocateDirect(size)分配直接内存,通过Unsafe类的freeMemory()方法手动回收;
当然也可以JVM回收调用直接内存的对象,通过回收该对象,触发直接内存的回收机制
-
特点
- 分配和回收艰难,读写效率高
- 不收JVM管理
- 常见于NIO操作,用作数据缓冲
-
直接内存优化?
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代码可以直接操作,性能较高
-
直接内存分配释放原理
分配:调用allocateDirect()方法会生成一个 DirectByteBuffer对象,DirectByteBuffer的构造方法里调用Unsafe类的setMemory()方法设置分配 直接内存大小,还生成Cleaner对象,方便直接内存回收;
回收:生成Cleaner有个回调方法会创建一个Deallocator 对象,它实现Runnable接口,Cleaner(弱引用对象)弱引用于ByteBUffer对象,当ByteBUffer被回收时,Cleaner会被放入引用队列中,ReferenceHandler守护线程会从引用队列获取Clener对象,通过Cleaner的clean方法调用freeMemory本地方法释放内存;
四.垃圾回收
GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。
1.如何判断垃圾是否可以回收?
1.引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1。
但是,如果出现循环引用的情况,对象无法回收,如下图所示
2.可达性分析法
所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。
Tracing GC的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡。注意再注意:tracing GC的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。
1.可以被当做GC ROOT的对象:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.方法区中的类静态属性引用的对象
3.方法区中的常量引用的对象
4.原生方法栈(Native Method Stack)中 JNI 中引用的对象。
5.处于激活状态的线程
6.正在被用于同步的各种锁对象
7.JVM自身持有的对象,比如系统类加载器等
8.通过System Class Loader或者Boot Class Loader加载的class对象(通过自定义类加载器加载的class不一定是GC Root)
参考:
https://bbs.csdn.net/topics/390669860
https://zhuanlan.zhihu.com/p/181694184
2.被GC判断为”垃圾”的对象一定会回收吗?
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)
如果这个对象被判定为有必要执行finalize()方法,那么**这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。**这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
参考:
https://blog.csdn.net/mine_song/article/details/63251367?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control
2.五种引用
1.五种引用的区别:
- 强引用 :只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
- 软引用:(SoftReference): 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身 (内存紧张时,一些不重要的文件,图片信息用软引用存放)
- 弱引用(WeakReference) :仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用引用的对象 可以配合引用队列来释放弱引用对象自身 (ThreadLocal本地线程原理中使用)
- 虚引用(PhantomReference):以NIO为例,在创建ByteBuffer的时候,会创建一个名为Cleaner的虚引用对象,ByteBuffer会分配一个块直接内存,并把内存地址传递给Cleaner;这样做的目的是当ByteBuffer没有被强引用时,会被垃圾回收掉,但是直接内存并不能java的垃圾回收管理,此时Cleaner会进入引用队列,由Reference Handler线程调用Unsafe.freeMemory方法把直接内存释放掉。
- 终结器引用(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不会管理的特殊区域;
特殊区域:
-
GC一般管理显示new出来的java对象,但是有一些内存空间是有本地方法(Native method)J(C,C++等语言)创建出来;这些内存不归JVM管理,需要手动调用对应这些C或C++的方法释放内存;
所以,通常涉及到这些内存的释放,需要覆盖finalize()方法,在覆盖方法里执行C或C++方法
-
打开的文件资源,这些资源也不属于垃圾回收器的回收范围。
一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用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.分代回收
新生代一般使用 复制算法,老年代一般使用标记清除或标记整理算法
回收步骤:
- 新生对象首先进入Eden
- 新生代空间不足时触发MinorGC,把Eden和from的存活数据复制到to,然后清除Eden和from,并发存活对象年龄+1,最后交换from和to区域(MinorGC会触发Stop The World 时,应用程序线程会被阻塞,直到GC线程结束)
- 当对象年龄超过阈值(最大寿命15),把对象从新生代放入老年代
- 当老年代触发GC,会先尝试进行MinorGC,空间仍然不足会触发FullGC,非常耗时
4.垃圾回收器
1.串行垃圾回收器(Serial/serial Old)
GC线程执行时,用户线程阻塞
新生代,老年代都是串行
新生代:复制算法
老年代:标记整理
2.并行垃圾回收器
多个GC线程间并发执行,GC线程和与用户线程并行执行,GC执行时用户线程阻塞
ParNew: Serial的并行模式,新生代并行,老年代串行;新生代复制算法、老年代标记-压缩;
Parallel Old:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法;
Parallel Scavenge:类似ParNew,更注重吞吐量
参数详解:
-XX:+UseAdaptiveSizePolicy 自适应对大小策略
-XX:GCTimeRatio GC时间占运行时间比例,公式1/(1+Ratio),例如Ratio为99,则单位时间内要求GC时间为1/100(程序运行100分钟,GC时间不操作1分钟),当超过1/100时,会缩小堆空间
-XX:MaxGCPauseMillis=ms GC导致程序最大暂停毫秒数
-XX:ParallelGCThreads=n GC线程数,一般为cpu核数
3.响应时间优先
CMS(Concurrent Mark Sweep)并发式标记清理垃圾回收器(主要用于老年代)
- 初始标记 需要Stop The World 仅仅标记GC Roots对象
- 并发标记 GC线程与用户线程并发执行,沿着GC Roots遍历引用链,并发标记阶段就是进行GC Roots Tracing的过程,时间较长
- 重复标记 因为并发标记时用户线程在执行过程中,可能会产生新的垃圾对象,需要STW
- 并发清除 GC线程与用户线程并发执行
由于CMS是标记清除算法,会有空间碎片,当老年代满时,可以选择退化为标记整理的垃圾回收器,例如:Serial Old
缺点:
- 标记整理算法,会有大量内存碎步,但是可以通过XX:CMSFullGCsBeForeCompaction 设置几次CMS回收后,使用Full GC进行一次碎片整理
- CMS并发清理时,与用户线程并发执行,并发清理阶段用户线程可能产生新的垃圾对象,所以GC必须在堆内存占满前完成
参数详解:
-XX:+ UseCMSCompactAtFullCollection
Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction
设置进行几次CMS回收后,使用Full GC进行一次碎片整理
-XX:ParallelCMSThreads
设定CMS的线程数量(一般情况约等于可用CPU数量)
-XX:ConcGCThreads:=threads CMS并发线程数
-XX:CMSInitiatingOccupancyFraction=percent 代表老年代空间占用达到percent%进行一次GC,由于并发清理时,用户线程也在执行,所以可能会产生新的垃圾对象,不能等老年代空间占满后才进行GC
-XX:+CMSScavengeBeforeRemark 重新标记之前对新生代进行垃圾回收,减少重新标记遍历对象
4.常见垃圾回收期组合
新生代GC策略 | 老年老代GC策略 | 说明 | |
---|---|---|---|
1 | Serial | Serial Old | Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。 |
2 | Serial | CMS+Serial Old | CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。 |
3 | ParNew | CMS | 使用 -XX:+UseParNewGC 选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项 -XX:+UseConcMarkSweepGC 选项,则新生代默认使用ParNew GC策略。 |
4 | ParNew | Serial Old | 使用 -XX:+UseParNewGC 选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。 |
5 | Parallel Scavenge | Serial Old | Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。 |
6 | Parallel Scavenge | Parallel Old | Parallel Old是Serial Old的并行版本 |
7 | G1GC | G1GC | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启; -XX:MaxGCPauseMillis=50 #暂停时间目标; -XX:GCPauseIntervalMillis=200 #暂停间隔目标; -XX:+G1YoungGenSize=512m #年轻代大小; -XX:SurvivorRatio=6 #幸存区比例 |
5.G1垃圾回收器
参考:
https://blog.csdn.net/coderlius/article/details/79272773
https://blog.csdn.net/shlgyzl/article/details/95041113?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.control
1.G1的特点
- G1的设计原则是"首先收集尽可能多的垃圾(Garbage - First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部- 采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时- 间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
- G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进- 行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天- 然就是一种压缩方案(局部压缩);
- G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的- survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不- 同代之间前后切换;
- G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次- 收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合- 收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿
2.G1内存模型
G1垃圾回收器取消了新生代,老年代物理内存的划分,而是把整个堆内存划分成多个region区域,切每个region区域大小一致;
region又分为四类,分别是Eden,Survivor,Old,以及Humongous 巨大对象区域
3.垃圾回收阶段
大致分为三个阶段:新生代会后Young GC, 并发标记Concurrent mark阶段和Mixed GC混合回收阶段
1.Young GC
新生代的会后与之前的垃圾回收相同,新生代空间占满,进入Young GC阶段,会把存活对象放入 Survivor幸存区,如果survior区也满了就直接放入老年代
Young GC 阶段:
- 阶段1:根扫描
静态和本地对象被扫描 - 阶段2:更新RS
处理dirty card队列更新RS - 阶段3:处理RS
检测从年轻代指向年老代的对象 - 阶段4:对象拷贝
拷贝存活的对象到survivor/old区域 - 阶段5:处理引用队列
软引用,弱引用,虚引用处理
2.Mixed GC阶段1- 全局并发标记
- 初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。 - 根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。 - 并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断 - 最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。 - 清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
3. Mixed GC阶段2- 拷贝存活对象
不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区
4.问题
1.Young GC时的跨代引用问题
当YoungGC时,回收新生代,那么怎么获取老年代GCRoots呢?
采用Remembered Set 和 Card Table 的形式
2.Remembered Set
CMS中:在老年代中划分了一块区域 用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。(记录老年代的对象引用了哪些新生代对象,及记录在老年代的一块区域)
G1使用point-in:意思是哪些分区引用了当前分区中的对象;新生代的Remembered Set 会记录老年代到新生代之间的引用;(记录 了引用该区域的 region区的对象,记录的是新生代对象被哪些老年代对象引用)
但是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)
一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
3. 卡表(Card Table)
一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。
4.如何保证应用程序在运行的时候,GC标记的对象不丢失呢?
有如下2中可行的方式:
- 在插入的时候记录对象
- 在删除的时候记录对象
刚好这对应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空间无法容纳该对象,就只能在老年代中进行分配空间。
- TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
- TLAB无法分配的对象,尝试放在Eden中
- 当Eden中放不下就只能放入老年代
对象分配规则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
五.GC调优
参考:https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247483966&idx=1&sn=dfa3375d36aa2c0c25a775522e381e62&chksm=ebf6da41dc815357e0d53c73865a23f41219e75bac5a4d510bfa31cc51594b59a20e2e4f6cb8&cur_album_id=1326602114365276164&scene=189#rd
https://www.cnblogs.com/shanheyongmu/p/5775003.html
新生代GC调优
新生代空间大小一占总堆内存的25%~50%,空间小,容易频繁MinorGC,空间大
调增老年代晋升阈值