Java内存管理机制

内存管理的五个存储区
1.堆:这里是由程序员控制的分配和回收的,当程序员忘记回收时,程序结束时系统会自动进行回收。

2.栈:这里的内存空间是由编译器分配的,里面的变量通常是局部变量,函数参数,指针等。

3.自由存储区:是那些由malloc等分配的内存块,和堆相似,不过它是由 free 来结束自己的生命。

4.全局/静态存储区:用来存储全局变量和静态变量

5.常量存储区:用来存储不能修改的常量

内存管理实际就是要求程序员对堆区的内存进行管理

Java的内存管理机制就是对象的分配和释放

分配:内存的分配是程序完成的,程序员通过new 关键字为对象申请内存空间(基本数据类型除外),对象都是在堆(Heap)中分配空间的。

释放:对象的释放是用GC(垃圾回收机制)来决定和执行的,判断是否可以回收的条件就是要监控对象是否引用或被引用等运行状况。当这个对象没有任何指针指向它,就会被GC进行自动回收,这样有效避免了内存泄漏。

内存管理最根本的还是引用计数机制,只有在引用计数为零的时候,内存就会被系统进行回收/释放 。

在Java中,内存的分配是由程序完成的,而内存的释放是由GC完成的;这种收支两条线的方法简化了程序员的工作;但同时它也加重了JVM的工作;这是Java程序运行速度较慢的原因之一。

GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

监视对象状态是为了更加准确、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好的理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引用对象。另外,每个线程对象可以作为一个图的起始顶点,例如,大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象与这个根顶点不可达,那么我们认为这个对象不再被引用,可以被GC回收。

GC的流程
找出堆中活着的对象
释放死对象占用的资源
定期调整活对象的位置

GC算法
Mark-Sweep 标记-清除算法
Mark-Sweep-Compact 标记-整理算法
Copying Collector 复制算法

Java内存区域

在这里插入图片描述

Java运行时内存区

1.程序计数区:为线程编号

2.虚拟机栈:Java方法执行的内存模型,每个方法被执行时都会创建一个栈帧

3.本地方法栈:Native方法服务

4.堆区:存放对象实例,几乎所有的对象实例以及其属性都在这里分配内存

5.方法区:存储已经被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。包含运行时的常量池
在这里插入图片描述
其中,对于这各个部分有一些是线程私有的,其他则是线程共享的。

线程私有的如下:
程序计数器当前线程所执行的字节码的行号指示器

Java虚拟机栈Java方法执行的内存模型,每个方法被执行时都会创建一个栈帧,存储局部变量表、操作栈、动态链接、方法出口等信息。
每个线程都有自己独立的栈空间
线程栈只存基本类型和对象地址
方法中局部变量在线程空间中

本地方法栈Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。

线程共享的如下:
Java堆存放对象实例,几乎所有的对象实例以及其属性都在这里分配内存。

方法区存储已经被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。

运行时常量池方法区的一部分。用于存放编译期生成的各种字面量和符号引用。

直接内存NIO、Native函数直接分配的堆外内存。DirectBuffer引用也会使用此部分内存。

对象访问
Java是面向对象的一种编程语言,那么如何通过引用来访问对象呢?一般有两种方式:
1.通过句柄访问
在这里插入图片描述
2.直接指针(此种方式也是HotSpot虚拟机采用的方式)
在这里插入图片描述

内存溢出

在JVM申请内存的过程中,会遇到无法申请到足够内存,从而导致内存溢出的情况。一般有以下几种情况:
1.虚拟机栈和本地方法栈溢出
StackOverflowError: 线程请求的栈深度大于虚拟机所允许的最大深度(循环递归)
OutOfMemoryError: 虚拟机在扩展栈是无法申请到足够的内存空间,一般可以通过不停地创建线程引起此种情况

2.Java堆溢出:当创建大量对象并且对象生命周期都很长的情况下,会引发OutOfMemoryError

3.运行时常量区溢出:OutOfMemoryError:PermGen space,这里一个典型的例子就是String的intern方法,当大量字符串使用intern时,会触发此内存溢出

4.方法区溢出:方法区存放Class等元数据信息,如果产生大量的类(使用cglib),那么就会引发此内存溢出,OutOfMemoryError:PermGen space,在使用Hibernate等框架时会容易引起此种情况。

垃圾回收器(GC)

GC的流程
1.找出堆中活着的对象

2.释放死对象占用的资源

3.定期调整活对象的位置

三个GC算法
1.标记清除算法:标记所有从根节点开始的可达对象,清除未被标记的对象

2.标记整理算法:标记从根节点出发,把所有标记的压缩到内存的一边剩下空间的对象清除

3.复制算法:原来的内存空间分为2份,每次只使用一块,当清理的时候把存活对象复制到另一块内存块中,清除原来的那一块内存

复制算法适合于新生代 ,标记整理算法适合老年代。所以产生了分代收集算法:根据对象的存活周期的不同将内存分为几块,短命新生代,用复制算法,长命老年代用标记整理算法。

