JVM内存管理


1、Java运行时数据分区

根据Java虚拟机规范的规定,Java虚拟机所管理的内存被划分为5个运行时数据区,如下图:
Java内存分布图

a、     方法区:用于存储已被虚拟机加载的内信息、常量、静态变量、即时编译器编译后的代码等数据,由此可见它是各个线程可以共享的内存区域。Java虚拟机规范将其描述为堆的一个逻辑部分,但其实需要把它和堆区分开来的。

b、     虚拟机栈:Java方法执行的内存模型。可以简单的理解为Java方法在虚拟机栈中执行,更细节的实现是,每个方法被执行的同时在虚拟机栈中创建一个用于虚拟机进行方法调用和方法执行的数据结构,称为栈帧。栈帧是虚拟机栈的栈元素。每一个方法从调用到执行完成,对应了栈帧压栈和弹栈的过程。

c、     本地方法栈:类似于虚拟机栈,不同是虚拟机栈针对的是执行Java方法,而本地虚拟机栈针对的是虚拟机使用到的native方法。本地方法: 简单地讲,一个Native Method就是一个java调用非java代码的接口。

d、     堆:Java堆是Java虚拟机管理的内存中最大的一块,在虚拟机启动时创建。与方法区一样,它也被各个线程共享。Java虚拟机规范:所有的对象实例以及数组都要在对上分配。现在由于编译器的发展所有的对象实例都要在堆上分配已经不是绝对的了,比如JIT编译器。

e、     程序计数器:与虚拟机栈和本地方法栈一样他是线程独享的,它用于记录正在执行的虚拟机字节码地址。当执行的是一个本地方法时,值为Undefined,在Java的各个运行时数据区域中只有程序计数器这个内存区域没有规定内存溢出的情况。

f、     运行时常量池:方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。

2、运行时数据分区的相关概念

a、栈帧:每个栈帧都包含了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。栈帧的大小取决于Java虚拟机对Java虚拟机规范的实现。一个线程中的方法调用链可能是很长的,很多方法都同时处于执行状态,对于执行引擎来说,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与之对应的方法称为当前方法,当前方法所在的类称为当前类,当前类中有对应的当前常量池(类的Class文件中关于常量池入口的定义)。
栈帧的概念结构图:
栈帧的概念结构图


b、局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。以变量槽(Slot)为基本单位,变量槽的大小时可变的,一般地它的大小要能够存放boolean、byte、char、short、int、float、reference(大小与虚拟机和指针是否压缩有关,用于查找对象在堆中存储的起始地址索引和对象方法区中的类型信息)、returnAddress(方法出口,旧式的虚拟机中用于异常处理,现已弃用),当使用了64位的物理存储空间时,最后变量的槽的外观也应该是像32位虚拟机中的一样,为了这个虚拟机会采用对齐补白的方式的手段。
     虚拟机通过索引定位使用局部变量表。对于一个非静态的方法索引0默认为当前方法所属的对象实例的引用(this),之后依次排列方法参数和方法内定义的局部变量。

c、操作数栈:也称作操作栈,它是一个后入先出栈结构,操作数栈的元素可以是任意的Java类型。操作数栈的作用可以理解为一个临时的存储区域,如下:

Java代码:
int a = 100;
int b = 98;
int c = a+b;

操作数栈内存结构图

d、动态连接:每个栈帧都包含一个指向运行时常量池中该栈所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件中常量池存有大量的符号引用,字节码中方法调用指令,就是以常量池中指向方法的引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候就转化为直接引用,称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这称为动态连接。具体可以研究哈Java类文件结构。

e、方法返回地址:有两种方式退出一个方法,通过正常方法出口和异常方法出口。正常方法出口指执行完方法所有的内容或者遇到return。异常方法出单指方法执行过程中遇到异常且这个异常没有在异常处理器表中搜索到对应的信息导致的方法异常退出。这两张方式都需要返回到方法的调用点,然后程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,这样才能恢复它上层方法的执行状态。

3、内存分配与回收由于java堆与方法区是线程共享的所以与程序计数器、虚拟机栈和本地方法栈线程独享的区域不同,需要动态的分配和回收内存。 

要回收一个对象,首先需要判断这个对象是否还被引用,怎么样判断呢?
a、引用计数法:给对象中添加一个引用计数器,如果有一个地方引用了这个对象一次,则计数器值+1;当引用失效时,计数器值-1,任何时刻计数器为 0 的对象就是不可能再被使用的对象。
引用计数法的优点是实现简单且效率较高,但它的缺点也很明显,就是很难解决对象之间相互循环引用的问题。
b、可达性算法:通过一系列称为“GC ROOTS”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC ROOT之间没有任何引用链相连时,则证明此对象时不可用的。
可达性算法解决了引用计数法遇到的问题,但是GC ROOTS得选择非常重要,GC ROOTS一般包括如下几种: 
     1、虚拟机(栈帧中的局部变量表)中引用的对象。
      2、方法区中类静态属性引用的对象。
      3、方法区中常量引用的对象。
      4、本地方法栈中本地方法引用的对象。

