深入理解jvm虚拟机(一)

一、运行时数据区域

java虚拟机在执行java程序的过程中会把它管理的内存区域划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建与销毁的时间。
深入理解jvm虚拟机(一)_第1张图片

程序计数器

  • 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
  • 字节码解释工具就是通过改变这个计数器的值来选取下一条需要执行的字节码指令的。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

java虚拟机的多线程是通过线程轮流切换并分配处理器执行的时间的方式来实现的,在任何一个确定的时刻,一个处理器(或一个内核)都只会执行一条线程中的指令。

  • 为了使线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,线程间互不影响,独立存储,这类内存是线程私有的内存。
  • 如果线程正在执行一个java方法,这个计时器记录的是正在执行的虚拟机指令的内存;如果正在执行的是native方法,这个计数器的则为空。

java虚拟机栈

  • 同样属于线程私有,生命周期与线程相同,虚拟机栈描述的是java方法执行的内存模型:每个方法运行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程都对应一个栈帧入栈到出栈的过程。

经常有人把java内存分为堆内存和栈内存,但实际上的内存划分远远比这个复杂。
所指的“栈”就是虚拟机栈,或者是说虚拟机栈中的局部变量表部分。

  • 局部变量表:存放编译期可知的各种基本数据类型、对象引用和returnAdress类型(指向一条字节码指令的地址)。其中64位长度的long和double会占用2个局部空间(Slot),其余的数据类型只占用一个。
  • 在方法运行期间不会改变局部变量表的大小。

本地方法栈

  • 和虚拟机栈类似,区别是虚拟机栈执行java方法,而本地方法栈则为虚拟机使用到的native方法服务。

java堆

  • 被所有线程共享,在虚拟机启动时创建,此内存区域唯一目的就是存放对象实例,也是垃圾收集器管理的主要区域(GC堆)。

方法区

  • 被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 运行时常量池
    • 运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分方法将在类加载后进入方法区的运行时常量池中存放。

二、对象的概念

对象的创建

  • 检查类是否已被加载解析和初始化过。如果没有则先执行类加载工作。
  • 为新生对象分配内存(指针碰撞,空闲列表,线程安全性)
  • 为分配到的内存初始化为0值
  • 对对象进行必要的设置(是哪个类的实例?如何才能找到类的元数据数据?对象的哈希码?对象的GC分代年龄?)
  • 对虚拟机而言一个新的对象已经产生了,但对于java程序,则还需要执行方法把对象按照程序员的意愿初始化

Hotspot虚拟机中对象的内存布局可以分为3块区域:对象头(header)、实例数据(Instance Data)和对齐填充。下图是Hotspot虚拟机对象头Mark Word。
深入理解jvm虚拟机(一)_第2张图片

对象的内存存储布局

  • 对象头
  • 实例数据:对象真正存储的有效信息
  • 对齐填充:占位符,对象的大小必须是8的整数倍

对象的访问定位

java程序通过栈上的reference数据来操作对上的具体对象

  • 句柄访问:java堆中会划分出一块内存来作为句柄池,reference存储对象的句柄地址,而句柄包含了对象实例数据与类型数据各自的具体的地址信息。优点是对象被移动的时候只会改变句柄的实例数据指针,而reference本身不会被修改。
    深入理解jvm虚拟机(一)_第3张图片
  • 直接指针:reference直接存储对象地址。优点是速度更快,它节省了一次指针定位的时间开销。Hotspot使用直接指针访问对象。
    深入理解jvm虚拟机(一)_第4张图片

三、垃圾收集器和内存分配策略

当需要排查各种内存溢出、内存泄露的问题时,当垃圾收集成为系统达到更高并发量的瓶颈的时候,我们就需要对这些自动化技术实施必要的监控和调节

对象是否已经死亡?

  • 引用计数算法:给对象添加一个引用计数器,每当一个地方引用它时计数器就+1;当引用失效的时候计数器就-1;任何时候如果计数器为0则对象就是不可再使用的。缺点是很难处理对象的循环引用问题。
  • 可达性分析算法:通过一系列的成为“GC Roots”的对象作为起始点,从这些结点开始向下搜索,搜索走过的路径成为“引用链(Reference Chain)”。如果从“GC Roots”开始,无法到达这个对象就说这个对象是不可用的。
    深入理解jvm虚拟机(一)_第5张图片

对象的引用等级

  • 强引用:指普遍存在的Object obj = new Object()这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:用来描述一些还有用但并非必须的对象,在系统将要发生内存溢出异常之前将会把这些对象列进回收范围中进行二次回收。
  • 弱引用:用来描述非必须对象,强度比软引用低,被弱引用关联的对象只能活到下一次垃圾收集发生之前,无论内存是否足够都会回收掉只被弱引用关联的对象。
  • 虚引用:最弱的一种引用关系。唯一目的就是在这个对象被收集器回收的时候收到一个系统通知。

对象生存还是死亡

任何一个对象的finalize方法只会被调用一次

  • 要真正宣告一个对象死亡,至少要经过两次标记过程。
  • 如果对象在进行可达性分析后没有发现与GC Roots相连的引用链,那么它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。(对象没有覆盖finallize方法或finalize方法已被虚拟机调用过,都会被视为没必要执行)
  • 当有必要执行finalize方法的时候,对象会被放到F-Queue队列之中,并在稍后一个由虚拟机自动建立的、低优先级的Finalize线程去执行它。(异步执行,防止死循环导致永久等待)
  • finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记。此时如果对象和任何一个引用链上的对象建立关联就不会被收集!
  • 否则对象被收集。

永久代的垃圾回收

  • 废弃常量:没有任何一个地方引用常量池中的某个常量,该常量即为废弃常量
  • 无用的类
    • 该类所有的实例都已经被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法

