JVM运行时数据区 ​
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以堆为例:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行时按需扩展 。虚拟机实现者可以使用任何垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的。
copy的javaGuide,详情可看网址:
什么是垃圾回收 ​
- 发生的地点:堆内存,元空间
- 目的:垃圾回收的主要目标是识别和清理不再被引用的对象,同时将仍然存活的对象移动到下一个区域。
- 时机:
- 申请heap space失败后会触发GC回收
- 系统进入idle后一段时间会进行回收
- 主动调用GC进行回收 System.gc()
堆空间的基本结构 ​
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。
现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法.
新生代:一个Eden空间和两个Survivor空间
Eden ,S0 ,S1
老年代:连续的内存空间
**元空间:**Java虚拟机用于存储类元数据的区域,取代了JDK 8及之前版本中的永久代。在元空间中,类的元数据(包括类的结构信息、字段信息、方法信息等)存储在本地内存而不是堆内存中。
元空间的对象(类的元数据)是由Java虚拟机动态地生成和管理的。当一个类首次被加载时,Java虚拟机会解析类的字节码并生成相应的元数据,然后将这些元数据存储在元空间中。这个过程是在类加载阶段完成的。
在元空间中,对象的进入不同于堆内存的垃圾回收,因为元空间主要是用于存储类的元数据,而不是实例对象。元数据的生命周期通常随着类的加载和卸载而变化,而不是像堆内存中的对象那样具有生命周期。
元空间的优势在于它不再受到永久代的大小限制,而且不容易发生永久代内存溢出的问题。此外,元空间的内存管理由本地内存来负责,不再需要手动调整永久代的大小。
总的来说,元空间中的对象(类的元数据)是在类加载过程中动态生成的,而且其生命周期与类的加载和卸载相关。元空间的管理更为灵活,并且避免了一些与永久代相关的问题。
新生代如何进入老年代 ​
在Java虚拟机中,新生代中的对象在经过多次垃圾回收后,如果仍然存活,就可能会被晋升(Promotion)到老年代。这个过程主要是由新生代的垃圾回收机制来控制和决定的。常见的新生代垃圾回收算法采用复制算法。
新生代通常被划分为三个区域:Eden空间和两个Survivor空间(通常分别称为S0和S1)。对象在Eden空间中被创建,经过一次垃圾回收后,仍然存活的对象会被复制到其中一个Survivor空间。当某个Survivor空间满时,还存活的对象会被复制到另一个Survivor空间,同时清空原来的Survivor空间。经过多次循环,仍然存活的对象最终会被晋升到老年代。
新生代中对象晋升到老年代的主要条件有两个:
- 年龄阈值: 对象在Survivor空间中经过一次垃圾回收,它的年龄就会增加。Java虚拟机通常会设置一个年龄阈值(默认为15),当对象的年龄达到或超过这个阈值时,就会被晋升到老年代。
- Survivor空间中相同年龄的对象大小超过Survivor空间的一半: 当某个Survivor空间中存放同一年龄的对象的大小超过了该Survivor空间的一半时,这些对象将直接晋升到老年代,而不再等待年龄阈值的判定。
通过这样的机制,新生代中的短周期对象会在Eden和Survivor空间中多次进行垃圾回收,只有经过多次回收仍然存活的对象才会进入老年代。这样的策略有效地减轻了老年代的压力,因为老年代主要存放生命周期较长的对象。
对象优先在 Eden 区分配 ​
当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.
大对象直接进入老年代 ​
大对象直接进入老年代的行为是由虚拟机动态决定的,它与具体使用的垃圾回收器和相关参数有关。大对象直接进入老年代是一种优化策略,旨在避免将大对象放入新生代,从而减少新生代的垃圾回收频率和成本。
长期存活的对象将进入老年代 ​
虚拟机给每个对象一个对象年龄(Age)计数器。
大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
两个假说:
- 强分代假说
- 熬过越多次垃圾收集过程的对象就越难以消亡。(简单理解就是越老的对象就具有”老而不死”的特性)
- 弱分代假说
- 弱分代假说(Weak Generational Hypothesis): 绝大多数对象都是朝生夕灭的.
死亡对象的判断方法: ​
可达性分析是目前主流的垃圾回收算法,它能够有效地处理各种情况,包括循环引用。引用计数法由于无法解决循环引用问题,已经较少被使用。
可达性分析算法(根搜索): ​
可达性分析是Java虚拟机中主要用于判断对象是否可被回收的一种方法。它基于的思想是,通过一组称为“根”的对象出发,根对象包括:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 本地方法栈中JNI(Java Native Interface)引用的对象
- Java虚拟机内部的一些特殊对象,如基本数据类型对应的Class对象等
从这些根对象出发,通过引用链追踪对象的引用关系,找到所有能够被访问到的对象。那些无法通过任何引用链与根对象连接的对象被认为是不可达的,即死亡对象,它们将被垃圾回收器回收。
引用计数法: ​
引用计数法是一种简单的垃圾回收算法,它通过维护一个计数器来记录每个对象被引用的次数。每当有一个引用指向该对象时,计数器加一;当引用失效或被释放时,计数器减一。当计数器的值为零时,说明该对象没有被引用,即没有指向它的引用,可以被回收。
引用计数法的问题在于难以解决循环引用的情况,即若存在两个或多个对象循环引用,它们的计数器永远不会归零,导致这些对象将一直存在,即使它们不再被程序所使用。因此,现代的垃圾回收算法通常采用可达性分析而不是引用计数法。
垃圾回收基本算法 ​
复制算法: ​
复制算法是新生代(Young Generation)垃圾回收中常用的一种算法,用于处理大量短生命周期的对象。该算法将内存划分为两个区域,通常是一个Eden空间和两个Survivor空间(一般为S0和S1)。
以下是复制算法的基本步骤:
- 对象分配: 初始时,所有对象都被分配到Eden空间。
- 垃圾回收: 当Eden空间满时,触发一次垃圾回收。在回收过程中,将Eden空间中仍然存活的对象复制到其中一个Survivor空间(例如,S0)。
- Survivor空间切换: 下一次垃圾回收时,将存活下来的对象从上一次使用的Survivor空间(例如,S0)复制到另一个Survivor空间(例如,S1)。
- 老年代晋升: 当一个对象在Survivor空间中经过多次回收仍然存活时,它将被晋升到老年代。
- 空间清理: 在复制过程中,已经复制到Survivor空间的对象会被清理掉。而在老年代,采用其他垃圾回收算法进行清理,例如标记-清除-整理(Mark-Sweep-Compact)。
这个过程会一直重复进行。由于新生代的对象寿命较短,大多数对象在一次或两次垃圾回收后就会被清理掉,从而减少了老年代的垃圾回收频率。
复制算法的优点是实现简单,内存分配高效。然而,由于它将内存分为两个部分,即Eden和Survivor,实际可用内存比总内存的一半还要少。此外,复制算法对于长生命周期的对象可能不太适用,因为这些对象需要被复制多次,增加了复制成本。因此,它更适用于应用中有大量短生命周期对象的场景。
标记清除整理算法: ​
标记-清除-整理算法是一种垃圾回收算法,通常用于老年代的内存管理。它包括以下三个主要阶段:
- 标记(Mark): 在这个阶段,垃圾回收器会标记所有从根对象可达的存活对象。根对象可以是虚拟机栈、方法区中的类静态属性等。通过递归或迭代的方式,标记所有与根对象直接或间接相连的对象。
- 清除(Sweep): 在标记阶段之后,垃圾回收器会清除所有未被标记的对象,即那些不可达的对象。这些对象被认为是垃圾,可以被回收。清除后,内存中会留下一片不连续的空闲空间。
- 整理(Compact): 在清除阶段之后,内存中可能存在不连续的空闲空间。为了提高内存的利用率,整理阶段将存活对象移动到内存的一端,以便在另一端形成一块连续的空闲空间。这有助于避免内存碎片化,提高程序运行时的内存分配效率。
分代收集算法: ​
分代收集算法是基于对象的生命周期长短划分内存区域,并针对不同区域使用不同的垃圾回收策略。通常将堆内存划分为新生代和老年代两个区域:
- 新生代(Young Generation): 新创建的对象通常寿命较短,因此新生代采用复制算法。新生代分为Eden空间和两个Survivor空间,对象在Eden空间被创建,经过一次垃圾回收后,仍然存活的对象被复制到其中一个Survivor空间。经过多次回收后,存活的对象将被晋升到老年代。
- 老年代(Old Generation): 老年代存放经过多次垃圾回收仍然存活的对象。由于老年代的对象寿命较长,采用标记-清除-整理算法。当老年代空间不足时,会触发老年代的垃圾回收,标记所有存活对象,清除未标记的对象,并整理存活对象的空间。
分代收集算法的基本思想是根据对象的生命周期将内存划分为不同的区域,并针对不同区域采用适合的垃圾回收策略,以提高垃圾回收的效率。
垃圾回收的触发时机 ​
垃圾回收的触发时机通常是由Java虚拟机根据一些内存管理的策略和阈值来判断的。以下是常见的触发垃圾回收的情况:
内存分配请求: 当应用程序申请内存空间时,如果虚拟机检测到堆内存的剩余空间不足以容纳新的对象,就会触发垃圾回收。这是最常见的触发时机之一。
System.gc()方法调用: Java提供了
System类中的gc()方法,它用于请求垃圾回收。然而,调用该方法并不保证会立即触发垃圾回收,因为虚拟机可以选择忽略该请求。javaCopy code System.gc();老年代空间不足: 当老年代的剩余空间不足以容纳新分配的对象时,会触发老年代的垃圾回收。
永久代或元空间空间不足: 在JDK 8及之前版本中,如果永久代的剩余空间不足以容纳新的类信息、方法信息等,会触发永久代的垃圾回收。在JDK 8及之后的版本中,由于使用了元空间,触发垃圾回收的时机可能与永久代时代不同。
应用程序线程空闲时: 在一些情况下,虚拟机可能会在应用程序线程空闲的时候触发垃圾回收,以最小化对应用程序性能的影响。
需要注意的是,虽然可以手动调用System.gc()方法来请求垃圾回收,但并不建议频繁地手动调用此方法,因为过度的垃圾回收可能会对应用程序性能产生负面影响。虚拟机一般会根据内部的策略和算法自动判断何时进行垃圾回收,以达到平衡性能和内存利用率的目标。
类加载回顾 ​
双亲委派 ​
- AppClassloader:自己写的类,用这个类加载器加载
- ExtClassloader:扩展类,加载E:\JAVA\jre\lib\ext中的类
- BootStrapClassloader:加载E:\JAVA\jre\lib\rt.jar中的类
笔记 ​
- 元空间需要垃圾回收吗
- 对象的引用计数存储在哪里:没有存储,由垃圾回收器动态维护和计算。现在已经不用引用计数法了
- String为什么被定义为不可变,因为可能内存泄露。