上面的两种方法,要判断对象是否还被引用都离不开“引用”,那么引用是个上面东东?
引用的定义(狭隘的):如果reference类型的数据中存储的数字代表的是另一快内存的起始地址,就称这块内存代表着一个引用。
上面对引用的定义是狭隘的,只能表示有没有被引用两种状态,所以在JDK1.2之后引用的概念得到了扩展:
     a、强引用:只要强引用还在,垃圾收集器用于不会回收掉被引用的对象。
          Object o = new Object();
     b、软引用:用于描述还有用但是并不是必须的对象。对于软引用关联的对象,在系统将要发生内存溢之前,将会把这些对象列进回收范围之中,进行第二次回收。
           SoftReference<A>  reference    =   new   SoftReference<>(   new   A());               
     c、弱引用:同软引用一样,它也描述非必须的对象。但弱引用关联的对象当垃圾收集器启动时,不管内存空间是否足够都会被回收掉。
           WeakReference<A>   reference   =   new   WeakReference<A>( new   A());
     d、虚引用:最弱的一种引用关系。虚引用关系不会对对象的生命周期构成印象,它的作用仅仅是指对象被回收时收到一个系统通知。
           PhantomReference<A>   reference   =   new   PhantomReference<A>( new   A(),   new   ReferenceQueue<>());
     注意: 虚引用的指示对象总是不可到达的。

对象何时被回收
即使在可达性算法中不可到达的对象,也并不是马上就被回收掉。回收之前还需要以下过程:
     a、标记不在引用链中的对象,并根据对象是否需要执行finalize()方法,将需要执行finalize()的对象放置到一个叫做F-Queue的对列中,之后启动一个低优先级的线程去执行。
     b、对F-Queue对列中的对象进行第二次标记,如果执行完finalize()之后对象没有被拯救,那么回收这个对象。

回收方法区
对于方法区的回收主要针对两部分内容,无用的类和废弃的常量。
     a、回收废弃的常量,与回收队中的对象类似,如果一个常量在系统中不在被引用,那么该常量将被回收。
     b、回收无用的类,回收无用的类并不是必须的,但如果要回收则需要确定一个类是否真的是无用。同时具备下面的3点的类,才能算无用的类
          1、该类所有的实例已经被回收。
          2、加载该类的类加载器已经被回收。
          3、该类对应的Java.lang.Class对象没有被引用到,无法再任何地方通过反射访问给类的方法。

垃圾收集分析算法
a、标记-清除算法
     先标记出需要回收的对象,标记完之后统一回收被标记的对像。
     该算法存在效率问题和回收后空间碎片太多的问题,尤其是内存碎片的问题,将导致需要分配大对象时由于没有连续的内存空间,不得不再次触发下一次垃圾回收,而垃圾回收又是低效的。

b、复制算法
     将可用内存按容量分为大小相等的两块,每次只是要其中一块。当这一块内存用完了,就将还存活的对象复制到还存活的另一快上,然后再把已使用的内存空间一次性全部清理掉。
     优点:解决了标记-清除算法的效率和内存空间碎片的问题。
     缺点:将可用内存缩小为原来的一半。且如果对象存活率较高时复制操作会变得频繁效率变低。极端情况下,如果对象100%存活还需要额外空间进行分配担保。

c、标记整理算法
     标记过程与标记-清除算法一样,但标记完后让所有存活的对象向一端移动,然后直接清理掉端边界意外的内存。
     优点:解决了内存空间碎片问题,内存使用更合理
     缺点:效率一般。
d、分代收集算法
     根据对象的生命周期不同将内存划分为几块。一般将Java堆分为新生代和老年代。在新生代中使用复制算法(Eden,Survivor),在老年代中使用标记-清除或者标记-整理方法。

内存的分配和回收策略
对象的内存分配,就是在堆上分配(不考虑JIT编译后的情况)。对象主要分配到新生代的Eden上,如果启动了本地线程分配缓冲(为每个线程分配一小块内存),将按线程优先在TLAB上分配。如何分配并非是固定不变的,取决于使用的垃圾收集器和虚拟机的内存设置参数。下面是几条普遍采用的分配规则:
     a、对象优先在Eden上分配,当没有足够的内存空间进行分配时,虚拟机将发起一次Minor GC。
     b、大对象直接进入老年代,大对象指的是需要大量连续的内存空间的对象,这样做的目的是避免在新生代中发生大量的内存复制。
     c、长期活着的对象将进入老年代,当一个新生代对象的年龄计数器的值大于一定范围时,就会晋升到老年代中。

如果一个对象在新时代的Eden上被分配并经历Minor GC后依然存活,并且被Survivor容纳移动到Survivor后对象年龄+1,之后对象在Survivor中每经历一次Minor GC且不死则对象年龄+1,当该年龄值大于一定范围时对象被移动到老年代中。但为了能更好适应不同程序的内存状况,虚拟机并是永远要求对象的年龄达到指定的值,如果在Survivor中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。 
新生代使用了复制收集算法,但为了保证内存的利用率,只使用其中一个Survivor来作为轮换备份空间,因此当存在大量的对象在Minor GC后还活着的情况时就需要老年代进行分配担保。当前题是老念代要有足够担保空间,如果空间不足那么只好发起一次 FULL GC。    





注:本文为《深入理解Java虚拟机》学习笔记,图片均来自百度图片。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值