深入理解对象和垃圾回收机制

2020-06-14  本文已影响0人  仕明同学

虚拟机创建对象的过程

image.png

使用Serial、ParNew(ParNew 收集器是年轻代常用的垃圾收集器,它采用的是复制算法)等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表

MS,mark-sweep 算法的简写(先扫描整个 heap,标出可到达对象,然后执行 sweep 操作回收不可到达对象。这个算法本身比较简单)

指针碰撞 空闲列表

并发安全的问题

4、 内存空间初始化
(注意不是构造方法)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如int值为0,boolean值为false等等)。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

内存分配完成后根据配置可能会需要将分配到的内存空间初始化为0值。

5、设置
设置对象头(哪个类的实例,hash code,GC代年龄等)
虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes在Java hotspot VM内部表示为类元数据)、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。


image.png

6、对象完成 初始化


对象的内存的布局

image.png image.png

对象头

Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。在32位系统占4字节,在64位系统中占8字节;
Class Pointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;
Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;

对象实际数据

对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节(64位系统中是8个字节)

对齐填充

Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。例如,一个包含两个属性的对象:int和byte,这个对象需要占用8+4+1=13个字节,这时就需要加上大小为3字节的padding进行8字节对齐,最终占用大小为16个字节。

对象头占用空间大小

image.png

指针压缩

从上文的分析中可以看到,64位JVM消耗的内存会比32位的要多大约1.5倍,这是因为对象指针在64位JVM下有更宽的寻址。对于那些将要从32位平台移植到64位的应用来说,平白无辜多了1/2的内存占用,这是开发者不愿意看到的。
从JDK 1.6 update14开始,64位的JVM正式支持了 -XX:+UseCompressedOops 这个可以压缩指针,起到节约内存占用的新参数。

对象的访问定位

image.png

1、句柄
Java堆中将会划分出一块内存来作为句柄池,reference中 存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
2、直接指针
使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址

两者比较

(1)使用句柄来访问的最大好处就是reference中存储的是稳 定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
(2)使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销, 由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。(HotSpot采用的就是这个)

判断对象的存活

引入计数方法

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象内存磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。

当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。

使用这种方式进行内存管理的语言:Objective-C
Python在用,但主流虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,这样做影响效率,

可达性分析(根可达)

image.png

在计算机编程中,跟踪垃圾收集(英语: Tracing garbage collection )是一种自动内存管理的算法,该算法通过分析某些“根”对象的引用关系,来确定需要保留的可访问对象,并释放其余的不可访问对象的内存空间。该算法在实际的软件工程中得到了广泛的应用

类的回收条件:
注意Class要被回收,条件比较苛刻,必须同时满足以下的条件(仅仅是可以,不代表必然,因为还有一些参数可以进行控制):
1、 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
2、 加载该类的ClassLoader已经被回收。
3、 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

finalize 方法强制对象不回收,但是这个线程执行比较低,需要延迟一下

image.png

finalize :当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”

即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。

image.png image.png

由于finalize方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。尽量不要使用finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,建议大家忘了finalize方法!因为在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好

对象的分配策略

image.png
class Main {   
  public static void main(String[] args) {     
    example();   
  }   
  public static void example() {     
    Foo foo = new Foo(); //alloc     
    Bar bar = new Bar(); //alloc     
    bar.setFoo(foo);   
  }
}  
class Foo {}  
class Bar {   
  private Foo foo;   
  public void setFoo(Foo foo) {     
    this.foo = foo;   
  }
}

在这个示例中,创建了两个对象(用alloc注释),其中一个作为方法的参数。方法setFoo()接收到foo参数后,保存Foo对象的引用。如果Bar对象保存在堆中,那么Foo的引用将逃逸。但在这种情况下,编译器可以使用逃逸分析确定Bar对象本身并没有逃逸example()的调用。这意味着Foo引用无法逃逸。因此,编译器可以安全地在栈上分配两个对象。

各种引用

一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收

虚引用 PhantomReference 这个其实开发中也不会使用,因为这个出来就会被回收,看不到具体的值,这个主要是来监听GC是否在运用,我理解的就是这样
最弱(随时会被回收掉)
垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作

分代收集理论

image.png

个人理解:分新生代(Eden,翻译为伊甸园,就是新生代)和老年代( Tenured ,翻译为终生制,就是老年代)

新生代
占比和老年代为 1:3
使用复制算法,因为新生代里面的对象都是朝夕生死,迭代的很快
老年代

每次GC的时候,如果是存活的对象,就直接放到from 区,同时格式化其他的区域

垃圾回收算法

复制算法(Copying)

内存移动是必须实打实的移动(复制),不能使用指针玩。

image.png
专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

标记-清除算法(Mark-Sweep)

image.png

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-整理算法(Mark-Compact)

image.png

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。

上一篇下一篇

猜你喜欢

热点阅读