Java内存与垃圾回收篇(对象内存与垃圾回收机制)上篇

Posted 狗哥狗弟齐头并进

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java内存与垃圾回收篇(对象内存与垃圾回收机制)上篇相关的知识,希望对你有一定的参考价值。

1 对象的实例化内存布局与访问定位

1.1对象实例化

在这里插入图片描述

1.2 创建对象的步骤

  1. 判断对象对应的类是否加载、链接、初始化。
    虚拟机遇到一条new指令。首先去检查这个指令的参数能否在元空间的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析、初始化。如果没有被加载,那么在双亲委派机制下进行加载,找到了就去加载,生成对应的class对象。如果没有找到久抛出ClassNotFoundException异常。
  2. 为对象分配内存
    首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。
    如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
    分配内存的时候有两个重要的分支:
    指针碰撞:
    如果内存是规整的,那么虚拟机将采用的是指针碰撞法来为对象分配内存。意思就是所有用过的内存存在一边,空闲的存在另外一边,中间放着一个指针作为分界点的治时期,分配内存就仅仅是把指针向空闲那百年挪动一段与对象大小相等的距离。如果垃圾回收器选择的是Serial,ParNew这种基于压缩算法,虚拟机采用这种分匹配方式。一般带有Compact过程收集器时,使用指针碰撞。
    空间列表:
    如果内存不是规整的, 已使用的内存和未使用的内存互相交错,那么虚拟机将次啊用的是空闲列表法来为对象分配内存。虚拟机需要为了一个列表,记录了那些内存块是可用的,再分配内存的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容,这种分配方式称为空闲列表。
  3. 处理并发安全问题
    采用CAS失败重试,区域加锁保证更新的原子性
    每个线程预先分配一块TLAB
  4. 初始化分配到空间
  5. 设置对象的对象头
    将类的元数据信息,对象的HashCode和对象的GC信息,锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
  6. 执行init方法进行初始化

2 对象的内存布局

在这里插入图片描述

3 对象的访问定位

在这里插入图片描述

4 执行引擎

4.1 执行引擎概述

在这里插入图片描述

  • 执行引擎是虚拟机核心的组成部分之一
  • 虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力。区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上,而虚拟机的执行引擎是由软件自行实现的,能够执行那些不被硬件直接支持的指令集格式。
  • JVM的主要任务是把字节码加载到内存,解释执行。其中就需要执行引擎将字节码指令解释、编译为平台上的本地机器指令。
  • JVM中的执行引擎充当了将高级语言翻译为机器语言的译者

4.2 Java代码编译和执行过程

在这里插入图片描述
在这里插入图片描述

什么是解释器,什么是JIT编译器?
解释器:当java虚拟机启动的时候会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容‘翻译’为对应平台本地机器指令执行。
JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

两者联系
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间推移,JIT发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更嘎哦的程序执行效率。

4.3 热点代码及探测方式

JVM是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,需要根据代码被调用执行的频率而定发。需要被编译为本地代码的字节码,也被称之为热点代码。JIT编译器在运行时会针对那些频繁被调用的热点代码做出深度优化,将其直接编译为对应平台的本地机器指令,依次提升Java程序的执行性能。

如何确定一段代码或者一个方法被调用了多少次,怎么确认是热点代码呢?那么目前HotSpot虚拟机所采用的热点探测法公使是基于计数器的热点探测。
每一个方法有两个不同类型的计数器,分别为方法调用计数器和回边计数器。
方法调用计数器用于统计方法的调用次数
汇编计数器则用于统计循环体内执行的循环次数。


5 StringTable

5.1 String 的基本特性

  • String声明为final不可被继承
  • String实现了Seriable接口,Comparable接口表示String支持序列化,可以比较大小。
  • jdk 8 内部定义的数组是 char 类型的数组,9之后改为 byte类型
  • 是不可变的字符序列

5.2 String的内存分配及拼接操作

Java 7中将字符串常量池的位置调整到Java对内
因为永久代默认比较小,且永久代的垃圾回收频率低。
再Java8中的元空间,字符串常量在堆中。

  • 常量与常量的拼接结果在常量池,原理是编译器优化。
  • 常量池中不会存在相同内容的常量。
  • 只要其中有一个是变量。结果就在队中。变量拼接的原理是StringBuilder
  • 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
  • 编译器优化会将"a"+“b”+“c"变为"abc”;
  • 如果出现变量,JDK5以前使用的是StringBuffer,JDK5以后底层实现先new一个StringBuilder。从局部变量表依次取字符,然后调用StringBuilder的append方法进行拼接。最后再调用toString()

6 垃圾回收概述

6.1 什么是垃圾?

垃圾指的是:在程序的运行过程中没有任何指针指向的对象,就是这个对象没有了引用。

6.2 为什么要进行GC?

  • 如果不对垃圾进行垃圾回收,那么这些垃圾所占的内存空间一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至导致内存溢出。
  • 如果不进行垃圾回收,内存迟早被消耗完
  • 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将占用的堆内存移到堆的另外一端。从而可以再次分配内存。

STW(Stop The World):指的是在进行垃圾收集算法时,Java其他应用程序暂停执行。全局停顿,java代码停止,native代码可以执行,但不能与jvm交互。

6.3 早期的垃圾回收与Java垃圾回收机制

6.3.1 早期垃圾回收

在早期C、C++时代,垃圾回收基本上是手工机型的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放
这种方式可以灵活控制内存释放的时间,但是惠给开发人员带来频繁申请和释放内存的管理负担。倘若一处内存区间由于程序员编码的问题忘记被回收,那么就i会产生内存泄漏。垃圾对象永远无法被清除,内存溢出就会造成应用程序崩溃。

