java 5分钟检查数据状态_《深入理解Java虚拟机》5分钟速成:(2/5)内存管理

第二部分:自动内存管理机制

前言:

1、运行时数据区域包括哪些?

2、GC需要考虑回收的内存有哪些?

3、判断对象是否“死去”的算法有哪些?

4、什么是引用计数法?

5、什么是根搜索算法?

6、Java 的4种引用方式?

7、有哪些垃圾收集算法?


第2章 Java内存区域与内存溢出异常

2.1 JVM内存管理这堵墙?

  • C和C++: 在内存管理领域,他们既拥有每一个对象的“所有权”,也担负着每一个对象生命开始到终结的维护责任。
  • Java:在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄露和内存溢出的问题。但是也出现问题:如果不理解虚拟机是怎么使用内存的,排查错误将会是一项非常艰难的工作。

2.2 运行时数据区域

运行时数据区域包括哪些?

c7bf3017326805d9a27231f052795e42.png

2.2.1 程序计数器(线程私有)

  • 程序计数器(Program Counter Register): 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。
  • 线程私有:每条线程都有一个独立的程序计数器。
  • 如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是native方法,计数器为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.2.2.Java虚拟机栈(线程私有)

  • 同样是线程私有;
  • 描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),一个方法对应一个栈帧
  • 栈帧(Stack Frame):用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 其中,局部变量表: 存放了各种基本类型、对象引用和returnAddress类型(指向了一条字节码指令地址)。其中64位长度long 和 double占两个局部变量空间,其他只占一个。

规定的异常情况有两种:

  • 1. 线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  • 2. 如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。

2.2.3.本地方法栈(线程私有)

  • 同样是线程私有;
  • 和Java虚拟机栈很类似,不同的是本地方法栈为Native方法服务。(Native方法:jvm执行的c/c++方法)

2.2.4.Java堆(线程共享)

  • 是Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。
  • 堆区唯一目的就是存放对象实例。
  • 堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。
  • 堆无法扩展时,抛出OutOfMemoryError异常(oom)。

其中,Eden空间、From Survivor空间、To Survivor空间 三者之间关系:

  • 新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
  • 默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
  • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

2.2.5.方法区(线程共享)

  • 线程共享: 所有线程共享
  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 当方法区无法满足内存分配需求时,抛出OutOfMemoryError。

2.2.6.运行时常量池(方法区的一部分)

  • 它是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池;
  • 常量池(Const Pool Table):用于存放编译期生成的各种字面量和符号引用。并非预置入Class文件中常量池的内容才进入方法运行时常量池,运行期间也可能将新的常量放入池中(new String(***)场景),这种特性被开发人员利用得比较多的便是String类的intern()方法。
  • 当方法区无法满足内存分配需求时,抛出OutOfMemoryError。

2.2.7.直接内存(nio新库中用到)

  • 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
  • JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
  • 优点:因为避免了在Java堆和Native堆中来回复制数据,提高了性能。
  • 当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建

对象的创建:

  • 接受new关键字指令后,检查指令参数是否能在常量池中定位到一个类的符号引用
  • 然后检查符号引用对应的类是否已被加载、解析和初始化。如果没有就执行。
  • 类加载通过后,虚拟机为新生的对象分配内存。
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。
  • 虚拟机设置对象头信息。

内存分配的方式:

  • 指针碰撞:内存是规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。
  • 空闲列表:内存不规整,虚拟机维护一个列表用来记录那些内存是可用的,分配时从中找到足够的内存划分给对象,并更新表记录。

保证线程安全的方式

  • CAS:对分配的内存空间进行同步处理,采用CAS配上失败重试的方式保证操作的原子性。
  • 线程分配缓冲区:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配好内存。

2.3.2 对象的内存布局

  • 对象在内存中存储的布局可以分为三块区域对象头、实例数据和对齐填充。
  • 对象头,又可以分为两部分,第一部分存储自身运行时数据,第二部分是类型指针
  • 自身运行数据:主要包括哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
  • 类型指针:即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据:对象真正存储的有用的有效信息,继承父类的字段也会包含。
  • 对齐填充:对象起始地址必须是8的整数倍,对齐填充起到了占位符的作用。

