03-JVM-垃圾回收算法
1、JVM内存分配与回收
1.1 对象优先在Eden区进行分配
堆中存储的对象,大多数情况下优先存储在Eden区,当Eden区存满没有足够的空间的时候,虚拟机将进行一次minorGC。当满足一定条件以后,就会进行FullGC。垃圾回收分为minorGC和fullGC两种,下面来看一下这两者的差别:
- Minor GC/Young GC:发生在年轻代的垃圾回收,频率比较高,回收速度也比较快
- Full GC/Major GC:一般回收老年代,年轻代,方法区的垃圾,Full GC的速度一般比Young GC慢10倍以上。
下面我们来看一段代码:(添加JVM运行参数:-XX:+PrintGCDetails)
情况一:
public class GCTest {
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
// allocation1=new byte[1000*1024];
//allocation2=new byte[80000*1024];
// allocation3=new byte [1000*1024];
} 运行结果:
Heap
PSYoungGen total 35840K, used 4301K [0x00000000d8380000, 0x00000000dab80000, 0x0000000100000000)
eden space 30720K, 14% used [0x00000000d8380000,0x00000000d87b3720,0x00000000da180000)
from space 5120K, 0% used [0x00000000da680000,0x00000000da680000,0x00000000dab80000)
to space 5120K, 0% used [0x00000000da180000,0x00000000da180000,0x00000000da680000)
ParOldGen total 81920K, used 0K [0x0000000088a00000, 0x000000008da00000, 0x00000000d8380000)
object space 81920K, 0% used [0x0000000088a00000,0x0000000088a00000,0x000000008da00000)
Metaspace used 3510K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
情况二:
public class GCTest { public static void main(String[] args) { byte[] allocation1,allocation2,allocation3;
allocation1=new byte[9000*1024];
allocation2=new byte[18000*1024];
} 运行结果: Heap PSYoungGen total 35840K, used 30720K [0x00000000d8380000, 0x00000000dab80000, 0x0000000100000000)
eden space 30720K, 100% used [0x00000000d8380000,0x00000000da180000,0x00000000da180000)
from space 5120K, 0% used [0x00000000da680000,0x00000000da680000,0x00000000dab80000)
to space 5120K, 0% used [0x00000000da180000,0x00000000da180000,0x00000000da680000)
ParOldGen total 81920K, used 0K [0x0000000088a00000, 0x000000008da00000, 0x00000000d8380000)
object space 81920K, 0% used [0x0000000088a00000,0x0000000088a00000,0x000000008da00000)
Metaspace used 3510K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
情况三:
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1=new byte[9000*1024];
allocation2=new byte[18000*1024];
allocation3=new byte [1000*1024];
}
运行结果:
[GC (Allocation Failure) [PSYoungGen: 30687K->936K(35840K)] 30687K->27944K(117760K), 0.0087232 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 35840K, used 2550K [0x00000000d8380000, 0x00000000dc980000, 0x0000000100000000)
eden space 30720K, 5% used [0x00000000d8380000,0x00000000d8513b40,0x00000000da180000)
from space 5120K, 18% used [0x00000000da180000,0x00000000da26a020,0x00000000da680000)
to space 5120K, 0% used [0x00000000dc480000,0x00000000dc480000,0x00000000dc980000)
ParOldGen total 81920K, used 27008K [0x0000000088a00000, 0x000000008da00000, 0x00000000d8380000)
object space 81920K, 32% used [0x0000000088a00000,0x000000008a460020,0x000000008da00000)
Metaspace used 3492K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 385K, capacity 388K, committed 512K, reserved 1048576K
从以上代码可以看出,第一个代码什么都没有执行的时候eden区域已经使用了14%的内存。当我们执行第二段代码的时候,先给变量分配到eden区去了。当我们执行第三段代码的时候发现,老年代区域也已经使用了32%了。从情况2到情况3的原因是因为,在情况二的时候eden区域已经到了100%,当在情况三的时候放开allocation3放出,此时eden区域已经满了,会触发一次minorGC,将allocation3放入survior区域,但是发现survivor区域放不下,于是需要提前把年轻代的移入到老年代,老年代可以放下部分allocation1,所以不会触发fullGC.如果对象能够存在eden区的话,还是会在eden区域分配内存。
以下代码就可以验证:
public class GCTest {
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1=new byte[9000*1024];
allocation2=new byte[18000*1024];
allocation3=new byte [1000*1024];
byte[] allocation4=new byte[2000*1024];
} 运行结果:
[GC (Allocation Failure) [PSYoungGen: 30687K->936K(35840K)] 30687K->27944K(117760K), 0.0091469 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 35840K, used 4839K [0x00000000d8380000, 0x00000000dc980000, 0x0000000100000000)
eden space 30720K, 12% used [0x00000000d8380000,0x00000000d874fe20,0x00000000da180000)
from space 5120K, 18% used [0x00000000da180000,0x00000000da26a020,0x00000000da680000)
to space 5120K, 0% used [0x00000000dc480000,0x00000000dc480000,0x00000000dc980000)
ParOldGen total 81920K, used 27008K [0x0000000088a00000, 0x000000008da00000, 0x00000000d8380000)
object space 81920K, 32% used [0x0000000088a00000,0x000000008a460020,0x000000008da00000)
Metaspace used 3502K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
1.2大对象直接进入老年代
大对象就是说需要大量连续内存空间的对象。设置大对象大小的JVM参数如下:-XX:PretenureSizeThreshold,对象超过设置的该值就会直接进入老年代。这样做的原因是为了避免大对象分配内存的时候因为复制操作而降低效率。
package com.jvm.jvmCourse3; public class GCTest {
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1=new byte[9000*1024];
//allocation2=new byte[180000*1024];
//allocation3=new byte [1000*1024];
// byte[] allocation4=new byte[2000*1024];
}
}
//运行结果:
Heap
PSYoungGen total 35840K, used 13301K [0x00000000d8380000, 0x00000000dab80000, 0x0000000100000000)
eden space 30720K, 43% used [0x00000000d8380000,0x00000000d907d730,0x00000000da180000)
from space 5120K, 0% used [0x00000000da680000,0x00000000da680000,0x00000000dab80000)
to space 5120K, 0% used [0x00000000da180000,0x00000000da180000,0x00000000da680000)
ParOldGen total 81920K, used 0K [0x0000000088a00000, 0x000000008da00000, 0x00000000d8380000)
object space 81920K, 0% used [0x0000000088a00000,0x0000000088a00000,0x000000008da00000)
Metaspace used 3504K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K //放开allocation2,可以看到直接放到了老年代
package com.jvm.jvmCourse3; public class GCTest {
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1=new byte[9000*1024];
allocation2=new byte[180000*1024];
//allocation3=new byte [1000*1024];
// byte[] allocation4=new byte[2000*1024];
}
} //运行结果如下:
Heap
PSYoungGen total 35840K, used 13301K [0x00000000d8380000, 0x00000000dab80000, 0x0000000100000000)
eden space 30720K, 43% used [0x00000000d8380000,0x00000000d907d730,0x00000000da180000)
from space 5120K, 0% used [0x00000000da680000,0x00000000da680000,0x00000000dab80000)
to space 5120K, 0% used [0x00000000da180000,0x00000000da180000,0x00000000da680000)
ParOldGen total 262144K, used 180000K [0x0000000088a00000, 0x0000000098a00000, 0x00000000d8380000)
object space 262144K, 68% used [0x0000000088a00000,0x00000000939c8010,0x0000000098a00000)
Metaspace used 3510K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K
1.3长期存活的对象进入老年代
JVM对参数进行分代管理,就需要区分哪些对象需要放在年轻代,哪些对象需要放在老年代,为了区分,JVM给对象设置了一个参数age。当对象在eden区域出生,经过一次minorGC后,能够在survivor区域存活,将年龄设置为1,age=1。对象在Survivor区域中每经历一次minorGC年龄就会增长1(+1),当达到一定年龄的时候(默认是15)就会将对象移动到老年代中去。法定年龄的设置参数如下:
1.4对象动态年龄判断
当前对象的Survivor区域里,有一批对象的总大小大于Survivor区域的50%,那么大于该批对象中年龄最大的的对象就可以直接进入老年代了。比如在Survivor区域中,age8+age9+age10+agen的一批对象大小的和大于Survivor区域的50%,那么age>n的对象就可以进入老年代了。这样做是为了让可能长期存活的对象尽早的进入老年代。对象动态年龄判断主要是发生minorGC之后。
1.5minorGC后存活的对象在Survivor区域存放不下
在本文刚开始的情况三种已经有这样的案例。在这种情况下可能会把存活对象的部分移动到老年代,部分留在Survivor区域。
1.6老年代空间分配担保机制
在进行minor gc之前,JVM都会计算一下老年代的剩余空间是否小于年轻代里所有对象的大小(包括垃圾对象)的和,如果小于,就会看参数“-XX:-HandlePromotionFailure”是否进行了设置,如果设置了,就会查看老年代的可用内存是否小于每一次minor gc后进入老年代的对象的平均大小,如果参数没有设置或者老年代可用空间小于每次minor gc后进入老年代对象的平均大小,那么就会触发 full gc,对老年代年轻代一起进行回收,如果回收完还是没有足够的空间存放新对象就会产生OOM.具体的流程如下图
1.7 Eden与Survivor区默认8:1:1
默认情况下Eden区域与Survivor区域的默认比例是8:1:1。新增对象存储到堆的时候先分配到Eden区域,当Eden区域满的时候就会触发MinorGC,MinorGC在进行回收的时候会将还存活的对象放到Survivor区域去,基本上MinorGC能清除99%的垃圾,因此Survivor区域够用即可,Eden区域尽量的大。
JVM默认有这个参数-XX:+UseAdaptiveSizePolicy,会导致这个比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy。
以上了解到了JVM内存区域的分配机制,下面我们来看一下如何判断对象可以被回收了。
2、怎么判断对象可以被回收
2.1引用计数法
顾名思义就是给对象添加一个引用计数器,当有地方引用的时候就+1,不引用了就-1,当引用计数器为0的时候就可以回收该对象了。该方法简单高效,但是主流的虚拟机并没有选择这样的方法来管理内存,最主要的原因就是它很难解决对象之间相互循环引用的问题。如下代码所示
public class ReferenctToMe {
Object tome=null; public static void main(String[] args) {
ReferenctToMe a=new ReferenctToMe();
ReferenctToMe b=new ReferenctToMe();
a.tome=b;
b.tome=a;
}
}
对象啊a和b相互引用,除此之外再无关联,都为彼此引用,所以引用计数器都不为0,因此垃圾回收器无法对其进行回收。
2.2可达性分析算法
通过一系列的"GC ROOT"的对象作为起点,从这个节点开始向下搜索,能找到的对象就是非垃圾对象,其余的未标记的对象就是垃圾对象可以进行回收。
GC Root 根节点:线程栈的局部变量,静态变量,本地方法栈的变量等等。
2.3常见引用类型
java的引用一般分为强引用、软引用,弱引用,虚引用。
强引用:一般普通变量的引用 如:private static User user=new User();
软引用:用SoftReference软引用类型包裹的对象,正常情况下不会被回收,但是在GC完成以后发现没有新的内存空间存放对象的时候,就会把软引用回收掉,如:
SoftReference<User> user=new SoftReference<>(new User());软引用通常用来实现内存敏感的高速缓存。
2.4 finalize()方法最终判定对象是否存活
在可达性分析算法中,没有找到GC Root根的不可达对象,也不一定是非死不可,此时它们处于死缓的状态,要判定对象死刑的话,至少要经历再次标记。标记的前提是对象在进行可达性分析后发现没有与GC Root相连接的引用链。
1、第一次标记并进行一次筛选
筛选的时候是看该对象是否有必要执行finalize()方法,当对象没有覆盖finalize方法,对象将被直接回收。
2、第二次标记
如果这个对象覆盖了finalize方法,该方法就是这个对象死里逃生的唯一机会。要成功的利用finalize()方法完成死里逃生的话,只要重新与引用链上的任何一个对象建立关联就可以了,比如把自己赋值给某个类变量或者对象的成员变量,那么在第二次标记的时候将它移出“垃圾回收”集合。如果这个对象没有利用finalize()成功的话就只能被回收了。
package com.jvm; import com.jvm.jvmCourse3.OOMTest; public class User { private int id;
private String name; byte[] a = new byte[1024*100]; public User(){} public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
protected void finalize(){
//OOMTest.list.add(this);
System.out.println("资源关闭,user "+id+"即将被回收");
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
} } package com.jvm.jvmCourse3; import com.jvm.User; import java.util.ArrayList;
import java.util.List;
import java.util.UUID; public class OOMTest {
//public static List<Object> list=new ArrayList<>();
public static void main(String[] args) {
List<Object> list=new ArrayList<>();
int i=0;
int j=0;
while(true){
list.add(new User(i++, UUID.randomUUID().toString()));
new User(j--,UUID.randomUUID().toString());
}
}
} 运行结果:
资源关闭,user -15169即将被回收
资源关闭,user -15171即将被回收
资源关闭,user -15170即将被回收
资源关闭,user -15172即将被回收
资源关闭,user -15173即将被回收
资源关闭,user -15174即将被回收
资源关闭,user -15175即将被回收
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.jvm.User.<init>(User.java:8)
at com.jvm.jvmCourse3.OOMTest.main(OOMTest.java:15)
根据上图的运行结果可以看到user id 都是负数,正数的都是被引用了,放在list里面,但是凭空new出来的游离的对象没有GC Root根,并且没有覆盖finalize()方法因此第一次标记的时候就会被回收。要让j--的对象如何实现自我拯救呢?将上述代码中的红色部分解除注释即可。
2.5如何判断一个类是无用的类
在方法区主要回收的就是无用的类,那么如何判断一个类是无用的类呢?判断一个类是无用的类需要同时满足一下3个条件才能算无用的类:
- 该类所有的实例都已经被回收,也就是java堆中不存在任何该类的实例。
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3、垃圾收集算法
3.1标记清除算法
顾名思义就是分为两个阶段“标记”和“清除”两个阶段,先标记需要回收的对象,标记完成后,统一对标记的对象进行清除。是最基础的收集算法,但是存在两个明显的问题。
- 效率问题(效率不是很高,跟复制算法比较)
- 效率问题(会产生大量的空间碎片)
3.2复制算法
为了提高效率,出现了复制算法。它是将内存分为大小相同的两块,每次使用其中一块,当使用的那一块内存满了时候,就将还存活的对象移动到未使用的那一块内存中去,然后对满了的这块内存整体清除,这样就是每次都对内存的一半进行回收。具体如下图:
3.3 标记整理算法
标记整理算法的标记过程与“标记-清除”算法一致,只是后续清除的时候不是像“标记-清除”算法一样直接清除,而是将存活对象向一端移动,然后清除端边界以外的内存,如下图所示:
3.4分代收集算法
分代收集算法就是根据对象存活周期不同将内存分为几块。一般把java堆内存分为新生代和老年代,这样就可以根据每个代的特点选择不同的垃圾收集算法。年轻代中每次收集都有大量的对象(99%)死亡,可以选择复制收集算法,付出少量的对象复制成本就可以完成每次垃圾回收,但是在老年代的对象存活得比较久,而且没有额外的空间对其进行分配担保,所以必须选择“标记-清除” 或者“标记-整理”算法进行垃圾回收。“标记-清除” 或者“标记-整理”算法 比“复制算法”慢10倍以上。
03-JVM-垃圾回收算法的更多相关文章
- JVM垃圾回收算法解析
JVM垃圾回收算法解析 标记-清除算法 该算法为最基础的算法.它分为标记和清除两个阶段,首先标记出需要回收的对象,在标记结束后,统一回收.该算法存在两个问题:一是效率问题,标记和清除过程效率都不太高, ...
- JVM垃圾回收算法(最全)
JVM垃圾回收算法(最全) 下面是JVM虚拟机运行时的内存模型: 1.方法区 Perm(永久代.非堆) 2.虚拟机栈 3.本地方法栈 (Native方法) 4.堆 5.程序计数器 1 首先的问题是:j ...
- JVM垃圾回收算法及回收器详解
引言 本文主要讲述JVM中几种常见的垃圾回收算法和相关的垃圾回收器,以及常见的和GC相关的性能调优参数. GC Roots 我们先来了解一下在Java中是如何判断一个对象的生死的,有些语言比如Pyth ...
- JVM 垃圾回收算法和垃圾回收器
JVM 垃圾回收算法和垃圾回收器. 一.垃圾回收的区域 栈:栈中的生命周期是跟随线程,所以一般不需要关注. 堆:堆中的对象是垃圾回收的重点. 方法区:这一块也会发生垃圾回收,不过这块的效率比较低,一般 ...
- 记录JVM垃圾回收算法
垃圾回收算法可以分为三类,都基于标记-清除(复制)算法: Serial算法(单线程) 并行算法 并发算法 JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代 ...
- JVM 垃圾回收算法
在说垃圾回收算法之前,先谈谈JVM怎样确定哪些对象是“垃圾”. 1.引用计数器算法: 引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当 ...
- Java基础:JVM垃圾回收算法
众所周知,Java的垃圾回收是不需要程序员去手动操控的,而是由JVM去完成.本文介绍JVM进行垃圾回收的各种算法. 1. 如何确定某个对象是垃圾 1.1. 引用计数法 1.2. 可达性分析 2. 典型 ...
- JVM 垃圾回收算法及案例分析
一. 在说垃圾回收算法之前,先谈谈JVM怎样确定哪些对象是“垃圾”. 1.引用计数器算法: 引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器- ...
- 第四章 JVM垃圾回收算法
说明:在阅读本篇之前,需要知道怎么判断对象的存活与否,见<第三章 JVM内存回收区域+对象存活的判断+引用类型+垃圾回收线程> 注意:本文主要参考自<分布式Java应用:基础与实践& ...
- JVM垃圾回收算法 及 垃圾收集器
摘自<深入理解Java虚拟机> 一.什么是: GC算法是 方法论,那么垃圾收集器就是具体的 实现. 二.四种 垃圾回收算法 1.标记-清除算法:最基础的收集算法:不足有两点:1标记和清除两 ...
随机推荐
- NodeJS2-3环境&调试----module.exports与exports的区别
exports默认会给他设置为module.exports的快捷方式,可以把它的里面添加属性,但是我们不能修改它的指向,如果修改了它的指向那它和普通对象没有任何区别了.因为在CommonJS中,模块对 ...
- 《Java基础知识》Java异常处理详解
1. Java 中的异常 前言:Java 中的异常处理是处理程序运行错误时的强大机制之一,它可以保证应用程序的正常流程. 首先我们将了解java异常.异常的类型以及受查和非受查异常之间的区别. 1.1 ...
- android开发检测用户是否使用了虚拟定位
在应用开发中,如果有签到打卡之类的功能,你是否会遇到检测用户是否使用了虚拟定位软件来进行打卡?如果有,那么请仔细阅读这篇文章.该文章会带你认识什么是虚拟定位.什么是应用分身,以及如何通过代码来检测用户 ...
- Bash脚本编程之数组
数组简介 在bash脚本编程当中,变量是存储单个元素的内存空间:而数组是存储多个元素的一段连续的内存空间. 数组由数组名和下标构成,如下. ARRAY_NAME[SUBSCRIPT] 数组按照下标的类 ...
- IT兄弟连 HTML5教程 CSS3属性特效 自定义文字
字体使用是网页设计中不可或缺的一部分.经常地,我们希望在网页中使用某一特定字体,但是该字体并非主流操作系统的内置字体,这样用户在浏览页面的时候就有可能看不到真实的设计.美工设计师最常做的办法是把想要的 ...
- Pick of the Week'19 | Nebula 第 45 周看点--Nebula 到底是不是原生存储?
每周五 Nebula 为你播报每周看点,每周看点由本周大事件.用户问答.Nebula 产品动态和推荐阅读构成. 今天是 2019 年第 45 个工作周的周五,来和 Nebula 看看本周有什么图数据库 ...
- asp.net 页面中添加普通视频的几种方式
第一种 是通过调用window media player进行播放诸如:wmv,asf等格式文件: <object align=center class="OBJECT" cl ...
- 线程提供的方法:static void sleep(long ms),会进入阻塞状态,休眠
package seday08.thread; import java.util.Scanner; /*** @author xingsir * 线程提供的方法:static void sleep(l ...
- ES-索引管理
参考: https://es.xiaoleilu.com/070_Index_Mgmt/00_Intro.html 创建索引 PUT /new_index 创建更多详细设置的索引: 删除索引 DELT ...
- How to: Map a Persistent Class to a Database View Which Has No Key Field如何:映射持久化类到无主键数据库视图
With XAF, you can build new applications from scratch or maintain existing databases. The How to: Ge ...