垃圾回收机制
文章目录
1.垃圾回收机制主要回收的内存区域
垃圾回收会涉及到两个区域:堆(Heap)和方法区
1.堆
垃圾收集器最主要回收的是对象,也就回收堆区域的内容,堆内存是垃圾回收机制中占比最大的一块,为了高效的进行垃圾回收器回收,会把堆内分为好几个子部分。在堆中,进行一次GC一般回收70%~95%的空间。
2.方法区
方法区中储存着对象所对应的类的信息,比如类的结构信息、常量池信息、方法信息等等。GC回收的是无用的常量信息和无用类。这是GC非主要工作的区域,因为要回收方法区的类的信息,条件非常苛刻。并且对方法区进行一次GC的性价比并不高。也正因为如此,JVM规范,并没有强烈要求垃圾回收器必须要实现对方法区的回收。
2.何为 “垃圾”
2.1何为堆中的垃圾
判断是否为垃圾有两种方式:引用计数法(Reference Counting)和根搜索法(Root Tracing)
1. 引用计数法(Referance Counting)
每一个对象都有一个引用计数器,当存在对该对象的引用时,引用计数器加一,每当一个引用失效,引用计数器减一,当引用计数器为0时,说明该对象不在其他地方被引用,变成了可以被回收的垃圾。
但是引用计数法无法解决循环引用的问题
假如A类对像中有B类对像的应用,B类对像中有A类对像的引用,假设其他地方的引用都已经失效,只剩下AB两对像间的循环引用,这样两个对像中的引用计数器的值都为1 ,所以无法被判定为垃圾
2.根搜索法(Root Tracing)
在实际的生产语言中(Java、 C#),都是使用跟搜索算法判定对像是否存活。
算法的基本思想是通过一系列称为“GC Roots”的点作为起始进行向下搜索。当一个对象到GC Roots没有任何引用链相连,则说明该对象是不可用的
2.2 何为方法区中的垃圾
对方法区中的信息进行GC的条件:
- 堆空间中没有对该类的任何对象,或者说该类的对象都已经被回收
- 加载该类的类加载器已经被回收。因为类的加载器和类对象(Class<?>)是一种双向引用的关系。
- 该类的Class<?>对象没在在任何地方被引用(比如没有对该类的反射机制操作)
可见回收条件还是很严格的
2.3 被标记的对象一定会死亡吗
在根搜索法(可达性分析)中不可达的对象也不一定非死不可,被标记后,该对象处于“缓死”阶段,要想想一个对象真正的死亡,需要经过两次标记过程。如果在可达性分析中如果发现一个对象与GC Roots中的引用链不相连,那么会对这个已经标记过一次的对象进行一次筛选,如果该对象没有覆盖fianlize()方法或者该方法已经被虚拟机执行。虚拟机就不会执行该方法。
当这个对象可以有资格执行finalize()方法时,该对象会被加入到一个F-Queue队列中,随后虚拟机会自动的家里一个叫做fianlizer的低优先级线程来执行队列中的对象的finalize()方法。这里的执行,指的是虚拟机会触发这个方法,但是不会保证会等待该方法会被执行结束。因为finalize()方法执行的时间过长,将很有可能导致F-Queue队列中的其他对象处于等待状态。finalize()方法是该对象逃离死亡的唯一机会,稍后会对队列中的对象实现二次标记,这时候如果对象没有逃离成功,它就真的死亡了。
我们举一个例子
package com.mec.aboutGC;
public class MyTest4 {
public static MyTest4 myTest4 = null;
public void isAlived() {
System.out.println("i am alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("MyTest4 is already Created");
myTest4 = this;
}
public static void main(String[] args) throws Exception{
myTest4 = new MyTest4();
myTest4 = null;//将该对象赋值为null,让垃圾回收器回收
System.gc();
Thread.sleep(500);//以为finalizer的线程优先级很低,所以延时0.5秒
if (myTest4 != null) {
myTest4.isAlived();
} else {
System.out.println("i am dead");
}
//下面的操作和上面的一样,但是这次并没有逃脱死亡的命运
myTest4 = null;
System.gc();
Thread.sleep(500);
if (myTest4 != null) {
myTest4.isAlived();
} else {
System.out.println("i am dead");
}
}
}
执行结果
从执行结果我们可以知道,第一次回收时,Mytest4对象成功逃脱了死亡。
3.垃圾回收算法
3.1 标记-清除法(Mark-Sweep)
总体来说,这种算法的效率并不高,因为每一次GC都要扫描所有的对象,进行判断、标记、清除。而且随着GC的执行次数的增加,内存碎片会越来越多。
3.2 标记-整理法(Mark-Compact)
标记整理法的标记过程和标记清除法是一样的,但不是标记了马上清除,而是先将存活的对象整理到内存的一端,然后将这一端以外的对象全部清除。
优点
没有内存碎片
缺点
要花费更多的时间来进行整理
3.3 复制算法(Copying)
复制算法的优缺点
优点:不会产生内存碎片。非常适合回收生命周期短的对象,因为每一次GC都会回收大量的对象。
缺点:会浪费一定的空间,当碎片存活率高的时候,这种算法的效率就会下降,如果不想浪费50%的空间,万一出现100%存活的极端情况。就不能复制了,所以这也是这种算法用于回收新生代,而不用于老年代的原因。
根据IBM的专门研究,有98%的对象只会存活一个GC周期,对于这些对象,就很适合使用复制算法,因为这样不用1:1的去划分工作区与复制区
3.4 分代算法(Generational)
当前商业虚拟机的垃圾收集器都采用这种分代算法,是因为这种算法经过了长期的考验,得到了大家的认可。
分代算法将堆空间分为新生代和老年代,对于不同的代的不同的特点,采用不同的垃圾回收算法,这样大大提高了垃圾回收效率
新生代被细分为1个eden区,两个survivor区,新生代的回收用的是复制算法,当endn区满后,会将ende区中的存活的对象放入到一个survivor区,并且清空end区,当这个survivor区满后,会将其中存活的对象放入到另一个servivor区中,并且清空前一个survivor区。当现在这个区也满时,将其存活的对像放入到老年代中。
Full GC在执行时,会将用户的工作线程暂时挂起(STW),用户的业务线程不工作对于服务器之类访问量大的程序来说是很可怕的,所以要尽量减少Full GC的使用。
4.垃圾回收器
垃圾回收器的目的是内存回收,内存回收就是垃圾收集者将那些dead的对象所占用的内存回收掉。
HotSpot虚拟机认为没有引用的对象是dead的
HotSpot将引用分为四种:Strong、 Soft 、Weak、 Phantom。
Strong就是 Object0 = new Object();这种方式的引用。
Soft、Weak、Phantom这三种则是继承Reference
垃圾回收器是GC算法的具体体现,其实并没有最好的垃圾回收算法只右最合适的垃圾回收算法,在适合的场景用适合的垃圾回收器
4.1 Serial收集器
Serial收集器是单线程收集器,收集时会暂停所有的工作线程(Stop the World)STW,使用的是复制收集算法,在虚拟机运行在Client模式时的默认新生代收集器。
Serial收集器是最早的垃圾,不论是新生代还是老年代都可以使用,新生代用的是复制算法,老年代用的是标记-整理算法,因为是单线程的垃圾回收器,所以不存在线程切换的问题,相对来说比较简单。
4.2 ParNew收集器
ParNew收集器算是Serial收集器的多线程版本,在虚拟机工作在Server模式下,ParNew收集器用于新生代的收集。如果你的电脑是单核的,那么该收集器与Serial收集器无异。只有在多CPU的情况下,PerNew收集器的效率才会体现出来。ParNew收集器相对于是Serial收集器对于新生代的多线程版本,也就是说ParNew收集器是一个针对新生代的收集器,它采用的也是复制算法。该收集器可以通过-XX:ParallelGCThreads来控制GC的线程数多少。
4.3 Parallel Scavenge收集器
Parallel Scavenge是收集器是一个多线程的收集器,它也是采用的是复制算法,但是在回收策略方面,与ParNew收集器有所不同,该收集器的特点是追求GC吞吐量最大化(GC的时间尽可能短),所以该收集器允许STW。
4.4 Serial Old收集器
Serrial Old收集器是一个单线程的的收集器,使用的是标记-整理法,他是一个老年代的收集器
4.5 Parallel Old收集器
老年代的吞吐量最优先收集器,它采用了多线程,采用的是标记-整理法。
4.6 CMS(Concurrent Mark Sweep)收集器
CMS收集器是一种以最短时间停顿为目标的收集器,CMS并不能达到效率最高,但是它尽可能的降低GC时服务的停顿时间,CMS收集器使用的是标记-清除法。它只针对老年代,经常与ParNew收集器连用,该收集器看可以做到GC线程与用户工作线程并发执行。
CMS收集器也有着很多缺点:
CMS收集器需要更多的CPU资源,因为是和用户线程并发执行,一般并发执行都需要占用CPU资源,CMS默认的垃圾回收线程数为 (CPU数量 + 3)/ 4, 当CPU数量为4时,垃圾回收线程就占用的25%的线程。当你的CPU数量少于4时,有可能对CMS收集器的吞吐量造成很大影响。
他采用的是标记-清除法,所以随着程序的长时间运行,会产生大量的内存碎片,当内存碎片过多时就有可能频繁的触发Full GC。
CMS收集器整个收集分为4个步骤:
- 初始标记:仅仅标记一下GC Roots可以联系到的对象,速度很快,会造成STW
- 并发标记:判断GCRoots中对象是否仍在使用中,可以用GC Roots Tracing还判别,并发标记会和用户的应用线程一起执行
- 重新标记:这个过程会造成STW不会和用户程序一起执行,它的目的是修改在并发标记中用户程序导致的对象标记的变化,比如,在初始阶段还存在的对象,在并发标记期间死亡了,就要在重复标记期间,对标记进行修改,重复标记的需要花费的时间要比初始标记要长,但是要比并发标记要短。
- 并发清除:不会造成STW,与用户程序共同执行,使用标记清除法将死去的对象的空间释放。
因为最花费时间的并发标记和并发清除可以和用户的线程一同执行,所以CMS收集器是以GC停顿时间最少为目的的收集器
5.垃圾收集器的并行与并发
并行(Paralell):指多个收集器可以同时进行,但是所有的工作线程需要等待
并发(Concurrent):指收集器在工作的同时,我们的工作线程也可以执行,但是这只能说是优化了GC停顿的问题,并没有解决,再一些关键的步骤,比如对dead对象进行标记时,工作线程还是需要等待的。
6.Java内存泄漏的经典原因
- 对象定义在错误的范围
/**
* 如果MyTest1类的生命周期过长,就会出现临时内存泄漏的问题
* 因为当执行完doSomenThing方法时,args就会被实例化,但是当这个方法执行完,再也没有用到
* 而args对象并不能被回收,因为MyTest1对象还会存在对它的引用,只有当MyTest1对象dead时
* args才能被真正的回收
*
*/
public class MyTest1 {
private String[] args;
public void doSomeThing(int length) {
if(length < 1) {
return;
}
args = new String[length];
args[0] = "Hello World";
}
}
如果换成以下写法,程序的逻辑并不会改变,而且不会发生临时内存泄漏的问题。
public class MyTest1 {
public void doSomeThing(int length) {
` String[] args;
if(length < 1) {
return;
}
args = new String[length];
args[0] = "Hello World";
}
}
这个例子告诫我们该是方法的局部变量就不要定义到一个类中作为一个成员,否则会造成临时内存泄漏的问题。
- 对异常的处理不当
public class MyTest2 {
public static void main(String[] args) {
DataInputStream dis;
DataOutputStream dos;
ServerSocket server;
try {
server = new ServerSocket(54343);
Socket socket = server.accept();
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF("Hello World!");
dis.close();
dos.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
如果在执行dos.writeUTF(“Hello World!”);时出现异常了。那么dis、dos、server都将不能得到正常的释放,造成了内存泄漏,所以关于资源释放的代码还是应该写到finally中。
- 集合数据管理不当
7.阅读GC日志
先写一个示例程序
package com.mec.memory;
public class MyTest6 {
public static void main(String[] args) {
int M = 1 << 20;
int K = 1 << 10;
byte[] bytes1 = new byte[2 * M];
byte[] bytes2 = new byte[2700 * K];
byte[] bytes3 = new byte[3 * M];
System.out.println("hello world!");
}
}
运行的参数
参数 | 作用 |
---|---|
-verbose:gc | 在控制台输出gc的情况 |
-Xms | 堆空间的起始大小 |
-Xmx | 堆空间的最大大小 |
-Xmn | 新生代的大小 |
-XX:+PrintGCDetails | 在控制台输出GC的详细信息 |
-XX:SeurvivorRatio | 调整eden区占用整个新生代的比率 |
程序的运行结果:
[GC (Allocation Failure) [PSYoungGen: 5734K->752K(9216K)] 5734K->5508K(19456K), 0.0034003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
hello world!
Heap
PSYoungGen total 9216K, used 4065K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 40% used [0x00000000ff600000,0x00000000ff93c5e0,0x00000000ffe00000)
from space 1024K, 73% used [0x00000000ffe00000,0x00000000ffebc040,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4756K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 46% used [0x00000000fec00000,0x00000000ff0a5020,0x00000000ff600000)
Metaspace used 2635K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
这就是GC日志的内容,接下来我们分析一下。
先分析这部分内容:
[GC (Allocation Failure) [PSYoungGen: 5734K->752K(9216K)] 5734K->5508K(19456K), 0.0034003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-
日志的开头 [ GC : 这个代表GC的停顿方式(调用时机)是Scavenge GC(Minor GC),除此之外还有 Full GC。
-
(Allocation Failure): 指的是触发这次GC的原因,这次gc的原因是分配失败。
-
[PSYoungGen: 5734K->752K(9216K)] : PSYoungGen☞的是两部分内容 PS + YoungGen,YoungGen代表着新生代,PS代表新生代用的垃圾收集器是 Parallel Scavenge。后面的数字5734K->752K(9216K),5734K
代表回收前新生代中使用的空间大小,752K代表着回收后的大小,他们的差值有两部分,一部分是真正被回收的空间,另一部分是离开了新生代到达老年代。9216K也就是9M,代表着新生代的大小。但是我们配置的是10M大小,因为该垃圾收集器使用的是复制算法,有一个survior空间是不用的,而我们设置的比例是eden:to survivor : from survivor = 8 : 1 : 1,所以说有1M空间是不用的,所以新生代的大小是9M。 -
5734K->5508K(19456K): 5734K代表整个堆空间中未回收前已使用的空间大小,5508K代表堆空间回收后已使用的大小,19456K代表着堆空间的总大,19M比配置的20M少1M,因为新生代中有1个servivor空间是不用的。
-
**0.0034003 secs:**表示本次垃圾收集用了0.0034003 secs秒。
-
**[Times: user=0.00 sys=0.00, real=0.00 secs] : ** 有的收集器会给出更具体的事件,user:使用者消耗的CPU时间,sys:系统内核消耗的CPU的时间,real:操作从开始到结束消耗所经过的墙钟时间。CPU时间与墙钟时间的区别是,墙钟时间除了包括CPU运算的时间外,还包括像磁盘IO等非运算的时间。
Heap
PSYoungGen total 9216K, used 4065K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 40% used [0x00000000ff600000,0x00000000ff93c5e0,0x00000000ffe00000)
from space 1024K, 73% used [0x00000000ffe00000,0x00000000ffebc040,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4756K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 46% used [0x00000000fec00000,0x00000000ff0a5020,0x00000000ff600000)
Metaspace used 2635K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
这里就不进行太详细的说明了,这里主要是堆空间和元空间使用情况的一些信息。比如eden space 8192K , 40% used,代表 eden 区总空间8192K,已经使用40%
7.分配策略与回收策略
7.1对象有现在Eden分配
大多数情况下,对象在新生代Eden区中分配。当新生代没有足够的空间时,虚拟机会执行一次Minor GC,我们可以通过-XX:PrintGCDetails参数,看到内存的一个使用情况。
其实当我们的程序什么都没有写时,eden区其实已经使用了部分空间,因为有很多java自己的系统类的对象都已经实例化。
内存的使用情况
我们从上面的数据可以发现当我们什么代码都没有写时,新生代依然使用了1150K,占了整个eden空间的14%, from space 和to space都使用了0%,
我们可以总结出两点结论:
- 生成的对象优先放入eden区
- java本身系统的对象也会在放在堆空间中,大约占用了eden空间的14%
7.2 大对象直接进入老年代
所谓的大对象是指,需要大量连续空间的Java对象,典型的大对象值那种特别长的字符串或者特别大的数组,当有大量大对象出现时,会造成一种很不好的现象,那就是堆空间明明有很多空间时就触发垃圾回收去回收他们,以此来保证有足够的空间来放置新的的大对象。
我们写一个例子;我们配置的堆空间大小为20M,新生代为10M,eden space、to space、from space三个区的比例为 8: 1 : 1。
public class MyTest6 {
public static void main(String[] args) {
int M = 1 << 20;
int K = 1 << 10;
byte[] bytes1 = new byte[8 * M];
}
}
GC日志的结果:
我们的在主函数中直接生成了8M大小的大数组,结果GC日志中的老年代的已使用的空间为8192k,刚好是8M,我也就验证了大对象直接进入老年代这一理论。
当然也有一个参数可以设定一个阈值,当一个对象的大小大于这个阈值时,就会被当做大对象
参数 | 作用 |
---|---|
-XX:PretenureSizeThreshold | 设定大对象的判定阈值 |
==注:==这个参数只对serial收集器、Parnew收集器有用,我所使用的jdk 1.8.如果不进行参数配置,默认的垃圾收集器是 Parallel Scavage收集器和Parallel Old收集器
7.3 长期存活的对像进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回首时,必然能识别出哪些是老年代,哪些是新生代,虚拟给每一个对象定义了一个年龄计数器,当一个对象经历了一个Minor GC,仍然存活,并进入到了survivor区,那该对象的年龄就变为1,对象每熬过一次GC,该对象的年龄就会加1,当年龄长到一定的程度,该对象就会进入老年代。这要涉及一个年龄阈值的概念,比如年龄阈值是15,那么当这个对象长到15岁是就进入到老年代。年龄阈值可以通过一个参数去设置。当然这只是一种理想的情况,MaxTenuringThreshold参数设定的阈值并不是固定不变的,是可以动态改变的,具体请看7.4动态对象年龄判定。
参数 | 作用 |
---|---|
-XX:MaxTenuringThreshold | 用来设置年龄阈值 |
7.4 动态对象年龄判定
为了更好的适应不用程序的内存情况,比如年龄阈值设置为15,然后survivor空间中有很多10岁的对象,这些对象一直存活,因为采用复制算法的原因,这些对象会一直会在survivor区中,浪费survivor区空间,所以并不是必须当一个对象的年龄到达MaxTenuringThreshold设定的值后,该对象才能进入老年代,除了大对象直接进入老年代的情况外,还有一种方式,在survivor空间中,当相同年龄的对象所占用空间大于总survivor空间的一半,年龄大于或者等于该年龄的对象就会进入老年代。
我们从此得出一个结论:一个对象并不是必须到达我们设置的年龄阈值后才能进入老年代,而是存在动态年龄判断的规则。除了上述的这些,还有一种情况对象会进入老年代,那就是空间分配担保
我们举一个例子来证明年龄阈值是可以动态变化的
package com.mec.aboutGC;
public class MyTest3 {
public static void main(String[] args) throws InterruptedException {
byte[] byte1 = new byte[1024 *1024];
byte[] byte2 = new byte[1024 *1024];
myGc();
Thread.sleep(1000);
System.out.println("11111");
myGc();
Thread.sleep(1000);
System.out.println("22222");
myGc();
Thread.sleep(1000);
System.out.println("33333");
byte[] byte3 = new byte[1024 *1024];
byte[] byte4 = new byte[1024 *1024];
byte[] byte5 = new byte[1024 *1024];
byte[] byte6 = new byte[1024 *1024];
byte[] byte7 = new byte[1024 *1024];
byte[] byte8 = new byte[1024 *1024];
myGc();
Thread.sleep(1000);
System.out.println("44444");
}
public static void myGc() {
for (int i = 0 ; i <= 40 ; i++) {
byte[] renbytes = new byte[1024 * 1024];
}
}
}
本此程序运行所添加的JVM参数为
参数 | 作用 |
---|---|
-verbose:gc | 显示gc的详细信息 |
-Xmx200m | 最大堆容量为200M |
-Xmn50m | 新生代为50M |
XX:+PrintTenuringDistribution | 打印GC后对象的年龄情况 |
-XX:+PrintGCDetails | 将垃圾收集的相关信息打印出来 |
-XX:TargetSurvivorRatio=60 | 在survivor区中,当相同年龄的对象大于60%时,动态调整年龄阈值,使大于或者等于这个年龄的对象进入老年代。 |
-XX:+PrintGCDateStamps | 打印GC的时间戳 |
-XX:+UseParNewGC | 使用ParNew收集器 |
-XX:+UseConcMarkSweepGC | CMS收集器 |
-XX:MaxTenuringThreshold=3 | 年龄阈值设定为3 |
运行结果:
2020-03-24T20:06:42.938+0800: [GC (Allocation Failure) 2020-03-24T20:06:42.938+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 2746104 bytes, 2746104 total
: 40551K->2712K(46080K), 0.0022040 secs] 40551K->2712K(60416K), 0.0022603 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
11111
2020-03-24T20:06:43.945+0800: [GC (Allocation Failure) 2020-03-24T20:06:43.945+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 568 bytes, 568 total
- age 2: 2630672 bytes, 2631240 total
: 43451K->2771K(46080K), 0.0023198 secs] 43451K->2771K(60416K), 0.0023745 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
22222
2020-03-24T20:06:44.951+0800: [GC (Allocation Failure) 2020-03-24T20:06:44.951+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
- age 1: 56 bytes, 56 total
- age 2: 568 bytes, 624 total
- age 3: 2630520 bytes, 2631144 total
: 43508K->2767K(46080K), 0.0011614 secs] 43508K->2767K(60416K), 0.0012075 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
33333
2020-03-24T20:06:45.955+0800: [GC (Allocation Failure) 2020-03-24T20:06:45.955+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 1 (max 3)
- age 1: 4194424 bytes, 4194424 total
- age 2: 56 bytes, 4194480 total
- age 3: 568 bytes, 4195048 total
: 43505K->4265K(46080K), 0.0045808 secs] 43505K->8902K(60416K), 0.0046193 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
44444
Heap
par new generation total 46080K, used 23295K [0x00000000f3800000, 0x00000000f6a00000, 0x00000000f6a00000)
eden space 40960K, 46% used [0x00000000f3800000, 0x00000000f4a957a0, 0x00000000f6000000)
from space 5120K, 83% used [0x00000000f6000000, 0x00000000f642a6b8, 0x00000000f6500000)
to space 5120K, 0% used [0x00000000f6500000, 0x00000000f6500000, 0x00000000f6a00000)
concurrent mark-sweep generation total 14336K, used 4636K [0x00000000f6a00000, 0x00000000f7800000, 0x0000000100000000)
Metaspace used 2637K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
7.5 空间分配担保
注:JDK1.6版本和之后的新版本空间分配担保策略不一样,我们先说JDk1.6版本的
Minor GC发生在新生代带,新生代一般使用的是复制算法,每一次GC都会把存活的对象放到一个survivor区里,但是万一出现了极端情况 ,那就是新生代的对象都存活,这样将所有的对象复制到一个survivor区,显然这个survivor是不够用的,所以老年代会担保剩下的对象,也就是说老年代会接受剩下的不能进入survivor区的对象。我们可以通过HandleAPromotionFailure参数的设定来决定是否允许担保。
具体的担保过程还是画个流程图吧
HandleAPromotionFailure参数一般都是开着的,因为可以减少没有必要的Full GC,**但是这个参数在JDK1.6 之后就不会影响空间分配担保策略,在JDK 1.6之后的规则变为只要老年代中的最大连续空闲空间大于新生代中的对象总大小或者大于历次进入老年代的对象大小的平均值,就会进行Minor GC,否则进行 Full GC,总体来说与以前的唯一处不同是,不会再有是否允许担保这个判断的存在。**也就是说JDK 1.6后的分配担保策略变成了
8.查看JVM默认参数
参数 | 作用 |
---|---|
-XX:+PrintCommandLineFlags | 得到JVm初始参数的默认值 |
9.HotSpot算法实现
9.1 枚举根节点
现代虚拟机判断一个对象是否可以被回收时,一般都使用的是根搜索法,也可以叫做可达性分析,可达性分析要将所有的用户线程全部挂起,因为要保证可达性分析时,引用关系的一致性,不能我在进行可达性分析时,引用关系还在动态变化,所以这就是进行GC时,必须停顿所有用户线程(STW)的原因。
在HotSpot虚拟机中,为了提高可达性分析的速率,使用一组OopMap的数据结构来达到这一目的。
9.2 安全点
在OopMap的帮助下,HotSpot虚拟机可以快速且准确地完成GC Roots枚举,使OopMap变化的指令非常多,如果每一条指令都生成对应的OopMap,那么就需要大量的额外空间,这样GC的空间成本就会变高,所以虚拟机并没不是为每条指令都生成一个OopMap,而是在特定的位置记录这些信息,这些特定的位置叫做安全点(SafePoint),即程序在执行过程中,并非任何时刻都可以停下来进行GC操作,而是只有当程序进入到安全点时才能暂停当前线程进行GC操作。
对于安全点还需要考虑,在进行GC操作时,如何将所有的用户工作线程在最近的安全点停顿下来,有两种处理办法:抢先式中断和主动式中断。
抢先式中断
抢先式中断就是先将所有的用户线程停下来,如果有的线程停顿的地方不是安全点,就恢复它,让它跑到安全点上再停下。之这种中断方法现在基本上不再用了。
主动式中断
GC将要中断线程时,会给每一个线程设置一个标记,每当线程执行到安全点时,查看这个标记,如果标记被设置,将该线程暂停。
9.3 安全区域
使用SafePoint似乎已经完美的解决了如何进入GC的问题,但是当程序不执行时,就无法到达安全点,停下该线程并且进行GC。比如正在阻塞的线程,这种情况就需要安全区(Safe Region)来解决,安全区可以理解为安全点的扩容,安全区域是指在一段代码片段之中,引用关系不会发生变化。也就是说,在这个区域的任何位置,执行GC都是安全的,我们可以将安全区域理解为安全点的扩展。
当线程执行到Safe Region中的代码,首先会标识自己已经进入了Safe Region,那样,当执行GC时就不用管在安全区里的这个线程了,离开安全区时,要检查系统是否完成了根节点枚举,如果完成,该线程就可以继续执行,否则该线程必须等待,直到收到可以离开安全区的信号为止。
10.总结
本文并没有对G1垃圾回收器做介绍,这也是有缺陷的地方,JVM的奥妙不是看一本书,或者听一个人的网课能彻底参透的,以后的路还很长。希望今天的努力,能成为本人找工作的实力。——2020年3月27日