2.3.3 对象的访问定位

栈到堆的关联实现(两种方式):

  • 方式1:句柄方式:堆中将划分出来一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址。reference中存储的是稳定的句柄地址,如果对象被移动,reference的地址无需改变。
b8996f1651b7d0a81639c24f5b08ec9c.png
方式2: 直接指针方式(HotSpot虚拟机采用此方式):优点:直接指针访问可以减少指针定位的时间开销 )
0c1a375070791518475be9937c7674ac.png

2.4 实战:OutOfMemoryError异常

2.4.1 Java堆溢出

2.4.2 虚拟机栈和本地方法栈溢出

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

2.4.4 本机直接内存溢出

2.5 本章小结

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

3.1 GC(Garbage Collection)

对于一般Java程序员开发的过程中,不需要考虑垃圾回收;但遇到内存溢出&泄露的问题,需要掌握调试&监控方法。

  • 哪些内存需要回收?

线程共享的Java堆和方法区,其他线程私有的内存不需要考虑回收问题。

  • 如何判定对象为垃圾对象(对象“已死”的判定算法)?

a. 引用计数法

b. 可达性分析法

  • 如何回收垃圾对象(借助:垃圾收集算法)?

a. 回收策略(标记清除、复制、标记整理、分带收集算法)

b. 常见的垃圾回收器(Serial、Parnew、Cms、G1)

  • 何时回收垃圾对象(不可达对象≠非死不可对象)?

3.2 对象“已死”的判定算法

由于程序计数器、Java虚拟机栈、本地方法栈都是线程独享,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,线程共享,是GC的所关注的部分。

在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收。

有两种算法可以判定对象是否存活:

  • 引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。)
  • 可达性分析算法(Jvm采用此方式):通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。

3.2.1 引用计数算法

3.2.2 可达性分析算法

在主流的商用程序语言(如我们的Java)的主流实现中,都是通过可达性分析算法来判定对象是否存活的。GCRoots的引用链,图示如下:

719953390ae77038d819a91a7afd93fd.png

Java中可以作为GC Roots的对象包括:

  • 虚拟机栈中引用的对象;
  • 本地方法栈中Native方法引用的对象;
  • 方法区静态属性引用的对象;
  • 方法区常量引用的对象。

3.2.3 再谈引用:

  • 强引用,类似 Object obj = new Object()这类引用,只要强引用还存在垃圾收集器永远不会收集掉被引用的对象。
  • 软引用,在系统将要发生内存溢出异常之前将会把这些对象列入回收范围之中进行第二次回收。JDK1.2之后提供了SoftReference类来实现软引用。
  • 弱引用,被弱引用关联的对象只能生存到下一次垃圾收集之前,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
  • 虚引用,不会对对象的生存时间造成影响,也无法通过虚引用来取得对象的实例,虚引用的作用是当对象被回收的时候收到一个系统通知。

3.2.4 对象Live or Dead?

对象判为死刑需要两次标记(不可达对象≠非死不可对象):

  • 1、没有与GC Roots相连的引用链。
  • 2、首先判断虚拟机是否需要执行finalize()方法,如果对象没用覆盖finalize方法或者finalize被虚拟机调用过,那么虚拟机都没必要执行finalize方法。否则对象被放置在一个叫做F-Queue的队列中,稍后虚拟机建立一个低优先级的Finalizer线程去执行finalize方法。如果在finalize方法里对象重新与GC Roots关联上,那么对象便复活。可以说finalize方法是对象逃脱死亡命运的最后一次机会,这种机会只有一次,因为同一个对象的finalize最多只会被虚拟机自动调用一次。

3.2.5 回收方法区

  • 主要回收废弃常量和无用的类。
  • 废弃常量:即没有任何地方引用这个常量,没有任何地方引用这个字面量。
  • 无用类条件:(同时满足以下三个条件,类可以被回收):

1、该类的所有实例都已被回收。

2、加载该类的classLoader已被回收。

