如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize()方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。
对象重生或死亡
如果在执行 finalize() 方法时,将 this 赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。
任何一个对象的 finalize() 方法只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会被再次执行,想继续在 finalize() 中自救就失效了。
方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:
-
废弃常量
-
无用的类
判定废弃常量
只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。比如,一个字符串 “bingo” 进入了常量池,但是当前系统没有任何一个 String 对象引用常量池中的 “bingo” 常量,也没有其它地方引用这个字面量,必要的话,"bingo"常量会被清理出常量池。
判定无用的类
判定一个类是否是“无用的类”,条件较为苛刻。
-
该类的所有对象都已经被清除
-
加载该类的 ClassLoader 已经被回收
-
该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。
学会了如何判定无效对象、无用类、废弃常量之后,剩余工作就是回收这些垃圾。常见的垃圾收集算法有以下几个:
标记-清除算法
标记的过程是:遍历所有的 GC Roots
,然后将所有 GC Roots
可达的对象标记为存活的对象。
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。
这种方法有两个不足:
-
效率问题:标记和清除两个过程的效率都不高。
-
空间问题:标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(新生代)
为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。这种算法有优有劣:
-
优点:不会有内存碎片的问题。
-
缺点:内存缩小为原来的一半,浪费空间。
为了解决空间利用率问题,可以将内存分为三块: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor。回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才使用的 Survivor 空间。这样只有 10% 的内存被浪费。
但是我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够,需要依赖其他内存(指老年代)进行分配担保。
分配担保
为对象分配内存空间时,如果 Eden+Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入 Eden 区。
标记-整理算法(老年代)
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历 GC Roots
,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。
分代收集算法
根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。
-
新生代:复制算法
-
老年代:标记-清除算法、标记-整理算法
1、标记清除
优点:基于最基础的可达性分析算法,它是最基础的收集算法;而后续的收集算法都是基于这种思路并对其不足进行改进得到的;
缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量不连续的内存碎片;这会导致分配大内存对象时,无法找到足够的连续内存;从而需要提前触发另一次垃圾收集动作;
缺点:
2次扫描 一次标记 一次清除
有内存碎片
2、复制算法
优点:
使得每次都是只对整个半区进行内存回收;内存分配时也不用考虑内存碎片等问题;实现简单,运行高效;
缺点:
空间浪费;效率随对象存活率升高而变低;
HotSpot虚拟机复制算法过程
-
将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;
-
每次使用Eden和其中一块Survivor;
-
当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;
-
而后清理掉Eden和使用过的Survivor空间;
-
后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;
默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;
分配担保机制
如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制(Handle Promotion)进入老年代;
3、标记压缩
优点:不会产生内存碎片;
缺点:增加了对存活对象需要整理的过程,效率更低;
4、分代收集算法
“分代收集”(Generational Collection)算法结合不同的收集算法处理不同区域。
新生代:每次垃圾收集都有大批对象死去,只有少量存活;所以可采用复制算法;
老年代:对象存活率高,没有额外的空间可以分配担保;使用"标记-清理"或"标记-整理"算法;
优点:根据各个年代的特点采用最适当的收集算法;
缺点:仍然不能控制每次垃圾收集的时间;
https://github.com/doocs/jvm/blob/main/docs/04-hotspot-gc.md
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
CMS收集器
老年代、"标记-清除"算法(不进行压缩操作,产生内存碎片)、并发收集、低停顿
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
CMS收集器3个明显的缺点:
对CPU资源非常敏感;
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败;
产生大量内存碎片;
https://blog.csdn.net/zqz_zqz/article/details/70568819
哪些情况下对象内存分配会直接进入老年代?
-
当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);Minor GC时,如果发现存活的对象无法全部放入Survivor空间,只好通过分配担保机制提前转移到老年代。
-
需要大量连续内存空间的Java大对象会直接进入老年代,容易提前触发老年代GC;
-
经过多次Minor GC,如果年龄达到一定程度,就晋升到老年代;
-
动态对象年龄判定:如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代;
方法区中可回收哪些对象?
-
废弃常量:与回收Java堆中对象非常类似;
-
无用的类:
-
(1)该类所有实例都已经被回收(即Java椎中不存在该类的任何实例);
-
(2)加载该类的ClassLoader已经被回收,也即通过引导程序加载器加载的类不能被回收;
-
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
JDK HotSpot虚拟机方法区调整
在JDK7中,使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;
在JDK8中,永久代已被删除,类元数据(Class Metadata)存储空间直接在本地内存中分配;
==========================================================================
在 HotSpot 虚拟机中,对象的内存布局分为以下 3 块区域:
-
对象头(Header)
-
实例数据(Instance Data)
-
对齐填充(Padding)
对象头
对象头记录了对象在运行过程中所需要使用的一些数据:
-
哈希码
-
GC 分代年龄
-
锁状态标志
-
线程持有的锁
-
偏向线程 ID
-
偏向时间戳
对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。
实例数据
实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。
对齐填充
用于确保对象的总长度为 8 字节的整数倍。
HotSpot VM 的自动内存管理系统要求对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对齐填充并不是必然存在,也没有特别的含义,它仅仅起着占位符的作用。
类加载检查
虚拟机在解析.class
文件时,若遇到一条 new 指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。
为新生对象分配内存
对象所需内存的大小在类加载完成后便可完全确定,接下来从堆中划分一块对应大小的内存空间给新的对象。分配堆中内存有两种方式:
- 指针碰撞
如果 Java 堆中内存绝对规整(说明采用的是“复制算法”或“标记整理法”),空闲内存和已使用内存中间放着一个指针作为分界点指示器,那么分配内存时只需要把指针向空闲内存挪动一段与对象大小一样的距离,这种分配方式称为“指针碰撞”。
- 空闲列表
如果 Java 堆中内存并不规整,已使用的内存和空闲内存交错(说明采用的是标记-清除法,有碎片),此时没法简单进行指针碰撞, VM 必须维护一个列表,记录其中哪些内存块空闲可用。分配之时从空闲列表中找到一块足够大的内存空间划分给对象实例。这种方式称为“空闲列表”。
初始化
分配完内存后,为对象中的成员变量赋上初始值,设置对象头信息,调用对象的构造函数方法进行初始化。
至此,整个对象的创建过程就完成了。
所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配的。也就是说在建立一个对象时两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。 那么根据引用存放的地址类型的不同,对象有不同的访问方式。
句柄访问方式
堆中需要有一块叫做“句柄池”的内存空间,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
引用类型的变量存放的是该对象的句柄地址(reference)。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址找到对象。
直接指针访问方式
引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。
需要说明的是,HotSpot 采用第二种方式,即直接指针方式来访问对象,只需要一次寻址操作,所以在性能上比句柄访问方式快一倍。但像上面所说,它需要额外的策略来存储对象在方法区中类信息的地址。
==================================================================
对象的内存分配,就是在堆上分配(也可能经过 JIT 编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代的 Eden 区上,少数情况下可能直接分配在老年代,分配规则不固定,取决于当前使用的垃圾收集器组合以及相关的参数配置。
以下列举几条最普遍的内存分配规则,供大家学习。
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
👇Minor GC vs Major GC/Full GC:
-
Minor GC:回收新生代(包括 Eden 和 Survivor 区域),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
-
Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。
在 JVM 规范中,Major GC 和 Full GC 都没有一个正式的定义,所以有人也简单地认为 Major GC 清理老年代,而 Full GC 清理整个内存堆。
大对象是指需要大量连续内存空间的 Java 对象,如很长的字符串或数据。
一个大对象能够存入 Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制。(还记得吗,新生代采用复制算法回收垃圾)
JVM 给每个对象定义了一个对象年龄计数器。当新生代发生一次 Minor GC 后,存活下来的对象年龄 +1,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去。
使用 -XXMaxTenuringThreshold 设置新生代的最大年龄,只要超过该参数的新生代对象都会被转移到老年代中去。
如果当前新生代的 Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
JDK 6 Update 24 之前的规则是这样的:
在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间, 如果这个条件成立,Minor GC 可以确保是安全的; 如果不成立,则虚拟机会查看 HandlePromotionFailure 值是否设置为允许担保失败, 如果是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的; 如果小于,或者 HandlePromotionFailure 设置不允许冒险,那此时也要改为进行一次 Full GC。
JDK 6 Update 24 之后的规则变为:
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保。
这个过程就是分配担保。
👇 总结一下有哪些情况可能会触发 JVM 进行 Full GC。
- System.gc() 方法的调用
此方法的调用是建议 JVM 进行 Full GC,注意这只是建议而非一定,但在很多情况下它会触发 Full GC,从而增加 Full GC 的频率。通常情况下我们只需要让虚拟机自己去管理内存即可,我们可以通过 -XX:+ DisableExplicitGC 来禁止调用 System.gc()。
- 老年代空间不足
老年代空间不足会触发 Full GC 操作,若进行该操作后空间依然不足,则会抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
- 永久代空间不足
JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中也称为永久代(Permanet Generation),存放一些类信息、常量、静态变量等数据,当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,会触发 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
- CMS GC 时出现 promotion failed 和 concurrent mode failure
promotion failed,就是上文所说的担保失败,而 concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。
- 统计得到的 Minor GC 晋升到旧生代的平均大小大于老年代的剩余空间
==============================================================
谈论 JVM 的无关性,主要有以下两个:
-
平台无关性:任何操作系统都能运行 Java 代码
-
语言无关性: JVM 能运行除 Java 以外的其他代码
Java 源代码首先需要使用 Javac 编译器编译成 .class 文件,然后由 JVM 执行 .class 文件,从而程序开始运行。
JVM 只认识 .class 文件,它不关心是何种语言生成了 .class 文件,只要 .class 文件符合 JVM 的规范就能运行。 目前已经有 JRuby、Jython、Scala 等语言能够在 JVM 上运行。它们有各自的语法规则,不过它们的编译器 都能将各自的源码编译成符合 JVM 规范的 .class 文件,从而能够借助 JVM 运行它们。
Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的, 因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更加强大。 因此,有一些 Java 语言本身无法有效支持的语言特性,不代表字节码本身无法有效支持。
Class 文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全都是连续的 0/1。Class 文件 中的所有内容被分为两种类型:无符号数、表。
-
无符号数 无符号数表示 Class 文件中的值,这些值没有任何类型,但有不同的长度。u1、u2、u4、u8 分别代表 1/2/4/8 字节的无符号数。
-
表 由多个无符号数或者其他表作为数据项构成的复合数据类型。
Class 文件具体由以下几个构成:
-
魔数
-
版本信息
-
常量池
-
访问标志
-
类索引、父类索引、接口索引集合
-
字段表集合
-
方法表集合
-
属性表集合
魔数
Class 文件的头 4 个字节称为魔数,用来表示这个 Class 文件的类型。
Class 文件的魔数是用 16 进制表示的“CAFE BABE”,是不是很具有浪漫色彩?
魔数相当于文件后缀名,只不过后缀名容易被修改,不安全,因此在 Class 文件中标识文件类型比较合适。
版本信息
紧接着魔数的 4 个字节是版本信息,5-6 字节表示次版本号,7-8 字节表示主版本号,它们表示当前 Class 文件中使用的是哪个版本的 JDK。
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必需拒绝执行超过其版本号的 Class 文件。
常量池
版本信息之后就是常量池,常量池中存放两种类型的常量:
- 字面值常量
字面值常量就是我们在程序中定义的字符串、被 final 修饰的值。
- 符号引用
符号引用就是我们定义的各种名字:类和接口的全限定名、字段的名字和描述符、方法的名字和描述符。
常量池的特点
-
常量池中常量数量不固定,因此常量池开头放置一个 u2 类型的无符号数,用来存储当前常量池的容量。
-
常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量属于哪种常量类型。
常量池中常量类型
| 类型 | tag | 描述 |
| — | — | — |
| CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 |
| CONSTANT_Integer_info | 3 | 整型字面量 |
| CONSTANT_Float_info | 4 | 浮点型字面量 |
| CONSTANT_Long_info | 5 | 长整型字面量 |
| CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
| CONSTANT_Class_info | 7 | 类或接口的符号引用 |
| CONSTANT_String_info | 8 | 字符串类型字面量 |
| CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
| CONSTANT_MethodType_info | 16 | 标识方法类型 |
| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
对于 CONSTANT_Class_info(此类型的常量代表一个类或者接口的符号引用),它的二维表结构如下:
| 类型 | 名称 | 数量 |
| — | — | — |
| u1 | tag | 1 |
| u2 | name_index | 1 |
tag 是标志位,用于区分常量类型;name_index 是一个索引值,它指向常量池中一个 CONSTANT_Utf8_info 类型常量,此常量代表这个类(或接口)的全限定名,这里 name_index 值若为 0x0002,也即是指向了常量池中的第二项常量。
CONSTANT_Utf8_info 型常量的结构如下:
| 类型 | 名称 | 数量 |
| — | — | — |
| u1 | tag | 1 |
| u2 | length | 1 |
| u1 | bytes | length |
tag 是当前常量的类型;length 表示这个字符串的长度;bytes 是这个字符串的内容(采用缩略的 UTF8 编码)
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否被 abstract/final 修饰。
类索引、父类索引、接口索引集合
类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
由于 Java 不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。一个类可能实现了多个接口,因此用接口索引集合来描述。这个集合第一项为 u2 类型的数据,表示索引表的容量,接下来就是接口的名字索引。
类索引和父类索引用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过该常量总的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
字段表集合
字段表集合存储本类涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。
每一个字段表只表示一个成员变量,本类中的所有成员变量构成了字段表集合。字段表结构如下:
| 类型 | 名称 | 数量 | 说明 |
| — | — | — | — |
| u2 | access_flags | 1 | 字段的访问标志,与类稍有不同 |
| u2 | name_index | 1 | 字段名字的索引 |
| u2 | descriptor_index | 1 | 描述符,用于描述字段的数据类型。 基本数据类型用大写字母表示; 对象类型用“L 对象类型的全限定名”表示。 |
| u2 | attributes_count | 1 | 属性表集合的长度 |
| u2 | attributes | attributes_count | 属性表集合,用于存放属性的额外信息,如属性的值。 |
字段表集合中不会出现从父类(或接口)中继承而来的字段,但有可能出现原本 Java 代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
方法表集合
方法表结构与属性表类似。
volatile 关键字 和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT 标志。
方法表的属性表集合中有一张 Code 属性表,用于存储当前方法经编译器编译后的字节码指令。
属性表集合
每个属性对应一张属性表,属性表的结构如下:
| 类型 | 名称 | 数量 |
| — | — | — |
| u2 | attribute_name_index | 1 |
| u4 | attribute_length | 1 |
| u1 | info | attribute_length |
=================================================================
在高性能硬件上部署程序,目前主要有两种方式:
-
通过 64 位 JDK 来使用大内存;
-
使用若干个 32 位虚拟机建立逻辑集群来利用硬件资源。
堆内存变大后,虽然垃圾收集的频率减少了,但每次垃圾回收的时间变长。 如果堆内存为 14 G,那么每次 Full GC 将长达数十秒。如果 Full GC 频繁发生,那么对于一个网站来说是无法忍受的。
对于用户交互性强、对停顿时间敏感的系统,可以给 Java 虚拟机分配超大堆的前提是有把握把应用程序的 Full GC 频率控制得足够低,至少要低到不会影响用户使用。
可能面临的问题:
-
内存回收导致的长时间停顿;
-
现阶段,64 位 JDK 的性能普遍比 32 位 JDK 低;
-
需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎就无法产生堆转储快照(因为要产生超过 10GB 的 Dump 文件),哪怕产生了快照也几乎无法进行分析;
-
相同程序在 64 位 JDK 消耗的内存一般比 32 位 JDK 大,这是由于指针膨胀,以及数据类型对齐补白等因素导致的。
在一台物理机器上启动多个应用服务器进程,每个服务器进程分配不同端口, 然后在前端搭建一个负载均衡器,以反向代理的方式来分配访问请求。
考虑到在一台物理机器上建立逻辑集群的目的仅仅是为了尽可能利用硬件资源,并不需要关心状态保留、热转移之类的高可用性能需求, 也不需要保证每个虚拟机进程有绝对的均衡负载,因此使用无 Session 复制的亲合式集群是一个不错的选择。 我们仅仅需要保障集群具备亲合性,也就是均衡器按一定的规则算法(一般根据 SessionID 分配) 将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可。
可能遇到的问题:
-
尽量避免节点竞争全局资源,如磁盘竞争,各个节点如果同时访问某个磁盘文件的话,很可能导致 IO 异常;
-
很难高效利用资源池,如连接池,一般都是在节点建立自己独立的连接池,这样有可能导致一些节点池满了而另外一些节点仍有较多空余;
-
各个节点受到 32 位的内存限制;
-
大量使用本地缓存的应用,在逻辑集群中会造成较大的内存浪费,因为每个逻辑节点都有一份缓存,这时候可以考虑把本地缓存改成集中式缓存。
场景描述
一个小型系统,使用 32 位 JDK,4G 内存,测试期间发现服务端不定时抛出内存溢出异常。 加入 -XX:+HeapDumpOnOutOfMemoryError(添加这个参数后,堆内存溢出时就会输出异常日志), 但再次发生内存溢出时,没有生成相关异常日志。
分析
在 32 位 JDK 上,1.6G 分配给堆,还有一部分分配给 JVM 的其他内存,直接内存最大也只能在剩余的 0.4G 空间中分出一部分, 如果使用了 NIO,JVM 会在 JVM 内存之外分配内存空间,那么就要小心“直接内存”不足时发生内存溢出异常了。
直接内存的回收过程
直接内存虽然不是 JVM 内存空间,但它的垃圾回收也由 JVM 负责。
垃圾收集进行时,虚拟机虽然会对直接内存进行回收, 但是直接内存却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收, 它只能等老年代满了后 Full GC,然后“顺便”帮它清理掉内存的废弃对象。 否则只能一直等到抛出内存溢出异常时,先 catch 掉,再在 catch 块里大喊 “System.gc()
”。 要是虚拟机还是不听,那就只能眼睁睁看着堆中还有许多空闲内存,自己却不得不抛出内存溢出异常了。
==============================================================
请描述下GC流程?或者一个对象从创建到GC的过程?
- 引用计数算法
优点:实现简单,判定高效;
缺点:很难解决对象之间相互循环引用的问题;
- 可达性分析算法
通过一系列"GC Roots"对象作为起始点,开始向下搜索,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到这个对象不可达),则证明该对象是不可用的;
优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;
缺点:实现比较复杂;需要分析大量数据,消耗大量时间;分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题);
-
虚拟机栈 (栈帧中本地变量表)中引用的对象;
-
方法区中 类静态属性引用的对象 ;
-
方法区中 常量引用的对象;
-
本地方法栈 JNI(Native方法)中引用的对象;
-
第一次标记:在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记;并且进行一次筛选:此对象是否必要执行finalize()方法;没有必要执行的情况,则标记对象已死;有必要执行的情况,则对象被放入F-Queue队列中;
-
第二次标记:GC将对F-Queue队列中的对象进行第二次小规模标记;finalize()方法是对象逃脱死亡的最后一次机会;一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;
因为其执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用);如果需要"释放资源",可以定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用;
如果有关键资源,必须显式的终止方法;一般情况下,应尽量避免使用它,甚至可以忘掉它;
运行中,非常多的指令都会导致引用关系变化;如果为这些指令都生成对应的OopMap,需要的空间成本太高;
只在特定的位置记录OopMap引用关系,这些位置称为安全点(Safepoint);
不能太少,否则GC等待时间太长;也不能太多,否则GC过于频繁,增大运行时负荷;
所以,基本上是以程序"是否具有让程序长时间执行的特征"为标准选定,如:方法调用、循环跳转、循环的末尾、异常跳转等;
只有具有这些功能的指令才会产生Safepoint;
抢先式中断(Preemptive Suspension):在GC发生时,首先中断所有线程;如果发现不在Safepoint上的线程,就恢复让其运行到Safepoint上;
主动式中断(Voluntary Suspension):在GC发生时,不直接操作线程中断,而是仅简单设置一个标志;让各线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起;
而轮询标志的地方和Safepoint是重合的;
线程不执行时没有CPU时间(Sleep或Blocked状态),无法运行到Safepoint上再中断挂起;
安全区域:指一段代码片段中,引用关系不会发生变化;在这个区域中的任意地方开始GC都是安全的;
线程执行进入Safe Region,首先标识自己已经进入Safe Region;
线程被唤醒离开Safe Region时,其需要检查系统是否已经完成根节点枚举(或整个GC);
如果已经完成,就继续执行;否则必须等待,直到收到可以安全离开Safe Region的信号通知,这样就不会影响标记结果;
优点:基于最基础的可达性分析算法,它是最基础的收集算法;而后续的收集算法都是基于这种思路并对其不足进行改进得到的;
缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量不连续的内存碎片;这会导致分配大内存对象时,无法找到足够的连续内存;从而需要提前触发另一次垃圾收集动作;
优点:使得每次都是只对整个半区进行内存回收;内存分配时也不用考虑内存碎片等问题;实现简单,运行高效;
缺点:空间浪费;效率随对象存活率升高而变低;
将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;
每次使用Eden和其中一块Survivor;
当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;
而后清理掉Eden和使用过的Survivor空间;
后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;
默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;
如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制(Handle Promotion)进入老年代;
优点:不会产生内存碎片;
缺点:增加了对存活对象需要整理的过程,效率更低;
“分代收集”(Generational Collection)算法结合不同的收集算法处理不同区域。
新生代:每次垃圾收集都有大批对象死去,只有少量存活;所以可采用复制算法;
老年代:对象存活率高,没有额外的空间可以分配担保;使用"标记-清理"或"标记-整理"算法;
优点:根据各个年代的特点采用最适当的收集算法;
缺点:仍然不能控制每次垃圾收集的时间;
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:
JVM收集器
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
新生代、复制算法、单线程收集;
缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";
Serial/Serial Old组合收集器运行示意图如下:
Serial/Serial Old组合
新生代、复制算法、多线程收集;
缺点:进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";
ParNew/Serial Old组合收集器运行示意图如下:
ParNew/Serial Old组合
新生代、复制算法、多线程收集;
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、单线程收集;
老年代、"标记-整理"算法(还有压缩,Mark-Sweep-Compact)、多线程收集;
老年代、"标记-清除"算法(不进行压缩操作,产生内存碎片)、并发收集、低停顿
CMS收集器运行示意图如下:
CMS收集器
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;
CMS收集器3个明显的缺点:
对CPU资源非常敏感;
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败;
产生大量内存碎片;
在堆上分配(JIT编译优化后可能在栈上分配),主要在新生代的Eden区中分配;
如果启用了本地线程分配缓冲,将线程优先在TLAB上分配;
少数情况下,可能直接分配在老年代中;
分配的细节取决于当前使用哪种垃圾收集器组合,以及JVM中内存相关参数设置;
当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);Minor GC时,如果发现存活的对象无法全部放入Survivor空间,只好通过分配担保机制提前转移到老年代。
需要大量连续内存空间的Java大对象会直接进入老年代,容易提前触发老年代GC;
经过多次Minor GC,如果年龄达到一定程度,就晋升到老年代;
动态对象年龄判定:如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代;
废弃常量:与回收Java堆中对象非常类似;
无用的类:(1)该类所有实例都已经被回收(即Java椎中不存在该类的任何实例);(2)加载该类的ClassLoader已经被回收,也即通过引导程序加载器加载的类不能被回收;(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
在JDK7中,使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;
在JDK8中,永久代已被删除,类元数据(Class Metadata)存储空间直接在本地内存中分配;
=================================================================
先记录下来,等会儿整理
Java 内存分配?
Java 堆的结构是什么样子的?
什么是堆中的永久代(Perm Gen space)?
说说各个区域的作用?
Java 中会存在内存泄漏吗,简述一下?
Java 类加载过程?
描述一下 JVM 加载 Class 文件的原理机制?
什么是类加载器?
类加载器有哪些?
什么是tomcat类加载机制?
类加载器双亲委派模型机制?
什么是GC? 为什么要有 GC?
简述一下Java 垃圾回收机制?
如何判断一个对象是否存活?
垃圾回收的优点和原理,并考虑 2 种回收机制?
垃圾回收器的基本原理是什么?
垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
深拷贝和浅拷贝?
System.gc() 和 Runtime.gc() 会做些什么?
什么是分布式垃圾回收(DGC)?它是如何工作的?
串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?
在 Java 中,对象什么时候可以被垃圾回收?
简述Minor GC 和 Major GC?
Java 中垃圾收集的方法有哪些?
讲讲你理解的性能评价及测试指标?
常用的性能优化方式有哪些?
说说分布式缓存和一致性哈希?
同步与异步?阻塞与非阻塞?
什么是GC调优?
常见异步的手段有哪些?
内存区域划分
OOM介绍
对象分配与回收
CMS和G1垃圾收集器
对象可达性分析
类加载机制
JVM调优参数
Full GC,Minor GC
对象的栈上分配(JIT编译器)
JVM内存模型(程序计数器、虚拟机栈、本地方法栈、堆、方法区)
JDK1.8做了哪些变化?(JDK1.7已经将原本位于永久代的字符串常量池移到堆中了,但是永久代的概念还存在,JDK1.8才彻底废除永久代,进而用元空间代替)
永久代和元空间,JDK1.8为什么要使用元空间代替永久代?
元空间溢出?(元空间不属于Java虚拟机,使用的是本地内存,存放的是类及方法的一些信息,动态加载类或者频繁加载类信息,但是没有及时卸载,会导致元空间溢出)
对象创建的两种方式(指针碰撞、空闲列表)、对象访问定位的两种方式(使用句柄、直接指针)
栈上分配与逃逸分析(JVM层面进行java性能优化的技巧)
判断对象是否存活的两种方式,引用计数法的缺点?(引用计数法、可达性分析法)
关于Object类的finalize()方法(jvm自动执行,无需手动调用,只能执行一次).
java的四种引用(强引用、软引用、弱引用、虚引用)
三种垃圾回收算法,各自的优缺点(标记-清除法、标记-复制法、标记-整理法)
Minor GC 和 Full GC 的区别,触发条件,以及空间分配担保策略?
内存溢出和内存泄露(内存泄露的堆积会导致内存溢出)
JVM参数调优(-Xms、-Xmx、-Xss、-XX:NewRatio、-XX:SurvivorRatio、-XX:+PrintGCDetails、-HeapDumpOnOutOfMemory)
发生OOM如何解决(首先尝试通过JVM参数调优扩大堆内存空间;再者dump出堆内存存储快照,使用JProfile工具进行分析)
垃圾收集器(CMS问的居多,另外,如果谈及发生gc会给用户带来什么不好的体验,可以谈谈Stop the World)
类加载机制的过程,准备阶段做了哪些工作?(准备阶段会给类的静态变量分配内存(方法区)并赋初值,如果类的静态变量被final修饰,那么初始化的值就不是零值,而是声明的值)
类的双亲委派模型定义,双亲委派模型的好处?如何破坏类的双亲委派模型?
jvm底层怎么保证的new一个对象,不会被多个线程影响
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/5883498b0e92604328daff3d46d22ba6.jpeg)
读者福利
由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴
更多笔记分享
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
活的两种方式,引用计数法的缺点?(引用计数法、可达性分析法)
关于Object类的finalize()方法(jvm自动执行,无需手动调用,只能执行一次).
java的四种引用(强引用、软引用、弱引用、虚引用)
三种垃圾回收算法,各自的优缺点(标记-清除法、标记-复制法、标记-整理法)
Minor GC 和 Full GC 的区别,触发条件,以及空间分配担保策略?
内存溢出和内存泄露(内存泄露的堆积会导致内存溢出)
JVM参数调优(-Xms、-Xmx、-Xss、-XX:NewRatio、-XX:SurvivorRatio、-XX:+PrintGCDetails、-HeapDumpOnOutOfMemory)
发生OOM如何解决(首先尝试通过JVM参数调优扩大堆内存空间;再者dump出堆内存存储快照,使用JProfile工具进行分析)
垃圾收集器(CMS问的居多,另外,如果谈及发生gc会给用户带来什么不好的体验,可以谈谈Stop the World)
类加载机制的过程,准备阶段做了哪些工作?(准备阶段会给类的静态变量分配内存(方法区)并赋初值,如果类的静态变量被final修饰,那么初始化的值就不是零值,而是声明的值)
类的双亲委派模型定义,双亲委派模型的好处?如何破坏类的双亲委派模型?
jvm底层怎么保证的new一个对象,不会被多个线程影响
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-U6bWY1UQ-1712160478158)]
[外链图片转存中…(img-GQ9zWI74-1712160478158)]
[外链图片转存中…(img-QWeQXVxY-1712160478159)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/5883498b0e92604328daff3d46d22ba6.jpeg)
读者福利
由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴
[外链图片转存中…(img-bgUtrAn6-1712160478159)]
[外链图片转存中…(img-roqWjuZe-1712160478159)]
[外链图片转存中…(img-rjGStoiY-1712160478159)]
更多笔记分享
[外链图片转存中…(img-ic3Movrv-1712160478160)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!