JVM内存管理机制

java内存区域与内存溢出异常

在这里插入图片描述

  • 程序计数器
  1. 程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选择下一条需要执行的字节码指令。
  2. 由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任何时刻,一个处理器都只会执行一条线程中的指令。为了线程切换后能回到正确的执行位置,每条线程都需要有一个独立的线程计数器,独立存储,我们称这类内存区域为“线程私有”的内存。
  3. 如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,这个计数器值则为空。
  • java虚拟机栈
  1. 和程序计数器一样,java虚拟机栈也是线程私有的,他的生命周期与线程相同。
  2. 它描述的是java方法执行的内存模型,每个方法在执行的的同时都会创建一个栈帧,用于存储局部变量表(含有基本数据类型和引用数据类型),操作数帧,每一个方法从调用至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出站的过程。
  3. 局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
  4. 在java虚拟机规范中,对这个区域规定了两种异常状况。如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常。
  • 本地方法栈
    与虚拟机栈所发挥的作用是相似的,区别是虚拟机栈执行的java方法(字节码)服务,而本地方法栈执行的为虚拟机使用到的Native方法服务。
  1. java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。在虚拟机启动时创建,此区域的唯一目的就是存放对象实例和数组。
  2. java堆是垃圾收集器管理的主要区域。因此,很多年时候称之为GC堆,
  3. 根据java虚拟机规范的规定:java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配。并且堆也无法再扩展,则会抛出OutOfMemoryError异常。
  • 方法区
  1. 也是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量。静态变量,即编译器编译后的代码数据,
  2. 运行时常量池是方法区的一部分,具有动态性,java语言并不要求一定只有编译期才能产生,运行期间也可能将新的常量放入池中。
  • 直接内存
  1. 它并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中的内存区域,但是却被频繁的使用
  2. JDK1.4中新加入NIO类,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这个内存的引用进行操作。
  3. 既然是内存,虽然不受java堆大小的限制,肯定受本机总内存大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,经常忽略直接内存,使得各个内存区域综合大于物理内存限制,从而导致动态扩展时出现OutOfMemberError异常。

HotSpot虚拟机对象探索

  • 对象的创建
  1. 当虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析,初始化。如果没有,那必须先执行相应的类加载过程。
  2. 为新生对象分配内存,所需大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。(指针碰撞:java堆中内存是绝对规整的,使用过的在一边,空闲的在另一边,中间放一个指针作为分界点的指示器,分配内存就是移动分界点。空闲列表:java堆中的内存并不是规整的,使用的和空闲的相互交错,虚拟机就必须维护一个列表,记录那些内存块是可以使用的。)选择哪种分配方式由所采用的垃圾收集器是否带有压缩整理功能决定。(带Compact过程的收集器使用指针碰撞,基于Mark-Sweep算法的手机器时,通常采用空闲列表)
  3. 还有一个需要考虑的问题,对象的创建在虚拟机中是非常频繁的行为,虽然仅仅是修改一个指针所只想的位置,在并发情况下也并不是线程安全的。解决方案一:对分配内存空间的动作进行同步处理(实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性)。解决方案二:把内存分配的动作按照线程划分在不同的空间之中进行。
  4. 执行init方法

对象的内存布局

  • 对象在内存中存储的布局可以分为3块区域:对象头,实例数据和对齐填充。
  1. HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码等。另一部分是指针类型,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  2. 实例数据部分是对象真正存储的有效数据,也是代码中所定义的各种类型的字段内容,无论是父类继承下来的,还是在子类中定义的。
  3. 对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。

对象的访问定位

  • 目前主流的访问方式有使用句柄和直接指针两种
  1. 如果使用句柄访问,那么java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。优点: reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集器移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而refererce本身并不需要修改
  2. 如果使用直接指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。优点:速度更快,它节省了一次指针定位的时间开销。

java堆溢出

java堆用来存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容纳限制后就会产生内存溢出异常。

方法区和运行时常量池溢出

由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行,String.intern()是一个native方法,他的作用是:如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到chang.iang池中,并且返回此String对象的引用。

垃圾收集器与内存分配策略

java内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不紊的执行者着入栈和出站操作,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知得,因此这几个区域的内存分配和回收都具备确定性,因为方法结束或者线程结束时,内存自然就跟随着回收了,而java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道创建那些对象,这部分内存的分配和回收都是动态的。

