JVM内存模型_图文并茂

写在前

刚开始学习java的时候,就经常看到面试的时候有问到JVM,那时候学习重心放在如何使用上,未曾去考虑底层。现如今,渐渐回味,发现这jvm真是深不可测。

这里,先允许我放上一篇参考的比较生动的博文:
Java虚拟机(JVM)你只要看这一篇就够了!
当然,我觉得这一篇明显还是不够的。

在自己能够理解的前提下,综合着我上一篇博客,
并发编程的魅力_3天打基础_fager
配合本篇描述,大概也就能把JVM杀的七零八落了。
上干货吧~

JVM内存模型
一、JDK7 虚拟机运行时数据区
在这里插入图片描述
1.程序计数器:
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2.java虚拟机栈
在这里插入图片描述
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
3.本地方法栈
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
4.java堆
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
5.方法区
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
在这里插入图片描述
6.运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)。
7.直接内存
非虚拟机运行时数据区的部分
在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。
二、HotSpot虚拟机对象探秘
主要介绍数据是如何创建、如何布局以及如何访问的。
1.对象的创建
遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(‘指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)。
前面讲的每个线程在堆中都会有私有的分配缓冲区(TLAB),这样可以很大程度避免在并发情况下频繁创建对象造成的线程不安全。
内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成。
2.对象的内存布局
在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。
实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。
补充:
(1)对象优先在Eden上分配;
(2)大对象直接进入老年代;
(3)长期存活的对象将进入老年代对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中。
3.对象的访问定位
使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。
通过句柄访问:
Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。
在这里插入图片描述
使用直接指针访问
reference 中直接存储对象地址
在这里插入图片描述
比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。
三、垃圾回收器与内存分配策略
1.概述
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有);栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。
而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收器所关注的就是这部分内存。
通俗点说:垃圾回收盯着堆和方法区(永久代),其他的地方不用管。
2.回收三代
在这里插入图片描述
堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆),也有称为永久代;栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。 所有的对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:伊甸区(Eden),幸存者区域(Survivor Sapce),老年代(Old Generation Space)。
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆),也有称为永久代;
在这里插入图片描述
在这里插入图片描述
3.引用计数法
在这里插入图片描述
引用计数法无法解决对象间相互引用的问题,会导致内存泄漏。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
4.可达性算法
在这里插入图片描述
可以作为GC Roots的对象有三种:
虚拟机栈的栈帧的局部变量表所引用的对象;
本地方法栈的JNI所引用的对象;
方法区的静态变量和常量所引用的对象。
在这里插入图片描述
在这里插入图片描述
根据可达性算法分析,对象实例3和实例5不可达,故会作为GC回收的目标。
5.再谈引用
前面的两种方式判断存活时都与‘引用’有关。但是 JDK 1.2 之后,引用概念进行了扩充,下面具体介绍。
下面四种引用强度一次逐渐减弱:
强引用:
在这里插入图片描述
类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。
软引用:
在这里插入图片描述
在这里插入图片描述
SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
弱引用:
在这里插入图片描述
在这里插入图片描述
WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
虚引用:
在这里插入图片描述
在这里插入图片描述
PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
6.生存还是死亡
即使在可达性分析算法中不可达的对象,也并非是“facebook”的(非死不可),这时候它们暂时出于“缓刑”阶段,一个对象的真正死亡至少要经历两次标记过程:如果对象在进行中可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象建立关联即可。
finalize() 方法只会被系统自动调用一次。
7.垃圾回收算法
7.1标记清除算法
标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程。标记—清除算法的执行情况如下图所示:
在这里插入图片描述

该算法有如下缺点:
标记和清除过程的效率都不高;
标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
7.2复制算法
复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:
每次只对一块内存进行回收,运行高效;
只需移动栈顶指针,按顺序分配内存即可,实现简单;
内存回收时不用考虑内存碎片的出现。
它的缺点是:可一次性分配的最大内存缩小了一半。
在这里插入图片描述

7.3标记整理算法
复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:
在这里插入图片描述
7.4分代回收
当前商业虚拟机的垃圾收集都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。
在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集;
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。
7.5GC算法综合分析
在这里插入图片描述
7.6小朋友你是否有很多疑问
为什么要分代?
分代的垃圾回收策略,是基于这样一个事实:不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
什么情况下触发垃圾回收?
在这里插入图片描述
为什么不是一块Survivor空间而是两块?
在这里插入图片描述
为什么Eden空间这么大而Survivor空间要分的少一点?
在这里插入图片描述
在这里插入图片描述
7.7七大收集器
收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。
在这里插入图片描述
说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。
7.7.1Serial 收集器
这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
在这里插入图片描述
7.7.2ParNew 收集器
可以认为是 Serial 收集器的多线程版本。
在这里插入图片描述
并行:Parallel
指多条垃圾收集线程并行工作,此时用户线程处于等待状态
并发:Concurrent
指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
7.7.3Parallel Scavenge 收集器
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。

7.7.4Serial Old 收集器
收集器的老年代版本,单线程,使用 标记 —— 整理。
在这里插入图片描述
7.7.5Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理。
在这里插入图片描述
7.7.6CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。
运作步骤:
初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象;
并发标记(CMS concurrent mark):进行 GC Roots Tracing;
重新标记(CMS remark):修正并发标记期间的变动部分;
并发清除(CMS concurrent sweep)。
在这里插入图片描述
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片。
7.7.7G1 收集器
面向服务端的垃圾回收器。
优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:
初始标记(Initial Marking);
并发标记(Concurrent Marking);
最终标记(Final Marking);
筛选回收(Live Data Counting and Evacuation)
在这里插入图片描述

补充

在这里插入图片描述
在这里插入图片描述

1)Java堆
      所有对象的实例分配都在Java堆上分配内存,堆大小由-Xmx和-Xms来调节,demo1如下所示:
public class demo1{  
      static class OOMObject{}  
      /** 
     * @param args 
     */  
    public static void main(String[] args) {  
        List<OOMObject> list = new ArrayList<OOMObject>();  
         while(true){  
            list.add(new OOMObject());  
                        }  
    }  
  }  
加上JVM参数-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError,就能很快报出OOM:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
并且能自动生成Dump。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下图有一个字有误,通过设置-XX:+UserParallelGC可以使用并行垃圾回收器。(应该是并,不是串)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值