(总结)深入理解Java虚拟机之自动管理内存

1、Java内存区域与内存溢出异常

1-1、运行时数据区

  1. 程序计数器
    1. 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器;
    2. 每个线程独有的程序计数器;
    3. Java的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现的。在一个确定的时刻,一个处理器只会执行一个线程里的指令;
    4. 如果执行的是java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址,若是执行本地(Native)方法,计数器的值为空;
  2. Java虚拟机栈
    1. 线程私有,服务于虚拟机执行Java方法(字节码);
    2. Java方法执行的线程内存模型,每个方法运行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等。
    3. 每个方法被调用直到被执行完的过程,都对应着一个栈帧在虚拟机中从入栈到出栈的过程;
    4. 局部变量表(栈中的一部分)
      1. 存放了编译期可知的各种Java基本类型和对象引用(reference);
      2. 这些数据类型在局部变量表中的存储空间以局部变量槽(Slot),64位的long和double会占用两个槽,其余占用一个槽;
      3. 局部变量表所需的内存空间在编译期间完成分配
    5. StackOverflowError:线程请求栈深大于虚拟机所允许的栈深(栈越界);
    6. OutOfMemoryError:栈扩展无法申请足够的内存(内存不足);
  3. 本地方法栈
    1. 与虚拟机栈类似,本地方法栈是为虚拟机执行本地(Native)方法服务的;
    2. StackOverflowError和OutOfMemoryError
  4. Java堆
    1. 所有线程共享的一块内存区域;
    2. 虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例;
    3. 可以是固定大小,也可可扩展(通过-Xmx和-Mms设定);
    4. 堆无法扩展时,抛出 OutOfMemoryError 异常;
  5. 方法区
    1. 各个线程共享的内存区域;
    2. 用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数量;
    3. JDK1.8,完全废弃永久代的概念,改用本地内存实现的元空间(Meta-space);
    4. 这区域的内存回收主要是针对常量池的回收和对类型的卸载;
    5. 无法满足新的内存分配需求时,抛出OutOfMemoryError
  6. 运行时常量池
    1. 方法区的一部分,用于存放Class文件的常量池表(用于存放编译期生成的各种字面量与符号引用);
    2. 相对于Class文件常量池的另一个动态特征是具备动态性,运行期间也可将新的常量放入池中,参考String类的intren();
    3. 无法申请内存时,抛出 OutOfMemoryError 异常;
  7. 直接内存
    1. 它不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域;
    2. 例如:引入的NIO类,引入了一种基于Channel和Buffer的I/O方式,它可以使用Native函数直接分配堆外内存,通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,这可以在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

在这里插入图片描述

1-2、HotSpot虚拟机对象揭秘

以HotSpot虚拟机和最常见的内存区域Java堆为例

  1. 对象的创建
    1. 遇到new指令时,进行类加载检查(检测这个指令的参数是否能在常量池中定位到一个类的引用,并且检查这个符号引用代表的类是否已经被加载解析,初始化过);
    2. 虚拟机为新生对象分配内存(所需内存已知大小);
    3. 虚拟机将分配的内存空间都初始化为0(不包括对象头);
    4. Java虚拟机还要对对象进行必要的设置(对象是哪个类的实例,如何才能找到类的元数据);
  2. 对象的内存布局
    1. 在堆内存中的存储布局划分为对象头、实例数据、对齐填充。
    2. 对象头包含两类信息
      1. 存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志、线程持有的锁等 “Mark Word”)
      2. 类型指针,对象指向它的类型元数据的指针(虚拟机通过这个指针确定是哪个类的实例);
    3. 实例数据的存储顺序通过-XX:FieldsAllocationStyle 改变 ,+XX:CompactFields
    4. 对齐填充:对象起始地址必须是8字节的整数倍;
  3. 对象的访问定位
    1. Java程序会通过栈上的reference数据操作堆上的具体对象;
    2. 主流方式使用句柄和直接指针两种;
  4. 常见参数
    1. -XX:+HeapDumpOnOutOfMemoryError : 内存溢出时Dump出当前的内存堆转储快照;
    2. -XX:MaxMetaspaceSize \ MetaspaceSize :元空间最大\初始容量
    3. -XX:MinMetaspaceFreeRatio \ MinMetaspaceFreeRatio : 垃圾收集之后控制的元空间剩余容量的百分比;
    4. -XX:MaxDirectMemorySize : 直接内存的容量大小(默认Java堆的最大值一致);

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

