Java内存管理与垃圾回收

1.Java内存管理


1.1内存分配与对象初始化

当我们使用new关键字创建一个Java对象时,JVM首先会检查这个new指令的参数是否能在常量池中定位到一个类的符号引用,然后检查与这个符号引用相对应的类是否已经成功经历过加载,解析和初始化等步骤(涉及类加载机制),当类完成装载步骤之后,就可以完全确定创建对象实例所需要的内存空间大小,接下来JVM将会对其进行内存分配,以存储所生成的对象实例。

如果内存空间中已用和未用的内存各自一边,彼此之间维系一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上就行,这种分配方式就叫指针碰撞,反之则只能用空闲列表(Free List)执行内存分配。

1.1.1内存分配方式之-----快速分配策略

因为堆区是线程共享的,因此在并发环境下,从堆区划分内存空间是非线程安全的,由于要保证数据操作的原子性,基于线程安全的考虑,如果一个类在分配内存之前就已经成功完成类装载步骤,JVM就会优先选择在TLAB(Thread Local Allocation,本地线程分配缓冲区)中为对象实例分配内存空间,TLAB在Java堆区中是一块线程私有区域的区域,它包含在Eden空间内。除了可以避免一系列非线程安全问题,还能够提升内存分配的吞吐量,这种内存分配方式称为快速分配策略


1.1.2完成内存分配后,初始化对象实例

JVM首先会对分配后的内存空间进行零值初始化,确保对象的实例字段在Java代码中可以不用赋初始值就能够直接使用。接着,JVM就会初始化对象头和实例数据,最后将对象的引用入栈后再更新程序计数器中的字节码指令地址。经过内存分配和初始化,一个Java对象实例才算创建成功。


1.2逃逸分析与栈上分配

Java堆区不是对象内存分配的唯一选择,对象内存分配还可以在堆外进行。

堆外存储好处:降低GC的回收率和提升GC的回收效率。

常见的堆外存储技术:利用逃逸分析技术筛选出未发生逃逸的对象,然后避开堆区直接选择在栈帧中分析内存空间。


1.2.1概念

JVM在执行性能优化之前的一种分析技术,具体目标就是分析出对象的作用域。
简单来说,当一个对象被定义在方法体内部之后,它的受访权限就仅限于方法体内,一旦其引用被外部成员引用后,这个对象就发生了逃逸,反之如果定义在方法体内的对象并没有被任何外部成员引用,JVM就会将其分配在栈帧中。

1.2.2优点

如果没有发生逃逸,那么对象就会在栈上分配内存,因此GC就无需执行内存回收,因为栈帧会伴随方法的调用而创建,伴随方法的执行结束而销毁。所以,栈上分配的对象所占用的内存空间会随栈帧的出栈而释放。

注意:JDK6后,已经默认开启逃逸分析了。

1.2.3举例

/**
 * 逃逸分析与栈上分配
 */
public class StackAllocation {
    
    public StackAllocation obj;
    
    public StackAllocation getObj() {
        /*方法返回StackAllocation对象实例,发生逃逸,分配在堆 */
        return null == obj ? new StackAllocation() : obj;
    }
    
    public void setObj() {
        /*为成员变量赋值,发生逃逸,分配在堆*/
        //方法体内的对象被外部成员obj引用,所以发生逃逸
        obj = new StackAllocation();
    }
    
    public void useStackAllocation1() {
        /*引用成员变量的值,发生逃逸,分配在堆*/
        //方法体内的obj对象的引用实际上为外部成员obj对象的引用,因此发生逃逸
        StackAllocation obj = getObj();
    }
    
    public void useStackAllocation2() {
        /*对象的作用域仅限于方法体内,未发生逃逸,分配在栈上*/
        //方法体内的对象并没有被任何外部成员引用
        StackAllocation obj = new StackAllocation();
    }
}


2.垃圾标记

2.1垃圾标记

在GC执行垃圾回收之前,需要区分区内存中哪些是存活对象,哪些是死亡对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时释放其占用的内存空间。

如何标记?

2.1.1标记算法——引用计数算法

为程序中的每一个对象创建一个私有的引用计数器,当目标对象被其他存活对象引用时,引用计数器中的值加1,不再引用就减1,当引用计数器中的值为0的时候,就意味该对象已经不再被任何存活对象引用,可以标记为垃圾对象。

缺陷: 一些明显死亡的对象之间存在相互引用时,引用计数器中的值永远不会为0,这样会导致GC在执行内存回收时永远无法释放掉无用内存所占用的内存空间,极有可能引发内存泄漏。

2.1.2标记算法——根搜索算法(基本在用这个)

以根对象集合作为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中的存活对象都会被根对象集合直接或者间接连接着),如果目标对象不可达,就意味着该对象已经死亡。

根对象集合存储的内容:
1.Java栈中的对象引用
2.本地方法栈中的对象引用
3.运行时常量池中的对象引用
4.方法区中类静态属性的对象引用
5.与一个类对应的唯一数据类型的Class对象



3.垃圾回收——分代收集算法

成功区分出内存中存活对象和死亡对象后,接下来就是执行垃圾回收释放无用对象所占用的内存空间了。

3.1回收算法之——标记-清除

标记后,清除。不执行其他操作。
缺点: 执行效率低下,并且由于被执行内存回收的无用对象所占用的内存空间有可能是一些不连续的内存块,不可避免地会产生一些内存碎片,从而导致后续没有足够的可用内存空间分配给较大的对象。

3.2回收算法之——复制

Java堆区分为新生代(又分为Eden空间,From Survivor空间,To Survivor空间)和老年代。
空间比例: Eden:From Survivor+To Survivor = 8:1

当执行一次Minor GC(新生代的垃圾回收)时,Eden空间中存活对象会被复制到To Survivor空间,并且之前已经经历过一次Minor GC且在From Survivor空间中存活下来的对象如果还年轻的话同样也会被复制到To Survivor空间内。

复制方向:(Eden->To Survivor,From Survivor->To Survivor)

不会复制到To Survivor中的特殊情况:

1.存活对象的分代年龄超过指定的阈值时,将会直接晋升到老年代中
2. To Survivor空间的容量达到阈值时,存活对象直接晋升到老年代中

当所有存活对象被复制到To Survivor空间或晋升到老年代后,剩下的均为垃圾对象,这就意味GC可以对这些死亡对象执行一次Minor GC,释放其占用的空间。

执行完Minor GC后,Eden空间和From Survivor空间将会被清空,存活下来的对象全部存储在To Survivor空间内,接下来From Survivor和To Survivor空间互换位置。其实复制算法无非就是使用To Survivor空间作为一个临时空间交换的角色,务必保证两块Survivor空间中一块必须时空的,着就是复制算法。

应用场景:多应用于新生代中。

缺点:

不适用于老年代中的内存回收:因为老年代中的对象生命周期比较长,极端情况下能与JVM生命周期保持一致,如果老年代也采用复制算法,那么内存回收需要额外的空间和时间,还会导致较多的复制操作从而影响GC执行效率。


3.3回收算法之——标记-压缩

成功标记垃圾对象后,该算法会将所有的存活对象都移动到一个规整且连续的内存空间中,然后执行Full GC(老年代的垃圾回收,或者称之为Major GC)回收无用对象所占用的内存空间。

成功执行压缩后,已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,则可以使用 指针碰撞 技术修改指针的偏移量将新对象分配在第一个空闲内存位置上,为新对象分配内存带来便捷。

应用场景:多应用于老年代中。

3.4图例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值