定义:
Java Virtual Machine - java 程序的运行环境 (java二进制字节码的运行环境)
好处:
- 一次编译到处执行 (屏蔽了字节码和底层操作系统之间的差异,对外提供了一致的运行环境)
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态 (提升程序的可扩展性)
1.程序计数器
Program Counter Register 程序计数器(寄存器)
1.2 作用
左边是二进制字节码 jvm 指令 右边是 java源代码
由二进制字节码经过解释器解释成->机械码->交给CPU
第一条指令执行之后解释器就会在程序计数器中找到下一条需要执行的指令
程序计数器在寄存器中(由于读写频繁)
程序计数器的作用其实就是记住下一条jvm指令的执行地址
- 特点
- 是线程私有的
- 不会存在内存溢出
2.Java 虚拟机栈
2.1
- 每个线程运行时所需要的内存,成为虚拟机栈
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
逃逸分析
* 如果方法内局部变量没有逃离方法的作用访问,他是线程安全的
* 如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全
2.2 栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
2.3 线程运行诊断
案例1:cpu占用过多
定位
* 用 top定位哪个进程对cpu的占用过高
* ps H(打印) -eo(感兴趣) pid,tid,%cpu | grep 进程(id) (用ps命令进一步定位是哪个线程引起的cpu占用过高)
* jstack + 进程ID(可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号)
案例2:程序运行很长时间没有结果
- 死锁 (互斥,请求与保持,不可剥夺,循环等待)
3.本地方法栈
给本地方法提供运行空间
4.堆
4.1 定义
Heap堆
- 通过new 关键字,创建按对象都会使用堆内存
特点 - 他是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2堆内存溢出
4.3堆内存诊断
- 1.jps工具-查看当前系统中有哪些Java进程
- 2.jmap工具-查看堆内存占用情况
- 3.jconsole工具-图形界面的,多功能的监测工具,可以连续监测
jps
/**
* 演示堆内存
*/
public class Demo {
public static void main(String[] args) throws InterruptedException {
System.out.println("1....");
Thread.sleep(30000);
byte[] bytes = new byte[1024 * 1024 * 10];
System.out.println("2....");
Thread.sleep(30000);
bytes = null;
System.gc();
System.out.println("3....");
Thread.sleep(10000000L);
}
}
jconsole
案例:
- 垃圾回收后,内存占用仍然很高
可以用JvisualVM分析类似于jconsole
5.方法区
5.3方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8之后会导致元空间内存溢出
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出
* -XX:MaxMetaspaceSize = 8m
*/
public class Demo_01 extends ClassLoader{//可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo_01 test = new Demo_01();
for (int i = 0; i < 10000; i++,j++) {//加载10000个类
//ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号,public 访问修饰符,类名,包名,父类,接口
cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
//返回 byte[]
byte[] code = cw.toByteArray();
//执行类的加载
test.defineClass("Class"+i,code,0,code.length);//Class对象
}
}finally {
System.out.println(j);
}
}
在VM options中加入
-XX:MaxMetaspaceSize=8m 来限制元空间大小
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示永久代内存溢出
* -XX:MaxPermSize=8m
*/
public class Demo_01 extends ClassLoader{//可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo_01 test = new Demo_01();
for (int i = 0; i < 20000; i++,j++) {//加载10000个类
//ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号,public 访问修饰符,类名,包名,父类,接口
cw.visit(Opcodes.V1_6,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
//返回 byte[]
byte[] code = cw.toByteArray();
//执行类的加载
test.defineClass("Class"+i,code,0,code.length);//Class对象
}
}finally {
System.out.println(j);
}
}
}
1.8已经不支持了,感兴趣的可以用1.6尝试(博主就偷偷懒啦~)
场景
- spring 用CGLIB生成一些代理类,是AOP的核心
- mybaits 用CGLIB产生Mapper接口的实现类
上面的ClassWriter实现ClassVisitor
$ClassWriter 继承 ClassVisitor
共同点:都是在运行期间动态生成类的字节码完成动态的类加载
代理技术里广泛的应用了这种字节码的动态生成技术
1.8之后由于使用的是系统内存,所以空间相对充裕很多,内存溢出的情况有很大改善
5.4运行时常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是 *.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
//二进制字节码(类的基本信息,常量池,类方法定义,包含了虚拟机指令)
public class Demo_02 {
public static void main(String[] args) {
System.out.println("hello world");
}
}
首先进入编译后的class文件所在目录,然后再控制台输入javap -v XXX.class
类的基本信息
常量池
类中的方法定义
5.5 StringTable特性
- 常量池中的字符串仅仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串标量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池
//串池
//StringTable ["a","b","ab"] 在数据结构上相当于Hash表 hashtable结构,不能扩容
public class Demo_03 {
//常量池中的信息,都会被加载到运行时常量池中,
//这时a b ab 都是常量池中的符号,还没有变为java字符串对象、
//eg: ldc #2 会把 a 符号变为 “a” 字符串对象 同时准备好空间(StringTable)
// 把 “a”作为key在StringTable中找
// 没找到则将 “a”放入串池
//ldc #2 会把 a 符号变为 “a” 字符串对象
//ldc #2 会把 b 符号变为 “b” 字符串对象
//ldc #2 会把 ab 符号变为 “ab” 字符串对象
public static void main(String[] args) {
String s1 = "a"; //懒惰的行为
String s2 = "b";
String s3 = "ab";
}
}
两个String拼接底层是怎么做到的?
public static void main(String[] args) {
String s1 = "a"; //懒惰的行为
String s2 = "b";
String s3 = "ab";
String s4 = s1+s2;//new StringBuilder().append("a").append("b").toString()
// new String("ab")
System.out.println(s3 == s4);//false
}
}
public static void main(String[] args) {
String s1 = "a"; //懒惰的行为
String s2 = "b";
String s3 = "ab";
String s4 = s1+s2;//new StringBuilder().append("a").append("b").toString()
// new String("ab")
String s5 = "a" + "b"; //javac 在编译期间的优化,结果已经在编译期间确定为ab
System.out.println(s3 == s5);//true
}
直接用串池中已经有的 “ab”
//["a","b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a")+new String("b");
//堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
System.out.println(s2 == x);//true
System.out.println(s == x);//false
}
👆本例中 String x = “ab” 先将 ab 放入串池,然后放入"a",“b”,创建 "ab"在堆中
将s放入串池 发现串池中已经有 “ab”,所以将串池中的"ab"返回
s2为串池中的 ab 也就是 x
而 s 为堆中的 ab 所以比较为false
5.6 StringTable 位置
5.7 StringTable 垃圾回收
/**
* 演示StringTab垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
* 堆内存最大值 打印字符串表统计信息 打印垃圾回收的信息
*/
public class Demo_05 {
public static void main(String[] args) throws InterruptedException{
int i = 0;
try {
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->786K(9728K), 0.0021355 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0
Heap
PSYoungGen total 2560K, used 593K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1a560,0x00000000fff00000)
from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 298K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 4% used [0x00000000ff600000,0x00000000ff64a830,0x00000000ffd00000)
Metaspace used 3244K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13330 = 319920 bytes, avg 24.000
Number of literals : 13330 = 571968 bytes, avg 42.908
Total footprint : = 1051976 bytes
Average bucket size : 0.666
Variance of bucket size : 0.668
Std. dev. of bucket size: 0.817
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1721 = 41304 bytes, avg 24.000
Number of literals : 1721 = 154792 bytes, avg 89.943
Total footprint : = 676200 bytes
Average bucket size : 0.029
Variance of bucket size : 0.029
Std. dev. of bucket size: 0.170
Maximum bucket size : 2
/**
* 演示StringTab垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
* 堆内存最大值 打印字符串表统计信息 打印垃圾回收的信息
*/
public class Demo_05 {
public static void main(String[] args) throws InterruptedException{
int i = 0;
try {
for (int j = 0; j < 10000; j++) {
String.valueOf(j).intern();
i++;
}
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 11663 = 279912 bytes, avg 24.000
Number of literals : 11663 = 632704 bytes, avg 54.249
Total footprint : = 1392720 bytes
Average bucket size : 0.194
Variance of bucket size : 0.208
Std. dev. of bucket size: 0.457
Maximum bucket size : 3
5.8 StringTable性能调优
- 调整-XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
6.直接内存
Direct Memory
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
进行磁盘读写时,java本身不提供这种功能,需要在本机内存中先读入系统缓冲区,然后读入java缓冲区byte[] 这样就相当与两次拷贝
NIO 会有一段直接内存,磁盘文件可以读取到直接内存,java代码也可以直接从直接内存中读取数据,少了一次缓冲区的赋值操作
6.3 直接内存溢出
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
垃圾回收
1.如何判断对象可以回收
1.1 引用计数法
当前对象被其他变量所引用,就让这个对象的计数+1,如果某一个变量不在引用它,就减一,减到0说明可以回收
弊端:循环引用,A、B对象互相引用
1.2可达性分析算法
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为GC Root?
MAT 可以查看
当前线程中,局部变量所引用的对象可以作为根对象
方法参数所引用的对象
1.3 四种引用
1. 强引用:
- 只有所有GC Roots对象都不通过[强引用]引用该对象,该对象才能被垃圾回收
2. 软引用:(SoftReference) - 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
- 可以配合引用队列来释放软引用自身
4. 弱引用:只要发生垃圾回收,会回收弱引用的对象 - 仅有软引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放软引用自身
5. 虚引用: - 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入列,由Reference Handler 线程调用虚引用相关方法释放直接内存
必须配合引用队列,
创建ByteBuffer实现类对象时,会创建一个Cleaner虚引用对象,ByteBuffer会分配一个直接内存,并且会把直接内存地址传递给虚引用对象,
当ByteBuffer被垃圾回收时,会将虚引用对象进入引用队列,而虚引用所在的引用队列会由一个ReferenceHandler的线程定时的在引用队列里找 Cleaner,如果有则调用Cleaner中的clean方法,
clean方法根据前面记录的直接内存的地址,调用Unsafe.freeMemory ,释放直接内存
- 终结器引用(FinalReference)
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
/**
* 演示软引用 当引用资源不是很重要、空间占用大时可以使用软引用
* 例如 图片资源 通过软引用引用对象,一次垃圾回收后,若空间仍然不 够,就会再次进行垃圾回收。
*
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ArrayList<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
// soft();
}
public static void soft(){
// list ----> SoftReference---->byte[]
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
当垃圾回收后软引用指向的对象部分为空,但上例中的list集合中会存储多个null值,这时软引用虽然无用但会占用部分空间,可以通过软引用队列遍历删除软引用
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
/* ArrayList<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();*/
soft();
}
public static void soft(){
// list ----> SoftReference---->byte[]
ArrayList<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],queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
//从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while (poll!=null){
list.remove(poll);
poll = queue.poll();
}
System.out.println("循环结束" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
一般都会在垃圾回收时把一些弱引用对象所占用的内存释放掉
弱引用对象自身需要释放资源也需要通过引用队列释放
/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --->WeakReference ----> byte[]
ArrayList<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.println(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束"+list.size());
}
1.4 标记清除
释放不是每个字节进行清理操作,只是把这个对象所占用内存的起始结束的地址记录下来,放在空闲的地址列表。下次分配新对象时,就会在地址列表中查找,有没有一块足够的空间容纳我的新对象,如果有则进行内存分配,并不会把对象所占用的内存进行清除处理
优点:标记清除只需记录起始结束地址,所以整个垃圾回收的速度快
缺点:会产生内存碎片 ,由于不会对空闲空间进行整理操作
当需要分配一块大的连续空间时 可能分配不下
1.5 标记整理
在清理垃圾的过程中,将可用的对象向前移动,让内存紧凑
优点:无内存碎片
缺点:速度慢,操作多 重新规划内存地址
1.6 复制算法
复制算法 将没被垃圾回收的对象放入To区,边放边整理
然后将FROM和TO区互换
优点:不会产生碎片
缺点:需要双倍的内存空间
1.7 分代垃圾回收
1.对象首先分配再伊甸园区域
2.新生代空间不足时,触发ninor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年两加1并且交换from to
3. minor gc 会引发 stop the world,暂停其它用户线程,等垃圾回收 结束,用户线程才恢复运行
4.当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
5.当老年代空间不足,会先触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长
2.1 相关VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx或 -XX:MaxHeapSize=size |
新生代大小 | -Xmn或(XX:NewSize=size + -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
3.垃圾回收器
3.1、串行
- 单线程
- 堆内存较小,时候个人电脑
–XX:+UseSerialGC=Serial+SerialOld
3.2、吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,STW的时间最短 0.2+0.2=0.4
1.8默认开启 并行垃圾回收器
多核cpu 垃圾回收时,多个垃圾回收线程同时进行垃圾回收
1.年轻代垃圾回收器~老年代垃圾回收器 (开启一个另一个随之开启)
2.(设置ParallelGC工作方式) 采用自适应大小调整策略 动态调整新生代
两个目标
3.比较智能,可以根据设定目标尝试调整堆的大小,达到期望的目标
调整垃圾回收时间和总时间的占比 (1/1+ratio) ratio默认99
4.默认200ms 与 上一条冲突 折中 —> ratio=19
5.控制运行时线程数
3.3、响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次STW的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
- 吞吐量没有并行高,由于有部分内核运行垃圾回收线程,占用cpu使用量
- 再并发清理的时候不能清理到刚产生的新垃圾(浮动垃圾)
1.同并行垃圾回收器 当年轻代和老年代垃圾回收碎片过多,无法存储对象时,会退化成SerialOld 串行垃圾回收器
2.并行垃圾回收线程数 默认和自己cpu 核数相同 设置为并行线程数的1/4
3.执行CMS垃圾回收的内存占比 (会预留一些空间给浮动垃圾)
4.重新标记之前,进行一次新生代垃圾回收 可以减轻重新标记的压力
CMS垃圾回收器 垃圾回收线程和用户线程并发执行
初始标识时仍需要STW 只标记根对象 很快 暂停时间短
线程并发执行,垃圾回收线程把剩余垃圾找出来 不用STW
重新标记需要STW 由于并发标记时 线程并发执行 工作时又可能产生新的对象改变对象的引用,可能对垃圾回收进行一些干扰 所以等到并发标记之后还需重新标记
CMS收集器的收集周期从一个称为初始标记的短暂停开始,该标记标识可从应用程序代码直接访问的活动对象的初始集。然后,在并发标记阶段,收集器标记从该集合可传递地到达的所有活动对象。因为应用程序在标记阶段运行并更新引用字段,所以不能保证在并发标记阶段结束时标记所有活动对象。为了处理这个问题,应用程序再次停止第二次暂停,称为remark,它通过重新访问在并发标记阶段修改的任何对象来完成标记。由于备注暂停比初始标记更重要,因此多个线程并行运行以提高其效率。
在remark阶段结束时,堆中的所有活动对象都保证已被标记,因此随后的并发扫描阶段将回收已标识的所有垃圾
3.4 GI
- 定义:Garbage First
- 2012JDK 7u4官方支持
- 2017 JDK9默认 取代了之前的CMS
适用场景
- 同时注重吞吐量和低延迟,默认的暂停目标时200ms
- 超大堆内存,会将堆分为多个大小相等的Region
- 整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
1)G1垃圾回收阶段
2)Young Collection
- 会STW
3)Young Collection + CM
- 在Young GC时会进行 GC Root 的初始标记
- 老年代占用堆空间比例达到阈值,进行并发标记(不户籍STW),有下面的JVM参数决定
-xx:InitiatingHeapOccupancyPercent=percent(默认45%)
4)Mixed Collection
会对E、S、O进行全面垃圾回收 - 最终标记(Remark)会STW
- 拷贝存活(Evacuation)会STW
-xx:MaxGCPauseMillis=ms
- SerialGC
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足发生的垃圾收集-full gc - ParallelGC
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足发生的垃圾收集-full gc - CMS
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足 - G1
新生代内存不足发生的垃圾收集-minor gc
老年代内存不足 (老年代空间占堆空间的45%触发 并发标记、混合收集)
若此时回收速度比新产生的垃圾快这时不叫full gc 处于并发垃圾收集
若产生垃圾的速度大于回收速度会退化full gc
6)Young Collection 跨代引用 - 新生代回收的跨代引用(老年代引用新生代)问题
7)Remark
- pre-write barrier + satb_mark_queue
当C的引用发生变化时,写屏障的指令就会执行,然后把C加入一个队列中,把C至与灰色 未处理完 ,接下来重新标记STW 暂停所有用户线程 把队列中所有的对象取出来再做一次检查
8)JDK 8u20字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了cpu时间,新生代回收时间略微增加
-xx:+UseStringDeduplication
String s1 = new String("hello");
String s1 = new String("hello");
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果他们值一样,让它们引用同一个char[]
- 注意,与String.intern()不一样
- String.intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
9)JDK 8u40并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再使用,当一个类加载器的所有类都不在使用,则卸载他所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
10)JDK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
11)JDK9并发标记起始时间的调整
- 并发标记必须在堆空间沾满前完成,否则退化为FullGC
- JDK9之前需要使用-xx:InitiatingHeapOccupancyPercent
- JDK9可以动态调整
- -xx:InitiatingHeapOccupancyPercent用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空当时间
12)JDK9更高效的回收
- 250+增强
- 180+bug修复
4. 垃圾回收调优
4.1调优领域
- 内存
- 锁竞争
- cpu占用
- io
4.2确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS,G1,ZGC
- ParallelGC
- Zing虚拟机
4.3 最快的GC时不发生GC
- 查看FullGC前后的内存占用,考虑下面几个问题
- 数据是不是太多?
- resultSet = statement.executeQuery("select * from 大表 ")
- 数据表示是否太臃肿?
- 对象图
- 对象大小
- 是否存在内存泄漏
- static Map map =
- 软弱引用
- 第三方放缓存实现
- 数据是不是太多?
4.4 新生代调优
- 新生代的特点
- 所有的new操作的内存分配非常廉价
*TLAB threal-local allocation buffer - 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
- 所有的new操作的内存分配非常廉价
- 新生代能容纳所有【并发量*(请求-响应)】的数据
- 幸存区打到能保留 【当前活跃对象+需要晋级对象】
- 晋升阈值配置得当,让长时间存活对象尽快晋升
- -XX:MaxTenuingThreshold=threshold
- -XX:+PrintTenuringDistribution
4.5 老年代调优
以CMS为例
- CMS的老年代内存越大越好
- 先不做调优,如果没有Full GC 那么已经… 否则先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
- -XX:CMSInitiatingOccupancyFraction=percent .
4.6 案例
- 案例1 Full GC 和 Minor GC频繁
- 案例2 请求高峰期发生Full GC,单次暂停时间特别长(CMS)
- 案例3 老年代充裕情况下,发生Full GC (CMS jdk1.7)