阿丹:
JVM的内存分区包括以下几个部分:
- 堆区(Heap) - 这是JVM的主要部分,用于存储实例对象和大多数Java对象,如数组和用户定义的类。
- 方法区(Method Area) - 这是线程私有的,用于存放类对象(加载好的类)。
- 栈区(Stack) - 这是线程私有的,包括虚拟机栈和本地方法栈。虚拟机栈用于保存调用关系的内存空间,而本地方法栈用于存放本地方法之间的调用关系(本地方法指的是JVM内部使用C++写的代码)。
- 程序计数器(Program Counter Register)- 这是线程私有的,放的是下一个要执行的指令的地址。
以下是学习JVM底层的大纲路径:
- 首先了解Java程序的执行过程,以及JVM的工作原理。
- 深入了解JVM的内存划分和内存管理,包括堆、栈和方法区的概念和作用。
- 掌握Java对象的创建和销毁过程,以及对象的内存布局和访问方式。
- 了解JVM的垃圾回收机制和内存分配机制,包括引用计数法、可达性分析法等垃圾回收算法。
- 掌握JVM的线程管理、线程同步和锁机制。
- 学习JVM的类加载机制和字节码解析,以及JVM的内部结构和工作原理。
- 掌握一些常用的JVM参数和监控工具,以便更好地管理和调优JVM。
以上是学习JVM底层的基础大纲路径,需要掌握的基础知识比较多,但只有系统地学习这些知识,才能更好地理解和应用Java语言和JVM。
在学习前一定要管理好自己的学习大纲来有计划系统化的学习以及形成足够完善的知识网络。
统一知识预先知晓:
运行时数据区分为了两个部分!
线程私有区:也就是每一个单个线程独有的只有自己可见的
其中的本地方法栈外连接了执行引擎-》本地的方法接口以及本地的方法库
线程共享区:可以理解为所有的线程操作的一个对象
堆!是什么?
堆是Java内存管理的主要区域,也是垃圾回收的主要区域。它用于存储所有对象实例和大多数Java对象,如数组和用户定义的类。
具体来说,Java堆是JVM中最大的一块内存区域,被划分为多个部分,包括新生代和老年代。新生代用于存储新创建的对象,而老年代则用于存储长时间存活的对象。每个对象在堆内存中都有一个独立的内存空间,包括对象头、实例变量和对象引用的变量。
在Java堆中,所有的对象实例和大多数Java对象都被分配内存空间。这些对象的内存空间通常是连续的,可以通过对象的引用直接访问其内存空间。此外,Java堆也存放了一些特殊的数据结构,如Java堆异常和Java本地方法栈。
Java堆的垃圾回收机制是Java内存管理的重要部分,它用于自动释放不再使用的对象占用的内存。Java垃圾回收器会自动检测堆中的对象,当一个对象不再被引用或不再被使用时,垃圾回收器会自动将其回收,并释放其占用的内存空间。
总之,Java堆是JVM中最大的一块内存区域,用于存储所有对象实例和大多数Java对象,并且是垃圾回收的主要区域。
堆!干什么?解决了什么?
堆是用于存储对象实例和大多数Java对象的地方,它是Java内存管理的主要区域。
堆内存用于存储由new创建的对象和数组,它只负责存储对象信息。每个对象在堆内存中都有一个独立的内存空间,包括对象头、实例变量和对象引用的变量。
通过垃圾回收机制,堆解决了程序运行中如何处理不再使用的对象占用的内存问题,从而自动释放内存空间,避免了内存泄漏和溢出的问题。
此外,Java中的线程每个都会有一个相应的线程栈与之对应,这保证了程序的并发运行。而堆则是所有线程共享的,也可以理解为多个线程访问同一个对象,比如多线程去读写同一个对象的值。
总之,堆是Java内存管理和垃圾回收的重要区域,它解决了如何存储和管理对象实例和Java对象,以及如何处理不再使用的对象占用的内存问题。
堆!如何实现的?
它通过JVM的内存管理和垃圾回收机制来实现。
在JVM中,堆是所有线程共享的内存区域,用于存储所有的对象实例和大多数Java对象。堆内存被划分为多个部分,包括新生代和老年代。新生代用于存储新创建的对象,而老年代则用于存储长时间存活的对象。
每个对象在堆内存中都有一个独立的内存空间,包括对象头、实例变量和对象引用的变量。在Java堆中,所有的对象实例和大多数Java对象都被分配内存空间。这些对象的内存空间通常是连续的,可以通过对象的引用直接访问其内存空间。
Java堆的垃圾回收机制是Java内存管理的重要部分,它用于自动释放不再使用的对象占用的内存。Java垃圾回收器会自动检测堆中的对象,当一个对象不再被引用或不再被使用时,垃圾回收器会自动将其回收,并释放其占用的内存空间。
垃圾回收机制的实现通常涉及到两个阶段:标记和清除。在标记阶段,垃圾回收器会遍历所有对象,找出所有引用的对象,并将其标记为活动的;在清除阶段,垃圾回收器会清理未被标记的对象占用的内存空间。
总之,堆是通过JVM的内存管理和垃圾回收机制来实现的。每个对象在堆内存中都有一个独立的内存空间,并且通过垃圾回收机制自动释放不再使用的对象占用的内存空间。
堆!什么工作原理?底层是什么?
在JVM中,堆是Java内存管理的主要区域,也是垃圾回收的主要区域。它用于存储所有对象实例和大多数Java对象,如数组和用户定义的类。
堆内存的工作原理如下:
- 每个对象在堆内存中都有一个独立的内存空间,包括对象头、实例变量和对象引用的变量。对象头包含该对象的类型信息、哈希码、GC分代年龄,以及指向类元数据的指针,实例变量则是对象真正存储的数据。
- 堆内存是所有线程共享的内存区域,线程在创建对象时,会直接在堆内存中分配相应的内存空间来存储该对象。
- Java堆是可扩展的,当需要更多的内存空间时,Java运行时会在运行时动态地扩展堆的大小。
- Java堆通过垃圾回收机制自动管理内存,当一个对象不再被引用或不再被使用时,垃圾回收器会自动将其回收,并释放其占用的内存空间。垃圾回收机制的实现通常涉及到两个阶段:标记和清除。在标记阶段,垃圾回收器会遍历所有对象,找出所有引用的对象,并将其标记为活动的;在清除阶段,垃圾回收器会清理未被标记的对象占用的内存空间。
Java堆的底层实现是通过数组来实现的。每个对象在堆内存中都有一个独立的内存空间,并且可以通过数组来访问该对象。同时,Java堆也通过数组来存储和管理内存空间,例如在进行垃圾回收时,垃圾回收器会通过数组来记录和管理内存空间的状态。此外,Java堆还支持内存的动态扩展和收缩,例如在需要更多的内存空间时,Java堆会自动扩展数组的大小,以提供更多的可用内存空间;当Java堆中的空闲内存过多时,Java堆会自动收缩数组的大小,以释放空闲的内存空间。
综上所述,Java堆通过数组来实现内存的管理和动态扩展与收缩。它是一个可扩展的内存区域,用于存储所有的对象实例和大多数Java对象,并且通过垃圾回收机制自动管理内存。
堆!堆的组成?
JVM的堆内存是Java内存管理的主要区域,用于存储所有对象实例和大多数Java对象。堆内存的组成可以细分为以下部分:
- 新生代:新生代是类的诞生、成长、消亡的区域,用于存放新创建的对象。它包括三个部分:Eden区、From Survivor区和To Survivor区。
- 老年代:老年代主要存放生命周期长的存活对象。
- 永久代(JDK1.8以前的版本):永久代在逻辑上虽然在堆内存空间,但在物理上与堆是独立的。
- 元空间(JDK1.8以后的版本):元空间是方法区的一个实现,是堆的一个逻辑部分。参照下面的说明1
需要注意的是,从JDK1.8开始,永久代被元空间替代,元空间从JVM的堆内存中移动到系统的本地内存。此外,堆内存还可以细分为其他部分,如S0和S1区,这两个区域在JDK的自带工具输出中可以看到。总之,JVM的堆内存由多个部分组成,主要用于存储和管理Java对象实例。
从Java 1.8开始,JVM中的方法区被实现为元空间,而不再是之前的永久代。元空间位于堆外内存,不与堆内存混用。元空间使用的是本地内存,其最大内存大小由系统内存决定,而不是由堆的大小决定。方法区是JVM的规范,而元空间则是JVM规范的一种实现。虽然元空间不再是方法区,但是它实现了方法区的功能,加载class二进制流到左侧的jvm堆外区域,因此可以说元空间是方法区的等价实现。
说明1:
元空间并不属于堆内存。在JVM的内存结构中,堆和元空间是两个独立的区域。
堆是Java内存管理的主要区域,用于存储所有对象实例和大多数Java对象,它是垃圾回收的主要区域。而元空间则是方法区的实现,它存储的是类的元数据,如类名、成员变量、方法等信息。元空间并不属于堆内存,而是使用本地内存,它的最大内存大小由系统内存决定。
因此,虽然元空间实现了方法区的功能,并且与堆内存有一定的关系,但是它并不属于堆内存的一部分。
我们来聊一聊关于堆的问题
堆内存被分为新生代和老年代,新生代可以被分为伊甸园去和两个幸存者区。
基于分代我们可以将垃圾回收的GC动作分为两个部分:
1、面向新生代的minor gc(young gc)
2、面向老年代的major gc(old GC)
3、一起垃圾回收就是full GC
新生代的GC的作用就是判断是Eden里面的数据是不是垃圾,如果不是垃圾的话就将这些数据放在幸存者区。如果是的话就清理掉。
那在minor gc判定你一次不是垃圾的时候,给你放到幸存者区。幸存者区有两个,在之后的每次GC的时候就判断一下是不是垃圾。同时年龄+1,当年龄到15岁的时候就可以成为老年代的一员了!
但是!
有几个直接进入老年代的情况:
1、大对象-》担保机制
担保转移动老年代的机制主要与 Survivor 区和 Full GC(全局垃圾收集)相关。下面是详细说明:
- 在 JVM 启动时,Eden 区和两个 Survivor 区(S0 和 S1)会被初始化。当 Eden 区满了,或者存活的对象年龄超过一定阈值(默认为 15),就会触发一次Minor GC。
- 在 Minor GC 过程中,活动(存活)的对象将被移动到 Survivor 区(S0 或 S1)。如果 Survivor 区也满了,那么存活的对象将被移动到另一个 Survivor 区或者老年代。
- 如果 Survivor 区没有足够空间存放所有的活动对象,那么 JVM 会触发 Full GC。如果 Full GC 仍然无法回收足够的空间,那么 JVM 会抛出 OutOfMemoryError 错误。
- 如果对象在 Survivor 区中经历了多次 GC 仍然存活,或者被显式地 pin(例如,被 final 修饰的对象的构造函数调用后),那么该对象将被移动到老年代。
- 当老年代满了,或者老年代中的对象数量增加到一定阈值(默认为 8000),或者系统内存空间不足,无法分配足够内存给对象分配空间时,会触发 Full GC。
- Full GC 会清理整个 JVM 的堆内存,包括新生代和老年代。所以 Full GC 的代价相对较大,应尽量避免频繁触发。
注意:以上的阈值和频率取决于具体的 JVM 设置和系统环境,可能会因 JVM 的不同版本和配置有所不同。
2、动态对象年龄判断
在幸存者区下面的某一个年龄以及一下的对象已经占据了整个幸存者区的一半以上,就将大于等于这个年龄的对象全部放到老年代。
老年代的GC就是判断老年代中的对象或者数据是不是垃圾,如果是的话就清理掉。
堆中的GC!!
标记清除
标记清除-老年代使用的cms用的就是这个
标记清除就是先对垃圾进行标记在进行清除 ,但是如图所示可以看到在内存中留下了很多空洞。
cms将数据清除整合了四个步骤
补充:GC判断垃圾的方式--可达性分析算法:
从GC-root开始用一条链路来将整个对象的引用图遍历出来,判断出有没有没有被引用的孤岛,那么就视为垃圾
如图所示的E就被判断成为了垃圾
1、初始标记
在我们去寻找最开始的gc-root的时候就需要去stw,但是这个停顿的时间是非常快的,所以对于用户和程序的感知不大,gc-root的由来就是应用的开始。
三色标记法
一个节点被标记状态为:白-灰-黑
要注意因为我画图没办法并发画的问题在前一个节点变成黑色的同时下一个节点就已经变成黑色了。
白色:代表没有经过标记
灰色:正在标记
黑色:标记完成
出现问题:浮动垃圾、对象消失
2、并发标记
如果是单线程那自然不会出现很多问题,但是实际情况就是在GC标记的过程中用户线程对于引用的关系是随时改变的,这就出现了下面的两种问题!
浮动垃圾-出现场景:
因为在标记是按照指针有序的进行的,在进行到一个节点的时候这个时候该节点突然释放掉了一个引用节点的标记引用,这个时候就会造成浮动垃圾
所以我们一会要研究一下jvm是如何解决的,其实这种情况问题不大,因为下一轮可达性算法的时候这个C就会被带走。
对象消失-出现场景:
这个是因为对象的引用关系进行更改变动导致的,会造成在系统中所引用的对象突然消失。
过程描述:
在可达性算法进行的时候在扫描到B对象的时候B对象突然释放掉了对于C对象的引用,同时A对象对C对象进行了引用,因为可达性算法是不可逆的,所以在接下来C就不会再被标记。就在GC中被删除掉了,对于整个程序来说C就突然消失了。这个情况是很恐怖的。
3、重新标记
主要解决上面的对象消失问题:
根据动图演示发现,首先是B要对C进行释放,然后A再和C进行连接,那么我只需要破坏掉中间的一个条件就可以避免这个事情的发生。
解决方式:
1、增量更新
使用类似于aop的概念完成了一次写隔离
我们对这个行为做一个记录,然后根据A作为根节点在进行一次扫描。注意在这个时候,因为如果其他的对象的引用再去改变,那不就扫描不完了?所以在这个是需要时间暂停也就是stw(世界停止)。
2、原始快照
在我开始遍历的时候就给整个对象引用图来一个快照,类似于mysql中的可重复读(快照读),
不管用户线程如何变化,在遍历的时候还是按照我保存的快照去走。
就算按照我们的场景B释放了对C的引用但是我还是给你标记下来。我给你B的删除动作无视掉掉了,就类似于mysql中的幻读。保证了我每次在遍历可达性的引用表的时候,我的这个表的稳定性。
同时因为保存了快照这个时候也是要时间停止的。STW(stop the world)!!
4、并发删除
并发的去删除掉没有被可达性算法达到的对象
标记复制
就是将垃圾放在一侧统一删掉,优点可见,但是肉眼可见的少了一半的内存空间。
标记整理
就是将内存先去标记然后移动整理,最后在进行垃圾的删除,是前两个的优化,解决了前两个的问题,但是出现了更多了逻辑复杂度以及涉及到位置的移动所以复杂度更高。
在jvm中的STW问题:世界停止
在 JVM 中,STW(Stop-The-World)是一个重要的概念,它指的是在垃圾回收(GC)期间,所有应用程序线程被暂停(Stopped)的阶段。STW 期间,应用程序线程无法继续执行,GC 操作可以顺利进行,但会带来一定的性能开销。
STW 问题主要出现在 CMS(Concurrent Mark Sweep)垃圾回收器中,它是一种并发的标记清除垃圾回收算法,主要适用于老年代(Old Generation)的垃圾回收。在 CMS 垃圾回收过程中,主要分为四个阶段:
- 初始标记(Initial Mark):这个阶段需要 STW,目的是标记出整个堆中所有活跃对象。
- 并发标记(Concurrent Mark):这个阶段是并发的,垃圾回收器线程和应用程序线程同时运行,垃圾回收器线程对堆中的对象进行标记,应用程序线程可以继续执行。
- 重新标记(Remark):这个阶段也需要 STW,目的是修正并发标记期间由于应用程序线程的动态变化而导致的标记错误。
- 清理(Sweep):这个阶段是并发的,垃圾回收器线程和应用程序线程同时运行,垃圾回收器线程回收不再使用的内存空间,应用程序线程可以继续执行。
由于初始标记和重新标记阶段需要 STW,因此在 CMS 垃圾回收过程中,应用程序线程会不可避免地出现停顿。为了减少 STW 带来的影响,可以采用以下几种方法:
- 使用 CMS 垃圾回收器的参数设置并发比例(
-XX:+UseConcMarkSweepGC
和-XX:CMSInitiatingOccupancyFraction
等参数),尽量减少并发标记和重新标记阶段的 STW 时间。 - 使用 G1(Garbage-First)垃圾回收器,它是一种面向对象分配策略的垃圾回收器,通过并行、并发和延迟清理的方式尽可能地减少 STW 时间。
- 采用 JIT 编译优化技术,将热点代码编译成本地代码,减少 STW 期间应用程序线程的停顿时间。
- 尽量避免分配大量短期对象,以减少垃圾回收的频率和 STW 的时间。
- 采用分布式架构或容器化技术,将应用程序部署在多个节点上,利用负载均衡等技术分担 STW 的压力。
总之,STW 是垃圾回收过程中不可避免的一个环节,但可以通过一些方法来降低它对应用程序性能的影响。
1、为什么堆里要分新生代和老年代?
分代假说:
1、绝大多数对象都是朝生夕灭的
2、熬过越多次垃圾收集过程的对象就越难以消亡
分区是为了优化GC!!!
2、GC线程是如何和用户线程并行的?
cms使用了四层步骤
根据上面文章的讲述可以知道
1、初始标记 2、并发标记 3、重新标记 3、并发删除
通过这四步来完成的!!!
3、GC-root是如何枚举的?
普通对象指针(Ordinary Object Pointer)维护GC -root
意思就是说,我们不在进行每次需要信息的是再去重新查询了,而是这里使用了预加载追加存放map的方式来管理,需要的时候直接返回输出整个map。进行追加的计算。预计算这个设计的思想在大数据中还是很常见的。