2-1、对象是否死亡

  1. 引用计数算法
  2. 可达性分析算法(通过一系列GC Roots 的跟根对象作为起始节点集,根据引用关系向下搜索,当对象不可达时,证明此对象不可能再使用);
    固定可作为GC Roots的对象:
    1. 在虚拟机栈中引用的对象(譬如当前正在运行的方法所使用到的参数、局部变量、临时变量);
    2. 在方法区中类静态属性引用的变量;(譬如:Java类的引用类型静态变量);
    3. 在方法区中常量引用的变量;(譬如:Java类的字符串常量池里的引用);
    4. 在本地方法栈中JNI引用的对象;
    5. Java虚拟机内部的引用;
    6. 所有被同步锁(synchronized关键字)持有的对象;
    7. 反映Java虚拟机内部情况的JMXBean、JVMTI注册的回调,本地代码缓存
  3. 引用分类
    1. 强引用、
    2. 软引用:将要发送内存溢出前,会将这些对象进行二次回收;
    3. 弱引用:只能生存到下一次垃圾收集发生为止;
    4. 虚引用:
  4. 真正宣告一个对象死亡,最多会经历两次标记过程,对象在可达性分析之后发现没有与GC Roots相连接的引用链,第一次被标记,随后进行一次筛选,筛选的条件判断对象是否有必要执行finalize()方法。 任何一个对象的finalize()方法只执行一次。
  5. 回收方法区
    1. 回收方法区的性价比是很低的;
    2. 主要回收两部分:废弃的常量和不再被使用的类型;
    3. 废弃的常量:系统中没有任意一个字符串的值是该字符串 ;
    4. 不再被使用的类型满足三个条件:1、该类的所有实例都已经被回收;2、加载该类的类加载器已经被回收;3、该类所对应的java.lang.class对象没有在任何地方被引用;

2-2、垃圾收集算法

垃圾收集算法可以分为引用计数式垃圾收集、追踪式垃圾收集两大类,以下讨论均属于追踪式垃圾收集的范畴。

  1. 分代收集理论
    1. 建立的假说
      1. 绝大多数对象都是朝生夕灭的;
      2. 熬过多次垃圾收集过程的对象就越难以消亡;
      3. 跨代引用相对于同代引用仅占极少数
    2. 部分收集(Partial GC)分为:新生代收集(Minor GC/Young GC) 老年代收集(Major GC/Old GC)、混合收集(Mixed GC)
    3. 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
  2. 标记-清除算法(主要缺点:执行效率不稳定、内存空间的碎片化问题)
  3. 标记-复制算法(一块较大的Eden和两块较小的Survivor空间)
  4. 标记-整理算法(所有存活的对象都向内存空间的一端移动)

