JVM自动内存管理机制

2021/7/28 7:07:55

本文主要是介绍JVM自动内存管理机制,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一,java内存区域与对应的内存溢出异常

java与C++之间有一堵由内存分配和垃圾收集技术所围成的墙,墙外面的人想进去,墙里面的人却想出来

  1. 对于学习C++的程序员,拥有每一个对象的所有权,又需要担负着每一个对象生命开始到终结的责任。
  2. 对于java程序员,在虚拟机内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete代码,不容易出现内存泄露和内存溢出的问题,不过也正是内存控制的权力交给了虚拟机,一旦出现问题,旧很难排查错误。

1. 运行时内存区域分布

java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,不同的区域有不同的作用,创建,销毁的时间。
image

JVM运行时内存分布

image

1. 线程私有三剑客

1. 程序计数器

problem counter Register是一块较小的内存空间,

1. 程序计数器的作用:
就是告诉虚拟机选取下一条需要执行的指令,是当前线程所执行的字节码的行号指示器。

我们所在程序中写的分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器。我认为在程序中需要利用行号来进行代码的跳转的操作都是依赖于计数器的。
2. 为什么线程私有?
最根本的原因在于机制,jvm通过切分处理器时间进行线程的轮流切换来实现多线程。

换一句话说,在一个时刻,一个处理器(多核处理器来说就是一个内核)只会执行一个线程中的一条指令,当然在多线程并发编程中是另外一种操作,不过这种说法对于普通的java程序来说是通用的。

线程是来回切换的,如何保存每个线程执行的进度,所以需要给每个线程来配备程序计数器,各个程序计数器之间不影响,程序计数器所需要的内存也很小,所以程序计数器就是线程私有的。
3. 程序计数器都底里面存储了什么?
1. 如果执行的java方法,程序计数器存储的是正在执行的虚拟字节码指令的地址;虚拟字节码应该就是这个指令,存储的是正在执行的指令的地址。

2. 如果正在执行的是native方法,程序计数器存储的是空值
4. 关于程序计数器有关异常的特殊情况
这个内存区域是唯一没有规定outofmermoryError的情况的区域。
2. java虚拟机栈

java virtual Machine Stacks java虚拟机栈

1. java虚拟机栈为什么是线程私有的?
对于线程来说,一般会调用多个方法。在每个方法执行的时候,会同时创建一个栈帧(stack frame)。

栈帧会存储局部变量表,操作栈,动态链接,方法出口等信息。这些信息都于方法的执行有关。当方法执行结束,对应的栈帧就会出栈。这里就可以解释递归为啥会出现栈相关的错误。

Java虚拟机栈与线程的生命周期相同。当线程结束,java虚拟机栈就会消失。

总的来说,java虚拟机栈描述的是一个线程的java方法的运行内存模型。
2. 为什么有人把java内存区分为栈内存和堆内存?
这种分法比较粗糙,java内存区域的划分实际上更加的细致一些,更加的复杂。这种划分方式只能说明大多数程序员最关注的,与对象分配关系最密切的区域就是这两块。

但是这里的栈指的就是Java虚拟机栈。或者说虚拟机中的局部变量表部分。
3. 关于局部变量表中的一些细节。
1. 局部变量表中存放了各种基本数据类型,对象引用,和returnAdress类型。

     1. 对象引用包括以指向对象起始地址的引用指针,代表对象的句柄,或者其他与此对象相关的位置,根据不同的虚拟机,实现可能不同。个人认为就是对于一个对象的引用。
	 
     2. rA类型是指向了一条字节码指令的地址,也相当于字节码指令的指针。
	 
2. 其中有一些特殊情况。对于64位的long类型和double类型的数据会占用2个存储空间Slot,其余的类型只占用一个空间。局部变量在方法运行前是完全确定的,所以局部变量表所占的内存可以在编译期间完全分配,内存大小固定。
4. 在java虚拟机中规定的异常情况。
1. StackOverflowError异常:线程请求的栈深度大于java虚拟机允许的深度。通常可以从递归方法看到。