6.3.2 Java垃圾回收机制

  • 自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险。
  • 没有垃圾回收,java也会和cpp一样,各种泄漏问题让你头疼不已。
  • 自动内存管理机制,将程序员从繁重的内存管理种顾释放出来,可以更专注与开发的工作。

担忧:
过度依赖自动回收,会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
遇到OOM,了解了回收机制才能做好更好的应对。
堆这些自动化技术实施必要的监控和调节。

7 *** 垃圾回收相关算法 ***

7.1 标记阶段: 可达性分析算法

7.1.1 垃圾标记阶段

此阶段判断对象是否存活

  • 在堆里存放着的几乎所有java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中那些是存活对象,那些事已经死亡的对象。只有北郊极为已经死亡的对象,GC菜会执行垃圾回收,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
  • 那么在JVM中究竟是如何标记一个死亡对象呢? 简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
  • 判断对象存活一般有两种方式:引用计数算法和可达性分析算法

因为引用计数算法会有循环引用的问题出现,会导致内存泄漏,JAVA
采用的算法是可达性分析算法。

7.1.2 可达性分析算法

在可达性分析算法中有一个很重要的名词就是 GC Roots,跟集合必须是以组活跃的引用。
基本思路:

  • 可达性分析算法是以根对象集合为起点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
  • 如果目标对象没有任何的引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被跟对象集合直接或者间接连接的对象菜是存活对象。

在Java语言中,GC Roots包括以下几类元素

  • 虚拟机栈引用的对象。例:被各个线程所调用的本地方法中使用到的参数、局部变量等。
  • 本地方法栈内引用的对象。就是本地方法引用国得对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。例:字符串常量池里的引用。
  • 所有被同步锁持有的对象。
  • JAVA虚拟机内部的引用,基本数据类型队以ing的内部引用。
  • 反应虚拟机内部的引用。

重要的是记住:栈引用,方法区中的引用,类静态属性以及常量池的引用。

7.1.3 finalize()

在这里插入图片描述
在这里插入图片描述

7.2 清除阶段 :标记清除算法

目前JVM中常用的三种算法是:复制算法,标记清除算法,标记压缩算法。
标记清除算法的执行过程
当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行亮相工作,第一项是标记,第二项是清除。
标记:Collector从引用根节点开始遍历,标记所有被引用的对象,一般是对象的Header中记录为可达的对象。
清除:Collector从堆内存从头到尾进行线性遍历,如果发现某个对象在其Header中记录为可达对象,则将其回收。
如图所示:
在这里插入图片描述
这种方式的缺点:

  • 效率不高
  • 在进行GC的时候有STW出现,导致用户体验差。
  • 这种方式清理出来的空闲内存是不连续的,会产生内存碎片,需要维护一个空闲列表。

注意:何为清除?
清除并不是真正的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有心对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。

8 清除阶段:复制算法

复制算法适合用于存活对象少的时候使用。
核心思想:
将活着的内存部分分为两块,每次只使用其中一块,在进行垃圾回收时将正在使用的内存中的存活对象赋值到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
如下图所示:
在这里插入图片描述
将内存划分为AB两块,当A中有存活对象的时候,将实体对象放到B中连续的区域,最后堆A区域进行垃圾回收,类似于新生代中S0,S1中就使用了复制算法。
优点

  • 没有标记和清除过程,实现简单
  • 不会出现碎片问题。
    缺点
  • 需要两倍的内存空间
  • 需要维护更多的引用,时间开销也不小

一般适合用于存活对象较少,这样子维护复制的开销就比较小。

9 标记压缩算法 (标记整理算法)

背景:
复制算法的高效性是简历在存活对象少,垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于皴法对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要其他的算法。

标记清除算法应用于老年代会产生内存碎片的问题,但是该算法不仅执行效率低。还会产生内存碎片。则诞生了标记压缩算法。

执行过程:
第一阶段和标记=清除算法一样,从根节点开始标记所有被引用对象。
第二阶段将所有存活对象压缩到内存的一段,按顺序排放。之后,清理外边界所有的空间。
在这里插入图片描述

10 小结

对比三种算法:
在这里插入图片描述
复制算法的效率很高。但是需要的空间也多。
标记-整理算法相对来说更平滑,但是效率上却很慢。比复制算法多了一个标记阶段,比标记清除多了一个压缩阶段。


11、分代收集算法

分代收集算法是基于这么一个事实:
不同的对象的生命周期是不一样的。因此,不同声明周期的对象可以采用不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,一遍提高垃圾回收效率。
目前所有的GC都是采用分代收集算法进行垃圾回收的。
在这里插入图片描述

12、增量收集算法和分区算法

增量收集算法

​ 因为其他的垃圾回收算法在进行垃圾回收的时候会出现STW事件,严重影响用户的体验,导致了增量收集算法的诞生。

基本思想

​ 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

​ 总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间的冲突的妥善处理,允许垃圾手机线程以分阶段的方式完成标记、清理或复制的工作。

缺点:

​ 是用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

目的都是为了降低延迟,降低停顿时间。

在这里插入图片描述
这些只是算法的思想。GC的过程很复杂。


读万卷书,行万里路。

读书人,读书魂,读书人一直都是读书人!!!

以上是关于Java内存与垃圾回收篇(对象内存与垃圾回收机制)上篇的主要内容,如果未能解决你的问题,请参考以下文章

JVM垃圾回收机制与内存回收

垃圾回收与内存分配——总结篇

java 怎么对一个对象强制垃圾回收

垃圾回收与对象的引用

JS高程中的垃圾回收机制与常见内存泄露的解决方法

java垃圾回收