新生代到老年代的执行流程
新生代分为三个区域:一个 Eden和两个Survivor,比例为8:1:1;后面1:1是为什么等新生代执行流程时你就懂了,那为什么8:1?新对象除了特别大的,剩下的是先进Eden,所以如果Eden不大,怎么能放开新对象呢?

在Eden中的对象满了之后就要移动到Survivor,这时是第一次新生代GC,Eden中的对象移动到Survivor的内存块是空的,没有那个对象了。

这就是复制算法的好处,如果移动到有对象的Survivor就会造成内存的空间碎片化,是非常可怕的,尤其是Survivor不大的情况下,Eden中的对象和另一个Survivor中的对象一起移动到空的Survivor可以进行空间整理,这样可以压缩空间,减少内存空间的碎片化。

在新生代进行15此MinorGC就会把年龄到15的放到老年代

老年代就用标记整理算法,当然也不是完全是标记整理算法,因为有的老年代垃圾回收器是用的标记清除算法。

垃圾回收器
年轻代的垃圾回收器有三种:
Serial :单线程的,
ParNew :Serial 多线程模式
ParallelScavenge :采用Copy算法,主要关注的是达到可控制的吞吐量,“吞吐量优先”,server默认新手回收器

老年代的垃圾回收器有四种:
Serial Old:Serial 老年代版本
Parallel Old:Server模式的默认老年代收集器,关注点在吞吐量以及CPU资源敏感的场合使用
CMS:使用标记清除,以获取最短回收停顿时间为目标的收集器,重视响应速度,希望系统停顿时间最短,对CPU资源非常敏感,越多越好
G1:java7上线,主要使用标记-整理算法,不会产生碎片,将整个Java堆划分为多个大小相等的独立区域,对内存分段比较要求高,用于替代CMS

理论基础
在通常情况下,我们掌握java的内存管理就是为了应对网站/服务访问慢,慢的原因一般有以下几点:
内存:垃圾收集占用cpu;放入了太多数据,造成内存泄露线程死锁
I/O速度太慢
依赖的其他服务响应太慢
复杂的业务逻辑或者算法造成响应的缓慢

其中,垃圾收集对性能的影响一般有以下几个:
内存泄露
程序暂停
程序吞吐量显著下降
响应时间变慢

垃圾收集的一些基本概念
Concurrent Collector:收集的同时可运行其他的工作进程
Parallel Collector: 使用多CPU进行垃圾收集
Stop-the-word(STW):收集时必须暂停其他所有的工作进程
Sticky-reference-count:对于使用“引用计数”(reference count)算法的GC,如果对象的计数器溢出,则起不到标记某个对象是垃圾的作用了,这种错误称为sticky-reference-count problem,通常可以增加计数器的bit数来减少出现这个问题的几率,但是那样会占用更多空间。一般如果GC算法能迅速清理完对象,也不容易出现这个问题。
Mutator:mutate的中文是变异,在GC中即是指一种JVM程序,专门更新对象的状态的,也就是让对象“变异”成为另一种类型,比如变为垃圾。
On-the-fly:用来描述某个GC的类型:on-the-fly reference count garbage collector。此GC不用标记而是通过引用计数来识别垃圾。
Generational gc:这是一种相对于传统的“标记-清理”技术来说,比较先进的gc,特点是把对象分成不同的generation,即分成几代人,有年轻的,有年老的。这类gc主要是利用计算机程序的一个特点,即“越年轻的对象越容易死亡”,也就是存活的越久的对象越有机会存活下去(姜是老的辣)。

吞吐量与响应时间
牵扯到垃圾收集,还需要搞清楚吞吐量与响应时间的含义
吞吐量是对单位时间内完成的工作量的量度。 如:每分钟的 Web 服务器请求数量
响应时间是提交请求和返回该请求的响应之间使用的时间。 如:访问Web页面花费的时间

吞吐量与访问时间的关系很复杂,有时可能以响应时间为代价而得到较高的吞吐量,而有时候又要以吞吐量为代价得到较好的响应时间。而在其他情况下,一个单独的更改可能对两者都有提高。通常,平均响应时间越短,系统吞吐量越大;平均响应时间越长,系统吞吐量越小; 但是,系统吞吐量越大, 未必平均响应时间越短;因为在某些情况(例如,不增加任何硬件配置)吞吐量的增大,有时会把平均响应时间作为牺牲,来换取一段时间处理更多的请求。

针对于Java的垃圾回收来说,不同的垃圾回收器会不同程度地影响这两个指标。例如:并行的垃圾收集器,其保证的是吞吐量,会在一定程度上牺牲响应时间。而并发的收集器,则主要保证的是请求的响应时间。

GC的流程
找出堆中活着的对象
释放死对象占用的资源
定期调整活对象的位置