2. OutOfMemoryError异常:当虚拟机栈动态扩展的时候,无法申请到足够多的内存。
5. java虚拟机栈有什么用:
里面实际上代表了java方法运行的顺序与机制,为java虚拟机执行方法服务,存放了java方法的运行,也就是栈帧。栈帧就是方法执行的数据结构。
3. 本地方法栈

Native Method Stacks 本地方法栈

1. 与java虚拟机栈的相同点:
1. 都是为java虚拟机执行方法服务

2. 可以抛出的异常相同

3. 同样是线程私有的。
2. 与java虚拟机栈的不同点:
1. 所面向的方法不同,java虚拟机面向的是java方法,而本地方法栈面向的是Native方法。对本地方法栈中的方法实现的语言,使用方式,数据结构并没有强制规定。

2. 不同的虚拟机所实现的结构不同,有的虚拟机直接将本地方法栈和虚拟机方法栈合二为一。

2. 线程共享之领域

1.java堆

Java Heap java堆

1. 什么是java堆?
java堆是java虚拟机所管理的内存中最大的一块。唯一目的就是存放对象实例。

java堆可以存放在不连续的空间上,只要逻辑上连续就可以。在实现时可以实现成固定大小的,也可以实现成可以扩展的(通过-Xmx和-Xms来控制)

在java虚拟机中规范的原文就是:所有的对象实例以及数组都要在堆上分配。 但是随着一些技术的发展,所有的对象都分配在堆上就没有那么绝对了。
2. java堆是线程私有的吗?
不是的,java堆伴随着Java虚拟机启动时创建,所有的对象实例都要在这边分配内存,所以是线程共享的。
3. java堆和垃圾回收机制有什么关系?
java堆是垃圾回收的主要区域,所有很多时候也叫GC堆(垃圾堆?)。从内存回收的角度来看,现在的垃圾回收基本上都采用的是分代回收算法。所以java堆中还可以细分为各种空间。如新生代,老年代;伊甸区,成年区,老年区等。

如果从内存分配的角度去看,线程共享的java堆可以划分出多个线程私有的分配缓存区(Thread Local Allocation Buffer)

最终不管如何划分,目的就是为了更好的回收内存。或者更快的回收内存。
4. java虚拟机规范所规定的异常:
OutOfMemoryError异常:一般来说主流的虚拟机都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,并且堆无法完成扩展的就会抛出对应的异常。
2.方法区

Method Area 方法区

1. 什么是方法区?
方法区用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。可以发现这里主要是当类加载时,虚拟机所需要知道的信息,存储在方法区里面。虽然属于堆的一个逻辑部分,但是别名叫做Non-Heap,目的是与java堆分开来。
2、常量池来自哪里?
在class文件中除了有类的版本,字段,方法,接口等描述信息,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。这部分内用会在类加载后存放到方法区的运行时常量池中。
3. 常量池是非动态的吗?
不是,运行时常量池与class文件常量池不同点就是具备动态性,不要求常量只能在编译期产生,运行期间也可以放入新的常量。例子是String类的intern()方法

不在JVM中的直接内存

这部分内存不属于JVM,但是在JVM运行时这些内存也会被占用,甚至报异常。了解不多。知道和IO操作,通道和缓存技术有关。

2. 如何去访问一个对象

image

1,在访问之前创建一个对象吧,那么创建对象时内存发生了什么?

Object object = new Object();

  1. 首先反应的是java虚拟机栈的该方法的栈帧的局部变量表创建一个引用变量
  2. 在堆中创建该对象实例,实例数据中存放各个字段的实例数据,相关的类型需要在方法区中查找,所以须包含能查找到此对象类型数据(对象类型,父类信息,实现的接口,方法等)的地址信息。

对于如何定义这个引用,或者通过那种方式来定位,以及访问到java堆中的对象的具体位置,不同的虚拟机实现的对象访问方式会有所不同。一般有两种方式来定义这个引用,一种叫做句柄,一种叫做直接指针。根据定义引用的不同,访问对象的方式也有所不同。

