前言
上一篇博客只是对内存区域做了一个简单的描述,如果想要更深入的理解,其实还远远不够,只需要明确堆与栈的区别即可。这一篇博客就从对象出发,总结对象在堆上分配的几个原则,同时总结一下JVM的垃圾回收策略,对于具体的垃圾回收器在后续中进行总结。
对象在堆上的表现
通过总结对象在堆上的表现,正好可以复习一下堆的各个组成区间,并引入一些垃圾回收算法。访问对象是常有的事情,即使是最简单的对象访问也会涉及Java栈,Java堆和方法区三个最重要的内存区域。
对象的结构
还是从之前总结的JVM内存模型出发,之前说过——在本地变量表(位于JVM stack的栈帧中)中,只有8中基本的数据类型,还有一种就是引用数据类型,这种数据类型指向了堆中的一块地址,这块堆中的地址就存储这对象的实例数据,但是还有一句话,对象的描述信息存储与方法区,所以我们可以大致描绘一个以下的图(出自:https://blog.csdn.net/zqz_zqz/article/details/70246212)
所以我们可以看出,对象的组成似乎分为了两个部分,一个是具体的数据部分(对象实例)存在堆上,一个就是这个对象的描述部分(类元信息) 存放在方法区。依旧是如上图所示,堆上的对象实例必须有一个指针指向在方法区上的元信息,否则怎么知道这个对象是啥子类型的。同时,我们是否还能记起,似乎每一个对象都能作为synchronized的锁呢?这个也和对象的结构有关系(参见之前的博客:偏向锁,轻量级锁,重量级锁)。其实一个对象的信息分为对象头,实例数据和对其填充三个部分。
对象头
对象头又分为Mark word,指向类信息的指针,数组长度(只有数组对象才有),在上一篇博客中介绍过mark word 如下所示:
这里不解释Mark word的具体信息。指向类信息的指针——即是指向Java对象的类数据保存在方法区。对其填充只是为了对齐相应的字段长度。
对象的访问定位
之前提到过,在操作数栈通过一个引用类型去获取对象信息和数据,但是针对访问方式其实并没有具体的探讨,在《深入理解Java虚拟机》一书中,针对这个问题做过总结——主流的访问方式又两种:使用句柄和直接指针的方式访问
使用句柄
这种方式其实就是在java堆中划分出一块内存来作为句柄池,reference中存储的就是句柄地址,并且句柄中包含了对象的实例数据和类型数据的地址。
直接指针
这种方式,reference类型中存储的就是对象在堆中的地址。
两种对象访问的方式都有优势,第一种:对象地址变化了,reference不用变更,维护起来比较方便。针对第二种:最大的好处就是访问速度更快。HotSpot采用的是第二种,因此之前我们介绍对象信息的时候,也已HotSpot为例。
对象在堆内存中的分配策略
对象的分配,往大方向上将其实就是在堆上分配对象,对象分配的规则不是100%固定的,这个和垃圾收集器和内存大小的参数有关
1、优先在Eden区域分配
实例代码:
/**
* autor:liman
* createtime:2019/11/26
* comment: 对象优先分配在eden区
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
*/
public class AllocationEden {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
byte[] allocation01,allocation02,allocation03,allocation04;
allocation01 = new byte[2*_1MB];
allocation02 = new byte[2*_1MB];
allocation03 = new byte[2*_1MB];
allocation04 = new byte[4*_1MB];//这里会发生一次GC,分配失败
}
}
关于如何看懂GC日志,这个可以参看大牛的这一篇博客——看懂GC日志
配置如下参数:
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
运行之后的GC日志如下:
[GC (Allocation Failure) [DefNew: 8134K->657K(9216K), 0.0050231 secs] 8134K->6801K(19456K), 0.0050633 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 4919K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff0297c0, 0x00000000ff400000)
from space 1024K, 64% used [0x00000000ff500000, 0x00000000ff5a4790, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 367K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
配置的堆大小为20M,且不可扩展,一半分配给了新生代,一半分配给了老年代。新生代按照8:1:1的比列分配给eden区,from,to区,按照优先分配的原则,在1~3行代码中,都会分配在eden区。在分配allocation4的时候所需要的4MB空间的时候,发现eden区不够了,这个时候触发MinorGC(年轻代的GC) ,GC的时候发现前面的三个allocation对象在survivor区域中也无法放下,所以就只好把这三个对象放到老年代去,GC结束之后,allocation4被放到eden区,老年代被allocation1~3占用。
2、大对象直接进入老年代
虚拟机提供了一个PretenureSizeThreshold的参数,这个参数指定一个大小,大于这个大小的对象会被直接被分配到老年代(这个参数只对Serial和ParNew垃圾收集器有效,垃圾收集器会在后面进行总结)
实例代码:
这里在上一个实例的基础上加入了PretenureSizeThreshold的参数,指定其为3M大小,我们分配的allocation数组大于3M因此会直接进入老年代。
/**
* autor:liman
* createtime:2019/11/26
* comment: 大对象直接进入老年代
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
* PretenureSizeThreshold这个参数只对Serial和ParNew两款垃圾收集器有效
*/
public class PretenureSizeThresholdDemo {
private static final int _1MB = 1024*1024;
public static void main(String[] args) {
byte[] allocation;
allocation=new byte[4*_1MB];
}
}
运行后的GC日志如下所示:
可以看到新生代几乎没有使用,老年代却使用了40%。
Heap
def new generation total 9216K, used 2154K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee1aa88, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
Metaspace used 3433K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 373K, capacity 388K, committed 512K, reserved 1048576K
3、长期存活的对象会进入老年代
虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在eden出生,并经过一次Minor GC 之后仍然存活,并且能被Survivor容纳,则将其移动到Survivor空间中,并且这个时候年龄计数器+1。经历过一次Minor GC,对象的年龄计数器+1,当这个计数器大于-XX:MaxTenuringThreshold时,则会晋升到老年代中。
这个直接先上实例:
/**
* autor:liman
* createtime:2019/11/26
* comment: 验证长期存活的对象进入老年代
* -verbose:gc -Xms80M -Xmx80M -Xmn40M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
* 对象的元数据会占用一些空间,需要将堆空间分配的足够大
*/
public class TenuringThreshold {
private static final int _1MB = 1024 * 1024;
@SuppressWarnings("unused")
public static void main(String[] args) {
byte[] allocation01, allocation02, allocation03,allocation04;
allocation01 = new byte[_1MB ];
allocation02 = new byte[16 * _1MB];
allocation03 = new byte[16 * _1MB]; //这里发生第一次GC
allocation03 = null;
allocation03 = new byte[16 * _1MB]; //这里发生第二次GC
}
}
-XX:MaxTenuringThreshold=1时的GC日志:
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 1 (max 1)
- age 1: 1731256 bytes, 1731256 total
: 20686K->1690K(36864K), 0.0100287 secs] 20686K->18074K(77824K), 0.0100809 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 1 (max 1)
: 18074K->0K(36864K), 0.0016417 secs] 34458K->18062K(77824K), 0.0016683 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 36864K, used 16712K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
eden space 32768K, 51% used [0x00000000fb000000, 0x00000000fc052040, 0x00000000fd000000)
from space 4096K, 0% used [0x00000000fd000000, 0x00000000fd000000, 0x00000000fd400000)
to space 4096K, 0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
tenured generation total 40960K, used 18062K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
the space 40960K, 44% used [0x00000000fd800000, 0x00000000fe9a3b78, 0x00000000fe9a3c00, 0x0000000100000000)
Metaspace used 3440K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
可以看到from/to空间的使用率为0
-XX:MaxTenuringThreshold=15时的GC日志:
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age 1: 1706376 bytes, 1706376 total
: 20030K->1666K(36864K), 0.0127152 secs] 20030K->18050K(77824K), 0.0127653 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age 1: 25504 bytes, 25504 total
- age 2: 1693592 bytes, 1719096 total
: 18378K->1678K(36864K), 0.0021712 secs] 34762K->18062K(77824K), 0.0021943 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 36864K, used 20029K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
eden space 32768K, 56% used [0x00000000fb000000, 0x00000000fc1eba08, 0x00000000fd000000)
from space 4096K, 40% used [0x00000000fd000000, 0x00000000fd1a3b38, 0x00000000fd400000)
to space 4096K, 0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
tenured generation total 40960K, used 16384K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
the space 40960K, 40% used [0x00000000fd800000, 0x00000000fe800010, 0x00000000fe800200, 0x0000000100000000)
Metaspace used 3438K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
这个时候发现from区域并不为空,因为allocation01并没有进入到老年代。
4、动态对象年龄判定
虚拟机并不永远要求对象的年龄达到-XX:MaxTenuringThreshold设定的值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,则大于等于该年龄的对象,都会进入到老年代。这个时候并不会理会XX:MaxTenuringThreshold设定的参数
如下代码:
/**
* autor:liman
* createtime:2019/11/26
* comment: 验证长期存活的对象进入老年代
* -verbose:gc -Xms80M -Xmx80M -Xmn40M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public class TenuringThresholdAge {
private static final int _1MB = 1024 * 1024;
@SuppressWarnings("unused")
public static void main(String[] args) {
byte[] allocation01, allocation02, allocation03,allocation04;
allocation01 = new byte[_1MB];
allocation02 = new byte[_1MB ];
allocation03 = new byte[16 * _1MB]; //这里发生第一次GC
allocation04 = new byte[16 * _1MB]; //这里发生第一次GC
allocation04 = null;
allocation04 = new byte[16 * _1MB]; //这里发生第二次GC
}
}
将-XX:MaxTenuringThreshold设置为15的GC日志如下:
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 1 (max 15)
- age 1: 2779840 bytes, 2779840 total
: 21710K->2714K(36864K), 0.0113288 secs] 21710K->19098K(77824K), 0.0113711 secs] [Times: user=0.00 sys=0.02, real=0.01 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
: 19098K->0K(36864K), 0.0020399 secs] 35482K->19086K(77824K), 0.0020638 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 36864K, used 16712K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
eden space 32768K, 51% used [0x00000000fb000000, 0x00000000fc052040, 0x00000000fd000000)
from space 4096K, 0% used [0x00000000fd000000, 0x00000000fd000000, 0x00000000fd400000)
to space 4096K, 0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
tenured generation total 40960K, used 19086K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
the space 40960K, 46% used [0x00000000fd800000, 0x00000000feaa3b78, 0x00000000feaa3c00, 0x0000000100000000)
Metaspace used 3438K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
发现from区并没有被使用,老年代却有46%被使用。
5、空间分配担保
从名字可以看出来,所谓的空间分配担保,无非就是老年代对新生代分配不了的对象做一个担保。《深入理解Java虚拟机》一书中对这个模块做了写总结,但是觉得写得有点混乱,其实只是不同的垃圾收集器,担保策略不同而已。
在执行任何一次Minor GC,JVM会检查一下老年代可用的内存空间,是否大于新生代所有对象的总大小(因为有可能一次GC之后,新生代所有的对象都存活下来了,都需要进入老年代,如果老年代没有足够的空间承接这些对象,那不就尴尬了)。如果发现大于,则放心GC,如果不大于,这个时候就需要看一个-XX:-HandlerPromotionFailure的参数是否设置了,如果有这个参数,就会进行下一步判断,看老年代可用的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。如果大于则冒险直接来一次Minor GC ,如果小于则先来一次Full GC (老年代的垃圾回收)然后在Minor GC,如果还是没有足够的空间,则抛出OOM。整个过程文字表述太臃肿,还是来图吧
这里直接上代码示例
/**
* author:liman
* createtime:2019/11/27
*/
public class HandlerPromotionTest {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation01, allocation02, allocation03, allocation04, allocation05, allocation06, allocation07;
allocation01 = new byte[8 * _1MB];
allocation02 = new byte[8 * _1MB];
allocation03 = new byte[8 * _1MB];
allocation01 = null;
allocation04 = new byte[8 * _1MB]; //这里会发生GC,这次GC之后,01被回收,02,03号对象进入到老年代,04被分到eden区
allocation05 = new byte[8 * _1MB];
allocation06 = new byte[8 * _1MB];
allocation04 = null;
allocation05 = null;
allocation06 = null;
allocation07 = new byte[8 * _1MB]; //这里会发生第二次GC,GC完成之后,07直接分配在了新生代
}
}
p.s:HandlePromotionFailure在JDK 1.5之前是默认关闭的,需要手动指定,在1.6之后就默认开启了。
[GC (Allocation Failure) [DefNew: 27854K->728K(36864K), 0.0215016 secs] 27854K->17112K(77824K), 0.0475125 secs] [Times: user=0.00 sys=0.01, real=0.05 secs]
[GC (Allocation Failure) [DefNew: 25936K->724K(36864K), 0.0037041 secs] 42320K->17108K(77824K), 0.0037575 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
Heap
def new generation total 36864K, used 9899K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
eden space 32768K, 28% used [0x00000000fb000000, 0x00000000fb8f5dc8, 0x00000000fd000000)
from space 4096K, 17% used [0x00000000fd000000, 0x00000000fd0b51d8, 0x00000000fd400000)
to space 4096K, 0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
tenured generation total 40960K, used 16384K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
the space 40960K, 40% used [0x00000000fd800000, 0x00000000fe800020, 0x00000000fe800200, 0x0000000100000000)
Metaspace used 3451K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 376K, capacity 388K, committed 512K, reserved 1048576K
最终两次GC之后,02和03号对象进入老年代,所占空间比例为8*2/40=40%符合预期(详细的GC步骤分析在代码中)
一个小结
这一部分是参考的《深入理解Java虚拟机》一书,感觉写的有些零散,这里做一些总结,大体对象分配流程如下:
上图中在经历survivor分配,进入老年代还有可能是对象年龄计数器达到阈值,则进入老年代。
垃圾收集的一些思想
垃圾收集过程其实包含两个大的过程,一个是标记,一个是清除。标记——既该回收的对象给其打上标记,清除就是释放该对象所在的内存空间。
标记对象的算法
其实有些资料都会提到应用计数和根搜索的方法,大体上也只有这两种。
引用计数
给对象中增加一个引用计数器,每当有一个地方引用它的时候,计数器+1,当引用失效的时候,计数器-1,任何时刻计数器为0的对象就会被回收。这种方式实现起来非常简单,但是很难解决循环依赖的问题(这里不再赘述)。但是提到引用这个概念,其实就得好好探讨一下了。
到底有几种引用
如下所示,根据局部变量表中对堆对象引用的强度分为强引用,软引用,弱引用和虚引用。后面会针对软引用,弱引用和虚引用举一些实例,这些实例可以不用深入,只需要理解各自垃圾回收时的表现即可。
强引用
我们通常所谓新建对象的赋值语句,即为强引用。
强引用可以直接访问目标对象,强引用所指的对象在任何时候都不会被回收,宁愿抛出OOM也不会回收。
StringBuffer str = new StringBuffer("hello world");
软引用
当堆内存空间不足的时候,就会被回收。
import java.lang.ref.SoftReference;
/**
* autor:liman
* createtime:2019/11/28
* comment:软引用实例
* -Xms16M -Xmx16M -Xmn8M -XX:SurvivorRatio=8
* 软引用:被GC标记之后,如果内存分配不足的时候才会被回收
*/
public class SoftRefDemo {
public static class User{
public User(int id,String name){
this.id = id;
this.name=name;
}
private int id;
private String name;
private byte[] data = new byte[1024*1024];
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static void main(String[] args) {
User u = new User(1,"liman");
SoftReference<User> userSoftReference = new SoftReference<User>(u);//构建一个软引用
u = null; //去除强引用
System.out.println(userSoftReference.get()); //从软引用中获取对象
System.gc(); //进行第一次GC
System.out.println("After GC:");
System.out.println(userSoftReference.get()); //垃圾回收之后,从软引用中获取对象
byte[] b = new byte[1024*1024*7]; //分配大对象,系统会自动进行GC
System.gc();
System.out.println(userSoftReference.get()); //还是从软引用中获得对象
}
}
弱引用
发现之后,不管如何都会被GC回收
import java.lang.ref.WeakReference;
/**
* autor:liman
* createtime:2019/11/28
* comment: 弱引用实例
* -Xms16M -Xmx16M -Xmn8M -XX:SurvivorRatio=8
* 弱引用,发现之后,不管如何都会被GC回收
*/
public class WeakReferenceDemo {
public static class User{
public User(int id,String name){
this.id = id;
this.name=name;
}
private int id;
private String name;
private byte[] data = new byte[1024*1024];
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static void main(String[] args) {
User u = new User(1,"liman");
WeakReference<User> userWeakReference = new WeakReference<User>(u);
u = null;
System.out.println(userWeakReference.get());
System.gc();
System.out.println("After GC:");
System.out.println(userWeakReference.get());
}
}
虚引用
和几乎没有引用一样,随时都可以被回收
import java.lang.ref.WeakReference;
/**
* autor:liman
* createtime:2019/11/28
* comment: 虚引用实例
* -Xms16M -Xmx16M -Xmn8M -XX:SurvivorRatio=8
* 虚引用,随时可被GC回收
*/
public class WeakReferenceDemo {
public static class User{
public User(int id,String name){
this.id = id;
this.name=name;
}
private int id;
private String name;
private byte[] data = new byte[1024*1024];
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public static void main(String[] args) {
User u = new User(1,"liman");
WeakReference<User> userWeakReference = new WeakReference<User>(u);
u = null;
System.out.println(userWeakReference.get());
System.gc();
System.out.println("After GC:");
System.out.println(userWeakReference.get());
}
}
可达性分析
通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径为引用链,当一个对象引用链表示这个对象不可达(既从根节点无法到达该节点,类似树形结构的遍历),则该对象会被标记为待回收对象。
那这里我们再进一步思考一下,是不是只要不会被回收的对象就可以作为GC Roots对象,实时证明确实如此,懒得打字了直接上图吧
清除对象的算法
这里并不是真正意义上的算法,应该说是一种垃圾回收的思想。同时需要明确的是,没有一个垃圾回收算法是十全十美的,只有最合适的
标记清除
标记清除是垃圾回收的思想基础,标记清除将垃圾回收分为两个阶段——标记和清除,在标记阶段标记可达的对象,这个阶段完成之后,没被标记的对象就是垃圾对象。在清除阶段将这些对象回收。
缺点:回收后的空间是不连续的,这对大对象的分配很不友好。
复制
复制算法的思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。(上一篇博客中对堆进行了详细的划分,其中的from/to空间采用的就是复制回收算法)
优点:不会有碎片,效率较高
缺点:系统空间使用率折半
标记整理
前面两种算法常用于新生代,但是对于老年代,更常见的是大部分对象都是存活的,如果依然使用复制算法,由于存活对象较多,复制的成本会很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法——这就是标记整理(标记压缩)算法。
标记整理算法在标记清除算法的基础上进行了优化,和标记算法一样,标记整理算法也会先标记需要回收的对象,之后清理边界外所有的空间,这种方法即避免了碎片的产生,不需要两块相同的内存空间了,因此,其性价比比较高。
分代算法
分代算法,并不是一个具体的算法,而是将内存划分为几个区针对不同的区采用不同的算法,这里承接上一篇博客最后内存区域的划分图如下:这里补充说明一下,新生代中默认eden区和from/to区的比例为8:1:1
总结
本篇博客其实是对《深入理解Java虚拟机》和《实战Java虚拟机》两本书相关内容的一个总结,主要总结了对象分配的几种可能,同时小结了一下垃圾回收的几种基础思想,这个是后面总结垃圾回收器的重点。
这里顺便总结一下对象会进入老年代的几种可能情况
1.躲过指定的gc次数,达到高龄之后进入老年代.这个高龄由一些参数指定,默认为15。
2.动态年龄判定规则,如果Survivor区域内年龄1+年龄2+年龄3+年龄n的对象总和大于Survivor区的50%,此时年龄n以上的对象会进入老年代,不一定要达到15岁.
3.对象过大,直接进入老年代
4.一次Minor GC之后,对象存活过多,survivor区容纳不下,则这些对象直接进入老年区
参考资料
《实战JAVA虚拟机 JVM故障诊断与性能优化》
《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》
目前看来,这两本书比网上90%讲JVM的博客质量都要好。