计数算法

给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加1;当饮用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,效率高,
缺点:不能解决对象之间相互循环引用的问题
应用案例:微软公司的COM(Component Object Model)技术,ActionScript 3的FlashPlayer.

可达性分析算法

  • 在主流的商用程序语言(java,c#等)的主流实现中,都是通过可达性分析来判断对象是否存活的。
  • 基本思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的。
  • 在java语言中,可作为GC Roots的对象包括下面几种:
    虚拟机栈中引用的对象。
    方法区中类静态属性引用的对象
    方法区中常量引用的对象
    本地方法栈中JNI引用的对象。

回收方法区

java虚拟机规范中说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”一般比较低

垃圾收集算法

标记-清除算法

  • 优点:简单
  • 缺点:
    效率问题:标记和清除两个过程的效率都不高
    空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾手机动作。

复制算法(为了解决效率问题)

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都对整个半区进行内存回收。内存分配时也就不用考虑内存碎片等复杂情况。只要移动堆顶指针,按顺序分配内存即可。实现简单,运行效率高。
缺点:将内存缩小为原来的一般,代价太高。并且在对象存活率较高时,就要进行较多的复制操作,效率将会变低。

标记-整理算法

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

分代收集算法

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

垃圾收集器

java虚拟机规范中对垃圾收集器如何实现并没有任何规定,因此不同厂商,不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别。(基于JDK1.7 Update 14之后的HotSpot虚拟机)

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的位置,则表示它是属于新生代收集器还是属于老年代收集器。

Serial 收集器(新生代收集器)

这是一个单线程收集器,但是它的“单线程”的意义并不仅仅说明它只会使用一个cpu或一条收集线程去完成垃圾收集工作,更重要的是在他进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
(Serial-Parallel-Concurrent Mark Sweep-Garbage First)虽然它存在这些缺点,但是实际上到现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。因为它简单和高效(相对其他单线程的收集器)

ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数,收集算法,Stop The World,对象分配规则,回收策略等都与Serial收集器完全一样。但是它是许多运行在Server模式下的虚拟机中首选的其中一个与性能无关的但是很重要的一个原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。(ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果)

CMS收集器(老年代收集器)

在JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可以认为有划时代意义的垃圾收集器—CMS收集器(Concurrent Mark Sweep),这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器。,它第一次实现了让垃圾收集线程与用户线程同时工作。但是却不能与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作。只能使用上面两种中的一个。

Parallel Scavenge 收集器(新生代收集器)

它也是使用复制算法的收集器,又是并行的多线程收集器,它的特点在于它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码的时间+垃圾收集时间)
它主要使用两个参数来用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的参数(大于0的毫秒数)和直接设置吞吐量大小的参数(大于0且小于100的整数)。

Serial Old收集器(老年代)

Serial Old 是Serial收集器的老年代版本,它同样是单线程收集器,使用“标记-整理”算法这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

Parallel Old收集器(老年代)

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器

  • CMS收集器是一种以获取最短回收停顿时间为目标的收集器,适应于互联网站或者B/S系统的服务器端上,响应快,停顿时间短,给用户好的体验。
    CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:初始标记,并发标记,重复标记,并发清除。,其中,初始标记,重新标记这两个步骤仍然需要“Stop The World”.初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变化的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  • 缺点:CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用一部分线程而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4
  • CMS收集器无法处理浮动垃圾,可能出现“concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记程序之后,CMS无法在当次收集中处理掉它们,只好停留下一次GC时再处理掉。这一部分垃圾就称为“浮动垃圾”
  • CMS是一款基于“标记-清除”算法实现的收集器,在收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

G1收集器

G1(Garbage-First)收集器是当今 收集器技术发展的最前沿成果之一,G1是一款面向服务端应用的垃圾收集器。目标是替换掉CMS收集器。

  • 特点
    并行与并发
    分代收集
    空间整合
    可预测的停顿
  • 在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时java堆的内存布局就与其他收集器有很大差别,他将整个java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但是他们已经不再是物理隔离,他们都是一部分Region(不需要连续)的集合。

内存分配

  • 对象的分配
    多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC 。
  • 大对象直接进入老年代
    所谓大对象,需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串及数组。
    虚拟机提供了-XX:PretenureSizeThreshold参数,令大于这个设置值得对象直接在老年代分配,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
  • 长期存活的对象将进入老年代。