2.使用句柄来实现引用

该方式相当于通过中转站的方式来进行对象访问

通过在堆中划分出一块内存来作为句柄池,引用对象中存储的就是对象的句柄地址,在句柄中包含了对象实例数据和类型数据各自的地址信息。
image
这种存储方法的优势在于一定程度上的解耦,引用中存储的是对象句柄,这样实例数据地址改变后,引用也不需要改变。

3.使用直接指针来实现引用

引用变量直接存储的就是对象实例的地址,对象实例中存储的类型数据的地址,层层套娃。优点是速度快,节省了一次查询的开销,HotSpot使用这种方式
image

3.关于OOM异常的总结

image

二,纠结的垃圾回收机制与相应的内存回收策略

  1. GC需要完成的三件事

哪些内存需要回收?
什么时候回收?
如何回收?

  1. 为什么要去了解GC和内存分配策略?

当需要排查各种内存溢出,内存泄露。
当垃圾收集成为系统达到高并发的瓶颈时。我们需要对自动化进行必要的调节。

  1. 对于内存分配策略和GC,我们需要关注哪部分内存呢?

对于程序计数器,java虚拟机栈,本地方法栈,这些线程私有的内存区域,生命周期于线程相同。VM栈中的栈帧随着方法的进入和退出进行出栈和进栈的操作。栈帧的大小也可以在编译期通过类型结构确定下来需要多少内存。所以逻辑上这几个内存区域的内存回收具有一定的确定性,线程或者方法接收的时候内存就回收。

但是对于方法区和堆来说,我们只有在程序运行期间才知道要创建那些对象,常量是否要增加。这部分的内存分配和回收都是动态的。垃圾收集器主要关注的就是这部分内存。

1. 确认对象的死亡

在垃圾回收之前,我们需要确认那些那些对象需要回收。

1. 确认对象死亡的一些算法

1.引用计数法

算法描述:给对象添加一个引用计数器,每当一个地方引用它时,引用计数器就+1,如果这个引用失效时,引用计数器就-1。可以认为当引用计数器为0时就是不会再被应用的。

  1. java为什么没有使用引用计数法?

因为应用计数法再多个个对象循环引用的时候就无法工作,当这些对象不再使用时,由于互相引用,使得引用计数器无法通知GC收集器来回收他们。

2.根搜索算法

主流的商务语言中都使用根搜索算法,算法的基本思路就是:通过一系列为GCroot的对象作为起始点,通过引用来链接对象,从root到一个节点对象中间叫引用链。当一个节点不可达是,我们认为这个对象是不可用的。\

当枝叶与树根断开,不论如何枝繁叶茂都会死亡。

  1. 如何分析那些对象适合作为root?
    1. 堆和程序计数器肯定是不行的,堆中都是对象实例数据,root很有可能在这里但是无法判定,程序计数器不含有对象。
    2. 本地方法栈中本地方法所引用的对象
    3. 虚拟机栈中的变量表所应用的对象,代表了方法在调用这些对象
    4. 方法区中类静态属性调用的对象,代表了这些对象作为类的静态属性存在。
    5. 方法区中常量引用的对象了,类似与类静态属性对象。都是要提前存在方法区内

2.java对于引用的扩充

如果引用类型的数据中存储的是另外一块内存的起始地址,认为这就是狭义的引用的定义。在jdk1.2之后中对应用进行了扩充,分为强引用,软引用,弱引用,虚引用
image

  1. 强引用:指的是狭义的引用,只要强引用存在,GC永远不会清理这些对象。
  2. 软引用:用于描述一些鸡肋的对象,当系统将要发生内存溢出之前,GC会清理这些对象。如果这次回收还没有得到足够的内存,就会报异常
  3. 弱引用:对应只有弱引用的话,可以逃过本轮的GC,但是下一轮GC将被回收。
  4. 虚引用:虚引用不带表引用,存在的意义就是在GC的时候可以得到一个系统通知。