3、对应的class对象没有任何地方引用,没法通过反射访问该类。

3.3 垃圾收集算法

  • 标记-清理法(Mark-Sweep):
  • 复制算法(Copying):--适合“新生代”
  • 标记整理算法(Mark-Compact):--适合“老年代”
  • 分代收集算法(Generational Collection):商业JVM均采用此方式。

a. 标记-清理法:

  • 首先对标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
  • 标记和清除效率都比较低,还有就是会产生大量的空间碎片。
f9f540beb3205d0d0d214409bd83bb05.png

b. 复制算法:

  • 将可用内存划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完后,就将还在着的对象复制到另外一块上,然后再把已使用的过的内存空间一次清理掉。
  • 实现简单,运行高效,但是内存缩小了一半。常用于存活对象少的新生代。
2463d73eecb79e2f336058581735d9bb.png

c. 标记整理算法:

  • 首先对需要回收的对象进行标记,对存活的对象都向一端移动;
  • 然后直接清理掉端边界以外的内存。
7940d8f7167c3f6141f4783d4bcda367.png

d. 分代收集算法:

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

3.4 Hotspot收集算法实现

枚举根节点(GC Roots):

  • 在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到直接获取那些地方存在着对象引用。
  • 在OopMap的帮助下可以准确的完成GC Roots枚举。

安全点(Safe Point):

  • HotSpot在特定的位置记录了这些信息,生成了对弈的OopMap,这些位置称为安全点。
  • 让所有的线程到达安全点的方式有抢占式中断和主动式中断。
  • 抢占式中断:在GC发生时中断所有线程,如果线程不再安全点上就恢复线程直到到达安全点。
  • 主动式中断(JVM采用此方式):对线程设置一个标志,各个线程主动去轮询这个标志,轮询标志和这个安全点重合,发现中断标志就中断挂起。

安全区域(Safe Region,扩展的Safe Point):

  • 如果程序没有分配CPU时间,线程处于睡眠或者阻塞状态,这时候线程无法响应JVM的中断请求,这种情况就需要安全区域爱解决。安全区域是指一段代码之中引用不会发生变化。
  • 在线程执行到安全区域的时候首先标识自己已进入安全区域,在GC时无需管标识自己为安全区域的线程,在线程要离开的时候要检查是否已经完成根节点枚举,如果完成就继续执行,否则必须受到可以离开的信号才可以离开。

3.5、垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。如果收集器中存在连线则说明收集器可以搭配使用。

--参照原文,略。

3.6 内存分配和回收策略

Java自动内存管理,分为两方面:

  • 给对象分配内存(自动):new instance时;
  • 回收给对象的内存(自动):gc instance时;

为什么要将堆内存分区?

对于一个大型的系统,当创建的对象及方法变量比较多时,即堆内存中的对象比较多,如果逐一分析对象是否该回收,效率很低。分区是为了进行模块化管理,管理不同的对象及变量,以提高 JVM 的执行效率。

堆内存分为哪几块?

  • Young Generation Space 新生区(也称新生代)
  • Tenure Generation Space养老区(也称旧生代)
  • Permanent Space 永久存储区

Minor GC 和 Full GC有什么区别?

  • Minor GC:新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般回收速度较快。
  • Full GC:老年代 GC,也叫 Major GC,速度一般比 Minor GC 慢 10 倍以上。

内存分配有哪些原则?

  • 对象优先分配在 Eden(新生代内存):(原因:基于对象朝生夕死特性,便于频繁GC)
  • 大对象直接进入老年代:(原因:避免大对象在Eden 和 2个Survivor之间来回拷贝)
  • 长期存活的对象将进入老年代:(原因:老年代GC不频繁的特性)
  • 动态对象年龄判定:不是固定依据门槛(比如15个 Minor GC周期)判定,改为动态判定。
  • 空间分配担保:复制算法(Eden->Survivor内存不足场景时),拷贝到老年代内存中。
340fed68486e3161c2505242ea0d8844.png

相关文章:

《深入理解Java虚拟机》5分钟速成:(1/5)走进Java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值