赞
踩
目录
对象头了解吗? mark word(hashcode、分代、锁标志位)、指向类信息的指针和数组长度(数组才有)
JVM 里 new 对象时,堆会发生抢占吗?JVM 是怎么设计来保证线程安全的?
有哪些GC算法? (可达性分析)标记清除算法(内存碎片)->复制算法(内存利用低)->标记整理算法->分代回收
Parallel Scavenge 收集器 (新生代、多线程 、复制、 关注垃圾回收吞吐量)
G1收集器(高吞吐低停顿的平衡) 分代回收, 标记整理+复制
Java程序会通过栈上的引用操作堆上的具体对象,对象的访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。
句柄 : 堆会划分出一块内存作为句柄池,引用中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。
优点是引用中存储的是稳定句柄地址,在GC过程中对象被移动时只会改变句柄的实例数据指针,而引用本身不需要修改。
直接指针 :引用中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。
使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销
虚拟机hotspot主要是使用直接指针来访问对象。
JDK1.2后对引用进行了扩充,按强度分为四种:
强引用:最常见的引用,例如Object obj - new Object()就属于强引用。只要对象有强引用指向且GC Roots可达,在内存回收时即使濒临内存耗尽也不会被回收。
软引用:弱于强引用,描述非必需对象。在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。
弱引用:弱于软引用,描述非必需对象。弱引用关联的对象只能生存到下次YGC(Minor GC)前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于YGC具有不确定性,因此弱引用何时被回收也不确定。
虚引用:最弱的引用,定义完成后无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列。
虚引用看起来和弱引用没啥区别,只是必须搭配ReferenceQueue。用虚引用的目的一般是跟踪对象被回收的活动。
引用计数:在对象中添加一个引用计数器,如果被引用,计数器加1,引用失效计数器减1,如果计数器为0则被标记为垃圾。原理简单,效率高,但是在Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。
可达性分析: 主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过称为 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。
可作为 GC Roots的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。 (引用的对象)
所谓的 GC Roots,就是一组必须活跃的引用,不是对象,它们是程序运行时的起点,是一切引用链的源头。
在 Java 中,GC Roots 包括以下几种:
在 HotSpot 中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。
CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,降低了 CPU 的访问效率。
内存对齐的主要作用是:
一般来说,对象的大小是由对象头、实例数据和对齐填充三个部分组成的。
new Object()
来说,Object 类本身没有实例字段,因此这部分可能非常小或者为零。一般来说,目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的,因此在 64 位 JVM 上,new Object()
的大小是 16 字节(12 字节的对象头 (8+4)+ 4 字节的对齐填充)。
在 64 位 JVM 上,未开启压缩指针时,对象引用占用 8 字节;开启压缩指针时,对象引用可被压缩到 4 字节。
而 HotSpot JVM 默认开启了压缩指针,因此在 64 位 JVM 上,对象引用占用 4 字节。
在Java中创建对象的过程包括以下几个步骤:
对象的生命周期包括创建、使用和销毁三个阶段:
在堆内存分配对象时,主要使用两种策略:指针碰撞和空闲列表。
①、指针碰撞(Bump the Pointer)
假设堆内存是一个连续的空间,分为两个部分,一部分是已经被使用的内存,另一部分是未被使用的内存。
在分配内存时,Java 虚拟机维护一个指针,指向下一个可用的内存地址,每次分配内存时,只需要将指针向后移动(碰撞)一段距离,然后将这段内存分配给对象实例即可。
②、空闲列表(Free List)
JVM 维护一个列表,记录堆中所有未占用的内存块,每个空间块都记录了大小和地址信息。
当有新的对象请求内存时,JVM 会遍历空闲列表,寻找足够大的空间来存放新对象。
分配后,如果选中的空闲块未被完全利用,剩余的部分会作为一个新的空闲块加入到空闲列表中。
指针碰撞适用于管理简单、碎片化较少的内存区域(如年轻代),而空闲列表适用于内存碎片化较严重或对象大小差异较大的场景(如老年代)。
会,假设 JVM 虚拟机上,每一次 new 对象时,指针就会向右移动一个对象 size 的距离,一个线程正在给 A 对象分配内存,指针还没有来的及修改,另一个为 B 对象分配内存的线程,又引用了这个指针来分配内存,这就发生了抢占。
有两种可选方案来解决这个问题:
对于Minor GC,其触发条件比较简单,当Eden 空间满时,就将触发一次Minor GC。
而Full GC触发条件相对复杂,有以下情况会发生 full GC:
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。
除此之外,可以通过 -Xmn参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
在JDK 1.7及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些Class的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC 的情况下也会执行 Full GC。如果经过 Full GC仍然回收不了,那么虚拟机抛出 java.lang.OutOfMemoryError。
Java的内存回收机制基于自动内存管理,开发人员无需手动释放内存。垃圾回收器会自动识别不再使用的对象,并回收它们所占用的内存空间。
垃圾回收算法主要有 :
新生代的垃圾收集主要采用标记-复制算法,因为新生代的存活对象比较少,每次复制少量的存活对象效率比较高。
基于这种算法,虚拟机将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。默认 Eden 和 Survivor 的大小比例是 8∶1。
垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial old、 Parallel old、CMS、G1.
单线程收集器,使用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其他所有的工作线程(Stop The World),直到它收集结束。
特点:简单高效;内存消耗小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。
Serial 是虚拟机在客户端模式的默认新生代收集器
Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为、参数与 Serial 收集器基本一致。
新生代收集器,基于复制算法实现的收集器。特点是吞吐量优先,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。
Parallel Scavenge收集器关注点是吞吐量,高效率的利用 CPU资源。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的
-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
Parallel Scavenge收集器的老年代版本。多线程垃圾收集,使用标记整理算法。
初始标记和重新标记会 STW,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除。
Concurrent Mark Sweep,并发标记清除,追求获取最短停顿时间,实现了让垃圾收集线程与用户线程基本上同时工作。
CMS垃圾收集器关注点更多的是用户线程的停顿时间。
CMS垃圾回收基于标记清除算法实现,整个过程分为四个步骤:
在整个过程中,耗时最长的是并发标记和并发清除阶段,这两个阶段垃圾收集线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:并发收集,停顿时间短。
缺点:
G1(Garbage-First Garbage Collector)在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。G1 有五个属性:分代、增量、并行、标记整理、STW。
G1垃圾收集器的目标是在不同应用场景中追求高吞吐量和低停顿之间的最佳平衡。
G1将整个堆分成相同大小的分区 ( Region),有四种不同类型的分区:Eden、 Survivor、Old和Humongous。分区的大小取值范围为1M到 32M,都是2的幂次方。
分区大小可以通过 -XX:G1HeapRegionSize 参数指定。
Humongous区域用于存储大对象。G1规定只要大小超过了一个分区容量一半的对象就认为是大对象。
G1收集器对各个分区回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大回收停顿时间,优先回收价值最大的分区。也是名字的由来(Garbage-First)
特点:可以由用户指定期望的垃圾收集停顿时间。
G1收集器的回收过程分为以下几个步骤:
G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。
这里的操作涉及存活对象的移动,会暂停用户线程,由多条收集器线程并行完成。
ZGC 是 JDK 11 时引入的一款低延迟的垃圾收集器,它的目标是在不超过 10ms 的停顿时间内,为堆大小达到 16TB 的应用提供一种高吞吐量的垃圾收集器。
ZGC 的两个关键技术:指针染色和读屏障,不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在 ZGC 中,只需要设置指针地址的第 42-45 位即可,并且因为是寄存器访问,所以速度比访问内存更快。
我们生产环境中采用了设计比较优秀的 G1 垃圾收集器,G1 采用的是分区式标记-整理算法,将堆划分为多个区域,按需回收,适用于大内存和多核环境,能够同时考虑吞吐量和暂停时间。
或者:
我们系统采用的是 CMS 收集器,CMS 采用的是标记-清除算法,能够并发标记和清除垃圾,减少暂停时间,适用于对延迟敏感的应用。
再或者:
我们系统采用的是 Parallel 收集器,Parallel 采用的是年轻代使用复制算法,老年代使用标记-整理算法,适用于高吞吐量要求的应用。
G1回收器的特色在于它将堆内存划分成多个大小相等的独立区域,并且通过维护一个优先列表来进行局部区域的垃圾收集,从而减少全堆垃圾收集的频率和停顿时间。G1也特别注重停顿时间的可预测性,并允许用户指定期望的停顿时间目标。
主要的垃圾收集活动确实发生在堆内存中,因为这是大多数Java对象存活和死亡的地方。不过,方法区也是垃圾收集的目标之一,例如回收废弃常量和无用的类。
程序计数器、虚拟机栈和本地方法栈通常随线程而生,随线程而灭,所以它们不是垃圾收集的目标。
CMS 适用于对延迟敏感的应用场景,主要目标是减少停顿时间,但容易产生内存碎片。G1 则提供了更好的停顿时间预测和内存压缩能力,适用于大内存和多核处理器环境。
这里简单地列一下上面提到的一些收集器的适用场景:
自己整理,借鉴很多博主,感谢他们
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。