3.对象如何死里逃生

当对象通过根搜索算法无法到达时,不会立刻被清理。还是会经历一些标记的过程。这些对象会放入一个队列中。如果需要这个对象复活,可以尝试在这个对象排队等待死亡的过程中,再次调用它。通常可以通过finalize方法实现,但是不常见。

4.方法区中有啥垃圾需要回收。

1.废弃常量

当你在系统中找不到引用这个常量的对象时,这个常量就是废弃的常量。与判断对象的算法类似。

2.无用的类

  1. 如何判断这是一个无用的类
  • 堆中不存在这个类的实例。
  • 这个类的类加载器也被回收。
  • 这个类对应的class对象没有在任何地方引用,反射不到。

不是说无用的类就一定会被回收,虚拟机中可以提供参数控制它。在大量使用反射,动态代理,等框架的场景。都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

2.垃圾收集算法

image

1.标记清除算法

image
首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象

主要缺点:

  • 效率问题:标记和清除的效率都不高
  • 空间问题:标记清除之后会产生大量的不连续的碎片,空间过于碎片化会导致,内存需要分配较大对象时没有足够的内存空间。不得不多触发一次垃圾收集动作。

2.复制算法

这个算法就是典型的空间换时间,将内存分为两半,通过扫描将活着的对象复制到另外一块上面,然后把使用过的一般内存直接全部一次清理掉。
image
这个算法的优点:

  • 效率快:扫描一遍完事。实现简单,运行高效
  • 不会存在内存碎片化的情况。

这个算法的缺点:

  • 代价很大,直接使得逻辑上的内存变成一半。

3.标记整理算法

将存活的对象标记后,将这些对象整理到一端后,再清理边界以外的所有对象。
image
优点:

  • 高效率
  • 不浪费空间

4.分代算法

将需要清理的区域分为新生代和老年代。对于新生代来说,每次GC都有大量的对象死去,少量存活。适合使用调整后的复制算法。对于老年代来说,存活的对象多,使用复制算法的话,有可能存活对像过多没有区域存放,适合使用整理类算法。

3. 垃圾收集算法的实现-垃圾收集器

虚拟机规范中没有对垃圾收集器的规定,所以不同厂商的垃圾收集器千差万别。这里写的是HotSpot的垃圾收集器
不过一般我们用的就是这个版本的JVM
image
image

1.Serial收集器

这是一个最古老,最历史悠久的GC收集器。
这是一个单线程的GC收集器
当Serial收集器出现时,世界都将停止。
image
这个收集现在依然是jvm运行再client模式下的默认新生代收集器。优点就是简单高效。

2.ParNew收集器

ParNew收集器就是Serial收集器的多线程版本,其余和Serial收集器一样,包括控制参数,收集算法,世界停止,等等。
image
虽然没啥除了多线程没有啥特点,但却是jvm在server模式下首选的新生代处理器。笑死,原因这个收集器的性能无关。因为CMS收集器只能与serial,ParNew收集器配合工作,这与收集器实现的代码框架相关。

虽然在单核和双核Parnew收集器的优势不明显,甚至还不如Serial,但是随着可以使用的CPU的数量的增加,可以充分利用系统资源。

3.Parallel Scavenge收集器

基本情况与ParNew一致
与ParNew不同之处:

  • 由于实现架构不同,不能与GMS收集器配合。
  • 关注点不同,ParNew关注每次GC用户进程所需要停顿的时间,而Parallel Scavenge,关注吞吐量。就是CPU运行用户代码与CPU运行时间的比值,所以PS比较适合交互少的系统。
  • PS可以根据当前系统的运行情况收集系统,来自己调节细节参数,自适应调节也是很重要的特性。

4.Serial old收集器