垃圾收集算法

  • 标记-清除算法:先标记需要清除的对象,再回收被标记的对象。缺点是效率不高和会产生大量内存碎片。
  • 复制算法:将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。优点是效率高,缺点是代价大,完全将内存缩减为原先的一半!
  • 标记-整理算法:标记过程一致,但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  • 分代收集算法:依据对象的存活周期的不同将内存划分为几块。一般是把java堆分成新生代和老年代。新生代中每次垃圾收集都有大批对象死去,那就选用复制算法;老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法进行回收。

HotSpot虚拟机的算法实现

  • 从GC Roots节点找引用链的操作中,必然消耗大量时间。而且枚举根节点时为防止分析过程中对象引用关系还在不断变化的情况,要进行停顿(指在整个分析期间整个执行系统看起来就像被冻结在某个时间点!)
  • 目前主流jvm虚拟机使用的都是准确式GC,执行系统停顿时使用一组OopMap的数据结构得知那些地方存放着对象引用,提高枚举的效率。
  • OopMap缺点:如果为每一个指令生成对应的OopMap那么GC的空间成本会很高。
  • SafePoint安全点:程序执行时只有到达安全点才能够暂停。依据“是否具有让程序长时间执行的特征”选定。
  • 主动式中断和抢先式中断。
  • safeRegion安全区域:指在一段代码片段之中,引用关系不会发生变化,因此在这个区域中任意地方开始GC都是安全的。

垃圾收集器
深入理解jvm虚拟机(一)_第6张图片

名称 线程种类 描述 优点 缺点 指令
Serial收集器 单线程 新生代 最基本的垃圾收集器 十分高效,有最高的单线程收集效率,适用于Client模式 多线程并发运行,当需要垃圾回收的时候,所有线程都被阻塞,只有垃圾回收线程在运行,直到回收完毕,其他线程才继续运行。 -XX:SurvivorRatio、-XX:PretenureSizeThreshold、 -XX:HandlePromotionFailure 等
ParNew收集器 多线程 新生代 由于新生代中只有它可以和CMS收集器配合工作,是许多运行在Server模式下的虚拟机中首选的新生代收集器。 可以和CMS配合工作,随着CPU数量的增加一定程度上优于Serial收集器 在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器 -XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等
Parralel Scavenge收集器 多线程 新生代 使用复制算法,并行的多线程新生代收集器。特点是关注点与其他收集器不同,目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间))。虚拟机运行了100分钟,垃圾收集花了1分钟那么吞吐量就是99% 关注于吞吐量的控制。高吞吐量则可以高效利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务 -XX:MaxGCPauseMillis、-XX:GCTimeRatio、-XX:+UseAdaptiveSizePolicy
Serial Old收集器 单线程 老年代 使用标记-整理算法,主要给Client模式下的虚拟机使用。如果在Server模式下,可以和Parallel Scavenge收集器搭配使用或作为CMS收集器的后备预案,也可以在并发收集发送Concurrent Mode Failere时使用。 -XX:+UseParallelOldGC
Parrallel Old收集器 多线程 老年代 使用标记-整理算法,注重吞吐量及CPU资源敏感的场合都可以优先考虑Parallel Scavenge加Parallel Old收集器。
CMS收集器 与用户线程一起并发执行 老年代 基于标记-清除算法,CMS收集器是一种以获取最短回收停顿时间为目标的收集器。适合重视服务的响应速度,希望系统停顿时间最短以给用户带来较好的体验的情况。整个运作过程分为4个步骤:初始标记,并发标记,重新标记,并发清除。其中1、3步仍需“stop the world” 并发收集、低停顿,适用于对响应速度要求较高的情况 1.对CPU资源敏感 2.无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次full GC的产生 3.基于标记-清除算法,可能产生大量空间碎片
G1收集器 新生代和老年代不再是物理隔离了,它们都是一部分region的集合 是一款面向服务应用的垃圾收集器,将内存化整为零为一个个Region,收集时优先收集价值最大的Region提高效率。运作大致分为:初始标记、并发标记、最终标记和筛选回收。 可以并行与并发,进行分代收集,进行空间整合以及建立可预测的停顿时间模型 由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用。标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽。

并行Parrallel:多条垃圾收集线程并行工作,但此时用户线程依然处于等待状态
并发Concurrent:用户线程和垃圾收集线程同时执行(不一定并行,可能交替执行),用户程序在继续运行而垃圾收集程序运行在另一个CPU上。

内存分配与回收策略
java中提倡的自动内存管理最终可以归结为为对象分配内存和回收分配给对象的内存。对象的内存分配大方向上讲就是在堆上分配,对象主要分配在新生代的eden区上,如果启动了本地线程分配缓冲则优先在TLAB上分配。少数情况下也可能会直接分配在老年代,细节取决于收集器的使用和参数的设置。

  • 对象优先在Eden中分配:当Eden区没有足够空间进行分配的时候,虚拟机将发起一次Minor GC。
  • 大对象直接进入老年代:大对象指需要大量连续内存空间的Java对象。
  • 长期存活的对象将进入老年代:虚拟机为分辨对象年龄定义了一个对象年龄计数器。如果对象出生并且经过第一次Minor GC后依然存活,并且能被Survivor容纳的话将被移动到Survivor空间中,并将对象年龄设置为1.对象每熬过一次Minor GC就增加1岁。当对象年龄大到一定程度的时候(默认15)就会被晋升至老年代。
  • 动态年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代。
  • 空间分配担保:在发送Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC就确保是安全的。如果不安全,则虚拟机会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC;如果小于或者参数设置不允许冒险则进行一次Full GC。

你可能感兴趣的:(java基础)