深入理解JVM内存区域

一、运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 这些区域都有各自的用途, 以及创建和销毁的时间, 有的区域随着虚拟机进程的启动而存在, 有些区域则依赖用户线程的启动和结束而建立和销毁。
运行时数据区域结构图如下,蓝色部分是线程共享的,红色部分线程私有的。
在这里插入图片描述

1、线程私有

1)、程序计数器

程序计数器是一块较小的内存空间, 它可以看作是当前线程所执行的字节码的行号指示器。 即指示当前正在执行哪段代码。
程序计数器的作用:
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、 循环、 跳转、 异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻, 一个处理器 都只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器, 各条线程之间计数器互不影响, 独立存储, 我们称这类内存区域为“线程私有”的内存。

2)、Java虚拟机栈

每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。
Java虚拟机规范中, 对这个区域规定了两种异常状况: 如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError异常; 如果虚拟机栈可以动态扩展( 当前大部分的Java虚拟机都可动态扩展, 只不过Java虚拟机规范中也允许固定长度的虚拟机栈), 如果扩展时无法申请到足够的内存, 就会抛出OutOfMemoryError异常

  • 局部变量表:就是局部变量的表,用于存放我们的局部变量的。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型。如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。
  • 操作数据栈:存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法是会一直运行入栈/出栈的操作。
  • 动态连接:Java语言特性多态(需要类加载、运行时才能确定具体的方法),动态特性(Groovy、JS、动态代理)。
  • 返回地址:正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)。
    Java虚拟机栈和栈帧的关系如下图:(下图Java栈就是Java虚拟机栈)
    在这里插入图片描述

3)、本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的, 它们之间的区别不过是虚拟机栈为虚拟机执行Java方法( 也就是字节码) 服务, 而本地方法栈则为虚拟机使用到的Native方法服务

2、线程共享区域

1)、Java堆

Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例和数组, 几乎所有的对象实例都在这里分配内存。
为什么是几乎所有的对象实例都在堆上分配内存?
因为还有栈上分配,虚拟机提供的一种优化技术,基本思想是,对于线程私有的对象,将它打散分配在栈上,而不分配在堆上。好处是对象跟着方法调用自行销毁(栈是线程私有,线程结束,栈就销毁了),不需要进行垃圾回收,可以提高性能。
Java堆中还可以细分为: 新生代和老年代; 再细致一点的有Eden空间、 From Survivor空间、 To Survivor空间等。

2)、方法区

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池:
是方法区的一部分。 Class文件中除了有类的版本、 字段、 方法、 接口等描述信息外, 还有一项信息是常量池, 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后进入方法区的运行时常量池中存放。

二、垃圾回收

1、怎么判断哪些对象可以回收

1)、引用计数算法

给对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加1; 当引用失效时, 计数器值就减1; 任何时刻计数器为0的对象就是不可能再被使用的。
引用计数算法的问题:
很难解决对象之间相互循环引用的问题。

public class ReferenceCountingGC {
    private static final int _1MB = 1024 * 1024;
    private byte[] bigSize = new byte[1 * _1MB];

    public static void main(String[] args) throws InterruptedException {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null; 
        //假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    }
}

上面代码中因为objA和objB相互引用,它们的引用计数不为0,即使objA和objB设置为null时使用引用计数法也不能回收。

2)、可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表,如局部变量)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
    注意:
    1、为什么类中成员变量引用对象为什么不算GC Roots?
    因为成员变量属于对象,而对象放在堆中,垃圾回收时这个对象可以被回收。因此这个对象即引用也不存在了,引用和堆中对象就没有关联了。
    2、虚拟机栈中引用对象当方法运行完就可以回收了。

2、引用

1)、强引用

一般的Object obj = new Object() ,就属于强引用。只要强引用还存在, 垃圾收集器永远不会回收掉被引用的对象。

2)、软引用

一些有用但是并非必需,用软引用关联的对象,系统将要发生OOM之前,这些对象就会被回收。

3)、弱引用

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

4)、虚引用

虚引用也称为幽灵引用或者幻影引用, 它是最弱的一种引用关系。 一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 在JDK 1.2之后, 提供了PhantomReference类来实现虚引用。

3、垃圾回收算法

1)、标记-清除算法

最基础的收集算法是标记-清除”算法,算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象, 在标记完成后统一回收所有被标记的对象。主要不足有两个: 一个是效率问题, 标记和清除两个过程的效率都不高; 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时, 无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
在回收钱这里插入图片描述
回收后:在这里插入图片描述

2)、复制算法

它将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。 这样使得每次都是对整个半区进行内存回收, 内存分配时也就不用考虑内存碎片等复杂情况, 只要移动堆顶指针, 按顺序分配内存即可, 实现简单, 运行高效。 只是这种算法的代价是将内存缩小为了原来的一半, 未免太高了一点。
在这里插入图片描述
回收后:
在这里插入图片描述
IBM专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。
我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保
复制收集算法在对象存活率较高时就要进行较多的复制操作, 效率将会变低。所以复制算法适合新生代。

3)、标记-整理算法

标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存
在这里插入图片描述
回收后:
在这里插入图片描述

5)、分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集算法, 这种算法并没有什么新的思想, 只是根据对象存活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中, 每次垃圾收集时都发现有大批对象死去, 只有少量存活, 那就选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集。 而老年代中因为对象存活率高、 没有额外空间对它进行分配担保, 就必须使用“标记—清理”或者“标记—整理”算法来进行回收

4、Minor GC和Full GC区别

新生代GC(Minor GC): 指发生在新生代的垃圾收集动作, 因为Java对象大多都具备朝生夕灭的特性, 所以Minor GC非常频繁, 一般回收速度也比较快
老年代GC(Major GC/Full GC): 指发生在老年代的GC, 出现了Major GC, 经常会伴随至少一次的Minor GC( 但非绝对的, 在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 Major GC的速度一般会比Minor GC慢10倍以上

5、堆内存分配策略

在这里插入图片描述

1)、对象优先在Eden分配

大多数情况下, 对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时, 虚拟机将发起一次Minor GC。

2)、大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象, 最典型的大对象就是那种很长的字符串以及数组 。 大对象对虚拟机的内存分配来说就是一个坏消息, 比遇到一个大对象更加坏的消息就是遇到一群朝生夕灭的短命大对象, 写程序的时候应当避免, 经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置它们。

3)、长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存, 那么内存回收时就必须能识别哪些对象应放在新生代, 哪些对象应放在老年代中。 为了做到这点, 虚拟机给每个对象定义了一个对象年龄( Age) 计数器,放在对象头中。 如果对象在Eden出生并经过第一次Minor GC后仍然存活, 并且能被Survivor容纳的话, 将被移动到Survivor空间中, 并且对象年龄设为1。 对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,会移动到另一块Survivor区,当它的年龄增加到一定程度( 默认为15岁), 就将会被晋升到老年代中。

4)、动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

5)、空间分配担保

新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。

6、垃圾收集器

收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值