使用标记整理算法,单线程
就是serial的老年代版本
这个版本主要也是client模式下的虚拟机使用
主要用途:

  • 搭配Parallel Scanvenge收集器使用
  • 作为CMS收集器的后备预案,在并发收集发生 concurrent Mode Failure 的时候使用
    image

5.Parallel old收集器

ps收集器的老年代版本,多线程,使用标记整理算法。可以与PS收集器形成吞吐量优先组合。
image

6.G1收集器

Garbage First收集器
相对CMS收集器下面的这些改进:

  • 基于标记整理算法,不产生空间碎片
  • 可以非常明确的控制停顿
    G1是将整个java堆划分为多个大小固定的区域,跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的时间回收垃圾最多的区域。

7.CMS收集器

Concurrent Mark Sweep 多线程 标记-清除

多线程标记清理收集器,目标是获取最短回收停顿时间。大多数应用都是在服务器上跑,重视与用户的交互速度,CMS就符合用户的需求。

CMS收集器收集过程:

  • 初始标记【STW】:标记根搜索算法可以直接标记的对象
  • 并发标记:标记根搜索算法非直接关联到的对象。
  • 重新标记【STW】:修正并发标记期间,因为用户程序标记发生变动的的一部分对象的标记记录
  • 并发清除:将标记的对象清除

因为整个过程耗时最长的并发标记和并发清除过程中,收集器线程是可以和用户线程一起工作。所以总体上来说,CMS收集器是可以与用户线程一起并发运行的。

CMS的缺点:

  • 对CPU资源非常敏感,多线程的通病了,在CPU核心多的时候,用户感受不到。当CPU核心少的时候,用户就会感受到明显的执行速度降低。
  • CMS收集器无法清理浮动垃圾,由于在CMS并发清理阶段用户线程还在不断的运行着,自然有新的垃圾不断产生。这次GC无法清理掉这些垃圾,只能等到下一次GC的时候清理掉。这些就是浮动垃圾。
  • Concurrent Mode Failure失败:由于垃圾收集阶段,用户线程还需要运行,老年代内存中还在不断的填入新的对象。所以CMS收集器不能在老年代几乎被填满的时候才开始运行。需要提前开始运行,默认是在68%的时候激活。要是CMS在运行期间预留的内存被占满了,无法满足用户程序的需要。这时就会出现Concurrent Mode Failure失败,使用Serial old来救场,停止所有线程来进行垃圾收集。
  • 标记清除算法带来的内存碎片化。可以通过参数设置碎片整理和压缩。

8.垃圾收集器之间的关系,有连线的就可以相互组合

image

4.从垃圾回收角度考虑的内存分配策略

image

1.对象优先在Eden分配,地方不够就触发一次minor GC

image

2.大对象直接进入老年代

主要是避免过多的大对象导致新生代内存不足,触发过多次数的minor GC

3.长期存活的对象进入老年代

为对象增加一个年龄计数器,每当熬过一次minor GC就增加一岁,15岁之后进入老年代。阈值可调节。

4.对象年龄动态判定

为了更好的适应不同程序的内存情况,不一定需要到达年龄界限才能进入老年代。

如果在survivor空间中相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。

三,内存监控与故障处理工具(了解,写的很简略)

image
JVM Process Status Tool
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类的名称,以及这些进程的本地虚拟机的唯一ID(LVMID)

JVM Statistics Monitoring Tool
用于监视虚拟机各种运行状态信息,显示本地或者远程虚拟机进程中的类装载,内存,垃圾收集,JIT编译等运行数据。用于运行期间定位虚拟机性能问题。

Configuration Info for java
实时的查看和调整虚拟机的各项参数

Memory Map for java
用于生成java堆转储快照(一般叫做heapdump或者dump文件),查询finalize执行队列,java堆和永久代的详细信息。

JVM Heap Analysis Tool
用于分析dump文件,一般不用

Stack Trace for java
用于生成当前虚拟机的线程快照(Threaddump,javacore文件),线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。主要是为了定位线程出现长时间停顿的原因。



这篇关于JVM自动内存管理机制的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程