2-3、HotSpot算法实现细节

  1. 根节点枚举
    1. 所有收集器在根节点枚举这一步骤都必须暂停用户线程。
    2. HotSpot的解决方案中,使用OopMap的数据结构直接得到哪些地方存放着对象引用的,一旦类加载动作完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来。
  2. 安全点(解决如何停顿用户线程,让虚拟机进入垃圾回收状态)
    1. 在特定的位置记录指对应的OopMap信息,这个特定的位置被称为安全点;
    2. 用户程序必须执行到安全点才能暂停;
    3. 如何在垃圾收集发生的时候,让所有线程(不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来
      1. 抢先式中断(几乎不采用)
      2. 主动式中断
        1. 轮询式中断: 需要中断线程的时候,设置标志位,用户线程轮询这个标志,一旦为真,就会在附近的安全点挂起
        2. 内存保护陷阱:test指令
      3. 安全区域(解决:没有分配处理器时间,例如:用户线程处于Sleep状态或者Blocked状态)
        1. 能够确保在某一个代码片段之中,引用关系不会发生变化,在这个区域中任意地方开始垃圾收集都是安全的。
  3. 记忆集与卡表(对象跨代引用或部分区域收集(Partial GC)所带来的问题)
    1. 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构;
    2. 在垃圾收集的场景中,收集器只需通过记忆集判断出某一块非收集取悦是否存在有指向收集区域的指针就可以了。
    3. 卡表是记忆集的一种具体实现,最简单的实现形式只是一个字节数组,每个元素都对应着标识,卡表页有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏,没有则表示为0;
  4. 写屏障(解决卡表元素如何维护的问题,例如:它们何时变脏、谁来把它们变脏);
    1. 写屏障 : 可以看作在虚拟机层面面对 “引用类型字段赋值” 这个动作的AOP切面;
    2. 避免伪共享:不采用无条件的写屏障,先检查卡表元素,只有卡表元素未被标记过时,才将其标记为脏,是否开启卡表更新的条件判断 -XX:+UseCondCarMark;
  5. 并发的可达性分析
    1. “标记” 阶段是所有追踪式垃圾算法的共同特征;
    2. 会产生 “对象消失” 的问题,即原本应该是黑色的对象被误标为白色,(破坏其中一个条件即可,参考下3,4,)
      1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
      2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用;
    3. 增量更新 (并发扫描结束后,在将这些记录过的引用关系中的黑色为根,重新扫描一次);
    4. 原始快照 (无论引用关系删除与否,都会按照刚开始扫描的那一刻的对象图快照来进行搜索);

2-4、经典垃圾收集器

收集算法是内存回收的方法论,垃圾收集器是内存回收的实践者;

  1. Serial收集器
    1. 单线程,进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束;
    2. 所有收集器里额外内存消耗最小的;
  2. ParNew收集器(Serial收集器的多线程版本)
    1. 只能与CMS收集器配合工作;
    2. 新生代采用标记-复制算法,老年代采用标记-整理算法
  3. Parallel Scavenge收集器 (注重吞吐量)
    1. 新生代收集器,基于标记-复制算法
    2. 目标达到可控制的吞吐量 (吞吐量 = 运行用户代码时间/[运行用户代码时间+运行垃圾收集时间])
    3. -XX:MaxGCPauseMills:控制最大垃圾收集器停顿时间;
    4. -XX:GCTimeRatio:正整数,用户期望虚拟机消耗在GC上的时间,不超过程序的运行时间的1/(1+N)
  4. Serial Old收集器
    1. Serial收集器的老年代版本;
    2. 主要意义是供客户端模式下的HotSpot虚拟机使用;
    3. 服务端模式:与Parallel Scavenge收集器搭配使用,或作为CMS收集器发生失败后的后备预案;
  5. Parallel Old 收集器(注重吞吐量)
    1. Parallel Scavenge 收集器的老年代版本,支持多线程并行收集,基于标记-整理算法
    2. 注重吞吐量或者处理器资源较为稀缺的场合:优先考虑Parallel Scavenge 加 Parallel Old收集器;
  6. CMS收集器(获取最短回收停顿时间为目标;初始标记、重新标记需要 Stop the world; 采用增量更新算法)
    1. 初始标记:只是标记一下GC Roots能直接关联到的对象,速度很快
    2. 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程;
    3. 重新标记:为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录;
    4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象;
    5. -XX: CMSInitiatingOccupancyFraction的值来提高CMS的触发百分比;
  7. G1收集器(收集器面向局部收集的设计思路,目标是在延迟可控的情况下获得更高的吞吐量,基于Region的内存布局形式,通过原始快照算法)
    1. Region (单次回收的最小单元)
      1. 把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region都可以根据需要扮演新生代的Eden空间,Survivor空间或者老年带空间;
      2. 特殊Humongous区域,专门用来存储大对象(超过一个Region容量一半的对象即可判断为大对象);
      3. 超过了整个Region容量的超级大对象,将会存放在N个连续的Humongous Region 之中,G1的大多数行为都把 Humongous Region作为老年代的一部分来进行看待;
    2. Region里面存在的跨Region引用对象
      1. 使用记忆集避免全堆作为GC Roots扫描,G1比其他的传统垃圾收集器有着更高的内存占用,(经验上至少需要Java堆容量10%至20%额外内存)
    3. G1收集器的停顿预测模型是以衰减均值,衰减平均值更准确的代表"最近的"平均状态。
    4. G1收集器的运作过程划分为四个步骤:
      1. 初始标记
      2. 并发标记
      3. 最终标记
      4. 筛选回收
    5. G1整体来看是"标记-整理"算法实现,但从局部(两个Region之间)来看又是基于“标记-复制”算法。

3-5、低延迟垃圾收集器

内存占用、吞吐量、延迟 三者共同构成了不可能三角。

  1. Shenandoah收集器
    1. OpenJDK才有
    2. (相对G1而言,三个主要不同点)并发的整理算法,使用连接矩阵代替记忆集,默认不使用分代收集
    3. 转发指针解决对象移动与用户程序并发的一种解决方案(旧对象上转发指针的引用位置,使其指向新对象,所有对该对象的访问转发到新的副本上);
    4. 内存屏障模型改进为基于引用访问屏障(内存屏障只拦截对象中数据类型为引用类型的读写操作,从而省去大量对原生类型,对象比较,对象加锁等场景设置内存屏障所带来的消耗);
    5. 工作过程
      1. 初始标记:会有"Stop The World"
      2. 并发标记
      3. 最终标记 :处理原始快照扫描,最终标记节点会有一小段短暂的停顿
      4. 并发清理:清理整个区域一个存活对象都没有的Region
      5. 并发回收:使用读屏障和转发指针
      6. 初始引用更新:创建线程集合点,确保所有并发回收阶段中进行的收集器都已完成分配给它们的对象移动任务(把所有指向旧对象的引用修正到复制的新地址,这个操作称为引用更新)
      7. 并发引用更新:真正开始引用更新操作,会有短暂停顿
      8. 最终引用更新:修正GC Roots中的引用,最后一次停顿
      9. 并发清理:回收Region的内存空间
  2. ZGC收集器
    1. ZGC收集器基于Region内存布局的,不设分代,使用了读写屏障、染色指针和内存多重映射等技术来实现可并发
    2. 标志性设计:采用的染色体指针;
    3. 染色体指针:直接将少量额外的信息存储到指针上的技术,将指针的高4位提取出来存储四个标记信息【Finalizable、Remapped、Marked1、Marked0】,通过标志位可以看到其引用对象的三色标记状态、是否进入了重分配集、是否只能通过finalize()方法才能访问的到;
    4. 染色体指针三大优势:
      1. 一旦某个Region的存活对象被移走,这个Region就能被释放和重用掉,不必等待整个堆中所有指向该Region的引用都被修正后才能清理;
      2. 大幅减少在垃圾收集过程中的内存屏障的使用数量、设置内存屏障、尤其是写屏障的目的通常是为了记录对象引用的变动情况;
      3. 作为一种可扩展的存储结构用来记录更多与对象标记、重定向过程相关的数据、以便于日后进一步提高性能;
    5. ZGC是如何工作的
      1. 并发标记:遍历对象图做可达性分析的阶段,会停顿
      2. 并发预备重分配:根据查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集;
      3. 并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系;
      4. 并发重映射:修正整个堆中指向重分配集中的所有引用;

3.6、选择合适的垃圾收集器

  1. Epsilon收集器: 只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小,没有任何回收行为的Epsilon便是很恰当的选择;
  2. (JDK9之后)查看GC基本信息: -Xlog:gc
  3. 查看GC详细xinxi:-Xlog:gc*
  4. 查看GC前后的堆,方法去容量变化:-Xlog:gc+heap=debug
  5. 查看GC过程中用户线程并发时间以及停顿的时间:-Xlog:safepoint
  6. 查看收集器Ergonomics机制:-Xlog:gc+ergo*=trace
  7. 查看熬过收集后剩余对象的年龄分布信息:-Xlog:gc+age=trace
  8. -Xlog[ : [selector] [: [output] [:decorators ] [: output-options] ] ]

3.7、内存分配与回收策略

  1. 主要解决:自动给对象分配内存以及自动回收分配对象的内存;
  2. 对象优先在Eden分配;
  3. 大对象直接进入老年代 :-XX:PretenureSizeThreshold 指定对于该设置值的对象直接在老年代分配;
  4. 长期存活的对象将进入老年代:-XX:MaxTrnuringThreshold =1 :晋升到老年代的年龄阀值;
  5. 动态对象年龄判断:HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold晋升的老年代
  6. 空间分配担保: 发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的,如果不成立,则虚拟机会查看-XX:HandlePromotionFailure 参数的设置值是否运行担保失败;

3、虚拟机性能监控、故障处理工具

3-1、bin目录提供的基础工具

JDK11提供的工具-官方文档

3-2、可视化故障处理工具

1、JHSDB :基于服务型代理的调试工具

1、通过 jps -l 查询程序的进程PID;
2、使用jhsdb hsdb --pid [进程ID] 进入图像界面

在这里插入图片描述### 2、Jconsole :Java监视与管理控制台

如下图所示:

在这里插入图片描述

3、VisualVM:多合一故障处理工具

1、jdk1.8之后不再集成 需要下载 [visualvm下载地址](https://visualvm.github.io/index.html)
2、配置visualvm_jdkhome 路径
3、visualvm可安装插件
4、生成、浏览堆转储快照: heapdump
5、分析程序性能 通过:Profiler 中可看CPU和内存等
6、BTrace动态日志追踪 :安装BTrace插件

4、Java Mission Control : 可持续在线的监控工具

5、HotSpot虚拟机插件及工具

1、HSDIS被官方推荐的HotSpot虚拟机即时编译代码的反汇编插件
2、LogCompliation
3、Project Creator
4、MakeDeps
5、Client Complier Visualizer
6、Ideal Graph Visualizer

参考书籍《深入理解Java虚拟机》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java虚拟机(JVM)是Java程序的运行环境,它负责将Java字节码转换为可执行的机器码并执行。深入理解Java虚拟机涉及了解JVM的工作原理、内存管理、垃圾回收、类加载机制等方面的知识。 首先,JVM的工作原理是通过解释器或即时编译器将Java字节码转换为机器码并执行。解释器逐条执行字节码指令,而即时编译器将字节码转换为本地机器码,以提高程序的执行效率。 其次,内存管理是JVM的重要任务之一。JVM将内存分为不同的区域,包括堆、栈、方法区等。堆用于存储对象实例,栈用于存储局部变量和方法调用信息,方法区用于存储类的信息。JVM通过垃圾回收机制自动回收不再使用的对象,释放内存空间。 此外,类加载机制也是深入理解JVM的关键内容之一。类加载是将类的字节码加载到内存中,并进行验证、准备、解析等操作。类加载器负责查找并加载类的字节码,而类加载器之间存在着父子关系,形成了类加载器层次结构。 还有其他一些与性能优化、调优相关的内容,如即时编译器的优化技术、垃圾回收算法的选择等,也是深入理解Java虚拟机的重要方面。 总的来说,深入理解Java虚拟机需要对JVM的工作原理、内存管理、垃圾回收、类加载机制以及性能优化等方面有较深入的了解。掌握这些知识可以帮助开发人员编写出更高效、稳定的Java程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值