内存溢出以及解决方案

  • Java 堆空间
    造成原因:
无法在 Java 堆中分配对象
吞吐量增加
应用程序无意中保存了对象引用,对象无法被 GC 回收
应用程序过度使用 finalizer。finalizer 对象不能被 GC 立刻回收。finalizer 由结束队列服务的守护线程调用,有时 finalizer 线程的处理能力无法跟上结束队列的增长

解决方案
使用 -Xmx 增加堆大小
修复应用程序中的内存泄漏

  • GC 开销超过限制
    造成原因
Java 进程98%的时间在进行垃圾回收,恢复了不到2%的堆空间,最后连续5个(编译时常量)垃圾回收一直如此。

解决方案:
使用 -Xmx 增加堆大小
使用 -XX:-UseGCOverheadLimit 取消 GC 开销限制
修复应用程序中的内存泄漏

  • 无法新建本机线程
    造成原因
内存不足,无法创建新线程。由于线程在本机内存中创建,报告这个错误表明本机内存空间不足

解决方案

为机器分配更多的内存
减少 Java 堆空间
修复应用程序中的线程泄漏。
增加操作系统级别的限制
ulimit -a
用户进程数增大 (-u) 1800
使用 -Xss 减小线程堆栈大小

JVM 加载 Class 文件的原理机制

Java 类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到 jvm 中,至于 其他类,则在需要的时候才加载。这当然就是为了节省内存开销

  • Java 的类加载器有三个
    在这里插入图片描述
    1.装载:查找和导入 class 文件;
    2.连接:
    在这里插入图片描述
    3:初始化:初始化静态变量,静态代码块

分布式垃圾回收

RMI 子系统实现基于 引用计数 的“分布式垃圾回收”(DGC),以便为远程服务器对象提供自 动内存管理设施。
当客户机创建(序列化)远程引用时,会在服务器端 DGC 上调用 dirty()。当客户机完成远 程引用后,它会调用对应的 clean()方法。 针对远程对象的引用由持有该引用的客户机租用一段时间。租期从收到 dirty()调用开始。 在此类租约到期之前,客户机必须通过对远程引用额外调用 dirty()来更新租约。如果客户 机不在租约到期前进行续签,那么分布式垃圾收集器会假设客户机不再引用远程对象。

java对象什么时候会被回收

可以使用引用计数器或者可达性分析两种方法

  • 引用计数器:
    当引用计数器为零的时候,表明没用引用再指向该对象,但是引用计数器不能解决循环引用的情况;
  • 可达性分析:
  1. 当不能从GC Root寻找一条路径到达该对象时, 将进行第一次标记。
  2. 第一次标记后检查对象是否重写了finalize() 和是否已经被调用了finalize()方法。若没有重写finalize()方法或已经被调用,则进行回收。
  3. 在已经重写finalize()方法且未调用的情况下,将对象加入一个F-Queue 的队列中,稍后进行第二次检查。
    4.在第二次标记之前,对象如果执行finalize()方法并完成自救,对象则不会被回收。否则完成第二次标记,进行回收。值得注意的是finalize()方法并不可靠

简述Minor GC、Major GC

  • Minor GC:清理年轻代空间(包括 Eden 和 Survivor 区域),释放在Eden中所有不活跃的对象,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。Survivor区被用来作为Eden及老年代的中间交换区域,当老年代空间足够时,Survivor区的对象会被移到老年代,否则会被保留在Survivor区。
  • Major GC:清理老年代空间,当老年代空间不够时,JVM会在老年代进行major gc。

java 中垃圾收集的方法

  1. 标记-清除:
    思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不
    高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在
    分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。
  2. 复制算法:
    为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只
    使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然
    后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式, 内存的代价太高,每次基本上都要浪费一般的内存。
    于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为
    8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。
    每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然
    后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对
    象通过分配担保机制复制到老年代中。(java 堆又分为新生代和老年代)
  3. 标记-整理
    该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高
    时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回
    收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
  4. 分代收集
    现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生
    代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那
    么这时就采用 复制 算法。老年代里的对象存活率较高,没有额外的空间进行分配担
    保,所以可以使用 标记-整理 或者 标记-清除 。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值