GC算法
Mark-Sweep 标记-清除算法
Mark-Sweep-Compact 标记-整理算法
Copying Collector 复制算法
Mark-标记从”GC roots”开始扫描(这里的roots包括线程栈、静态常量等),给能够沿着roots到达的对象标记为”live”,最终所有能够到达的对象都被标记为”live”,而无法到达的对象则为”dead”。效率和存活对象的数量是线性相关的。
Sweep-清除扫描堆,定位到所有”dead”对象,并清理掉。效率和堆的大小是线性相关的。
Compact-压缩对于对象的清除,会产生一些内存碎片,这时候就需要对这些内存进行压缩、整理。包括:relocate(将存货的对象移动到一起,从而释放出连续的可用内存)、remap(收集所有的对象引用指向新的对象地址)。效率和存活对象的数量是线性相关的。
Copy-复制将内存分为”from”和”to”两个区域,垃圾回收时,将from区域的存活对象整体复制到to区域中。效率和存活对象的数量是线性相关的。

其中,Copy对比Mark-sweep
1.内存消耗:copy需要两倍的最大live set内存;mark-sweep则只需要一倍。
2.效率上:copy与live set成线性相关,效率高;mark-sweep则与堆大小线性相关,效率较低。

分代收集
分代收集是目前比较先进的垃圾回收方案。有以下几个相关理论
分代假设:大部分对象的寿命很短,“朝生夕死”,重点放在对年青代对象的收集,而且年青代通常只占整个空间的一小部分。
把年青代里活的很长的对象移动到老年代。
只有当老年代满了才去收集。
收集效率明显比不分代高。

HotSpot虚拟机的分代收集,分为一个Eden区、两个Survivor去以及Old Generation/Tenured区,其中Eden以及Survivor共同组成New Generatiton/Young space。通常将对New Generation进行的回收称为Minor GC;对Old Generation进行的回收称为Major GC,但由于Major GC除并发GC外均需对整个堆以及Permanent Generation进行扫描和回收,因此又称为Full GC。
在这里插入图片描述
Eden区是分配对象的区域。
Survivor是minor/younger gc后存储存活对象的区域。
Tenured区域存储长时间存活的对象。

分代收集中典型的垃圾收集算法组合描述如下:
年青代通常使用Copy算法收集,会stop the world
老年代收集一般采用Mark-sweep-compact, 有可能会stop the world,也可以是concurrent或者部分concurrent。

那么何时进行Minor GC、何时进行Major GC? 一般的过程如下:
对象在Eden Space完成内存分配
当Eden Space满了,再创建对象,会因为申请不到空间,触发Minor GC,进行New(Eden + S0 或 Eden S1) Generation进行垃圾回收
Minor GC时,Eden Space不能被回收的对象被放入到空的Survivor(S0或S1,Eden肯定会被清空),另一个Survivor里不能被GC回收的对象也会被放入这个Survivor,始终保证一个Survivor是空的
在Step3时,如果发现Survivor区满了,则这些对象被copy到old区,或者Survivor并没有满,但是有些对象已经足够Old,也被放入Old Space。
当Old Space被放满之后,进行Full GC

但这个具体还要看JVM是采用的哪种GC方案。
New Generation的GC有以下三种:
Serial
ParallelScavenge
ParNew

对于上述三种GC方案均是在Eden Space分配不下时,触发GC。
Old Generation的GC有以下四种:
Serial Old
Parallel
CMS

对于Serial Old, Parallel Old而言触发机制为
Old Generation空间不足
Permanent Generation空间不足
Minor GC时的悲观策略
Minor GC后在Eden上分配内存仍然失败
执行Heap Dump时
外部调用System.gc,可通过-XX:+DisableExplicitGC来禁止

对于CMS而言触发机制为:
当Old Generation空间使用到一定比率时触发;HopSpot V1.6中默认是92%,可通过PrintCMSInitiationStatistics(此参数在V1.5中不能用)来查看这个值到底是多少;可通过CMSInitiatingOccupancyFaction来强制指定,默认值并不是复制在这个值上,是根据如下公式计算出来的:((100 -MinHeapFreeRatio) +(double)(CMSTriggerRatio* MinHeapFreeRatio) / 100.0)/ 100.0;MinHeapFreeRatio默认值:40 CMSTriggerRatio默认值:80

当Permanent Generation采用CMS收集且空间使用到一定比率触发;Permanent Generation采用CMS收集需设置:-XX:+CMSClassUnloadingEnabled Hotspot V1.6中默认为92%;可通过CMSInitiatingPermOccupancyFraction来强制指定,同样,它是根据如下公式计算出来的:((100 -MinHeapFreeRatio) +(double)(CMSTriggerPermRatio* MinHeapFreeRatio) / 100.0)/ 100.0;MinHeapFreeRatio默认值:40 CMSTriggerPermRatio默认值:80

Hotspot根据成本计算决定是否需要执行CMS GC;可通过-XX:+UseCmsInitiatingOccupancyOnly来去掉这个动态执行的策略。
外部调用System.gc,且设置了ExplicitGCIInvokesConcurrent;需要注意,在hotspot 6中,在这种情况下如果应用同时使用了NIO,可能会出现bug。

参考文章:
JAVA、OC的内存管理机制的本质
Java面试题精编版—内存管理
谈谈Java内存管理

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