2.1 揭开 JVM 内存分配与回收的神秘面纱
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。
2.1.1 对象优先在 eden 区分配
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.
- 新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
- 老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上
2.1.2 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)
2.1.3 长期存活的对象将进入老年代
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
2.1.4 动态对象年龄判定
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一
半,年龄大于或等于该年龄的对象可以直接进入老年代
2.2 对象已经死亡?
2.2.1 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0
的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是难以解决循环引用问题。
2.2.2 可达性分析法
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。
可作为 GC Roots 的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
2.2.3 再谈引用
强引用
类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。
软引用
如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
弱引用
在垃圾收集器工作时,无论内存是否足够都会回收只被弱引用关联的对象。
虚引用
在任何时候都可能被垃圾回收。设置虚引用的目的就是能在对象被收集器回收时收到一个系统通知。
2.2.4 不可达的对象并非“非死不可”
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,他们处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
2.2.5 如何判断一个常量是废弃常量
假如常量池中存在一个"abc",当前栈中没有它的引用,就说明是废弃常量。
2.2.6 如何判断一个类是无用的类
需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
2.3 垃圾收集算法
2.3.1 标记 —— 清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
两个明显的问题:
- 效率不高
- 空间会产生大量碎片
2.3.2 复制算法
把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。
2.3.3 标记-整理算法
针对老年代的特点,过程仍然与“标记-清除”算法一样,但后续是把存活对象移到内存的另一端。
2.3.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为新生代和老年代,再选择合适的垃圾收集算法。
新生代每次垃圾回收都有大量对象死去,只有少量存活,复制算法。
老年代的对象存活几率比较高,而且没有额外的空间对它进行分配担保,所以选择**“标记-清除”或“标记-整理”**算法进行垃圾收集。
2.4 垃圾收集器
垃圾回收算法是内存回收的理论,而垃圾回收器是内存回收的实践。根据具体应用场景选择适合自己的垃圾收集器
2.4.1 Serial 收集器
单线程收集器。只会使用一条垃圾收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程。
2.4.2 ParNew 收集器
Serial 收集器的多线程版本。
并行(Parallel):指多条垃圾收集线程并行工作,此时用户线程处于等待状态
并发(Concurrent):指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,垃圾回收线程在另一个 CPU 上运行。
2.4.3 Parallel Scavenge 收集器
使用复制算法的多线程收集器
区别:
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
吞吐量就是 CPU 中 运行用户代码的时间/CPU总消耗时间。
2.4.4.Serial Old 收集器
Serial收集器的老年代版本,使用 “标记-整理”算法的单线程收集器。
2.4.5 Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用“标记-整理”算法的多线程收集器。
2.4.6 CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的并发收集器。使用“标记-清除”算法。非常符合在注重用户体验的应用上使用。
运作过程:
- 初始标记(CMS initial mark):标记 与GC Roots 关联的对象
- 并发标记(CMS concurrent mark):开启 GC 和用户线程,用一个闭包结构去记录可达对象
- 重新标记(CMS remark):修正并发标记期间的变动部分
- 并发清除(CMS concurrent sweep):开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
缺点:对 CPU 资源敏感,无法处理浮动垃圾,“标记-清除”算法会导致收集结束时有大量空间碎片产生。
2.4.7 G1 收集器
面向服务器的垃圾收集器,主要针对有多颗处理器和大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:初始标记、并发标记、最终标记、筛选回收。
3. 类文件结构
3.1 概述
下图展示了不同的语言被不同的编译器编译成 .class 文件最终运行在 Java 虚拟机之上。 .class 文件的二进制格式可以使用 [WinHex]查看
3.2 Class 文件结构总结
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合 }
}
Class文件字节码结构组织示意图:
3.2.1 魔数
> u4 magic; //Class 文件的标志
每个 Class 文件的头四个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。
3.2.2 Class 文件版本
> u2 minor_version;//Class 的小版本号
> u2 major_version;//Class 的大版本号
紧接着魔数的四个字节存储的是 Class 文件的版本号:第五和第六是次版本号,第七和第八是主版本号。
3.2.3 常量池
> u2 constant_pool_count;//常量池的数量
> cp_info constant_pool[constant_pool_count-1];//常量池
紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1(常量池计数器是从1开始计数的,索引值为0代表“不引用任何一个常量池项”)。
3.2.4 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为final 等等。
3.2.5 当前类索引,父类索引与接口索引集合
> u2 this_class;//当前类索引
> u2 super_class;//父类索引
> u2 interfaces_count;//接口索引
> u2 interfaces[interfaces_count];//一个类可以实现多个接口
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合用来描述这个类实现了那些接口。
3.2.6 字段表集合
> u2 fields_count;//Class 文件的字段的个数
> field_info fields[fields_count];//一个类可以有几个字段
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
3.2.7 方法表集合
> u2 methods_count;//Class 文件的方法的数量
> method_info methods[methods_count];//一个类可以有个多个方法
methods_count 表示方法的数量,而 method_info 表示的方法表。
3.2.8 属性表集合
> u2 attributes_count;//此类的属性表中的属性数
>attribute_info attributes[attributes_count];//属性表集合
4. 类加载过程
4.1 类加载过程
Class 文件需要加载到虚拟机中之后才能运行和使用,系统加载 Class
类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析,过程都是在程序运行期间完成的。
类加载过程的第一步,主要完成下面3件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
4.1.2 验证
4.1.3 准备
这个阶段正式为类分配内存并设置类变量初始值,内存在方法区中分配(含 static 修饰的变量不含实例变量)。
基本数据类型的零值:
4.1.4 解析
这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
- 符号引用:以一组符号来描述所引用的目标,符号可以使任何形式的字面量。 直接引用
- 直接引用:可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
4.1.5 初始化
真正执行类中定义的 Java 程序代码
对于初始化阶段,虚拟机规范了5种情况下,必须对类进行初始化(只有主动去使用类才会
初始化类):
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4
条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final
修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。 - 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
- 当一个接口中定义了JDK8新加入中被default关键字修饰的接口方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
4.2 卸载
卸载类即该类的Class对象被GC。
卸载类需要满足3个要求:
- 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被GC
5. 类加载器
通过一个类的全限定名来获取描述此类的二进制字节流。
5.1 类加载器总结
VM 中内置了三个重要的 ClassLoader,BootstrapClassLoader 由C++实现,其他类加载器均Java
实现且全部继承自 java.lang.ClassLoader :
- BootstrapClassLoader(启动类加载器) :由C++实现,加载 lib 下或被 -Xbootclasspath 路径下的类
- ExtensionClassLoader(扩展类加载器) :加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
- AppClassLoader(应用程序类加载器) :面向我们用户的加载器,加载用户路径上所指定的类库。
5.2 双亲委派模型
5.2.1 双亲委派模型介绍
每一个类都有一个它对应的类加载器。系统中的 ClassLoder在协同工作的时候会默认使用双亲委派模型。即在类加载的时候,首先系统会判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
加载的时候,首先会把该请求发给父类加载器的 loadClass() 处理,因此所有的请求都传送到顶层的启动类加载器BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
5.2.3 双亲委派模型的好处
- 保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),
- 保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。
5.2.4 如果我们不想用双亲委派模型怎么办?
可以自定义一个类加载器,然后重写 loadClass() 即可。
6.补充
堆和栈的区别
栈是运行时单位,代表着逻辑。堆是存储单位。
1、功能不同
- 栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们的对象都存储在堆内存中。
2、共享性不同
- 栈内存是线程私有的。堆内存线程共享的。
3、异常错误不同
- 如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError、OutOfMemoryError。
堆空间不足:java.lang.OutOfMemoryError。
4、空间大小
- 栈的空间大小远远小于堆的。
什么时候会触发FullGC
1、老年代空间不足
- 老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象。
java.lang.OutOfMemoryError: Java heap space
调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2、CMS GC时出现promotion failed和concurrent mode failure
- 使用CMS进行旧生代GC时,GC日志中出现promotion failed和concurrent mode
failure两种状况时可能会触发Full GC。应对措施为:增大survivor空间、老年代空间或调低触发并发GC的比率
3、 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
- Hotspot为了避免由新生代对象晋升到旧生代导致旧生代空间不足 现象,在进行Minor
GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
调优命令有哪些?
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
- jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
- jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
- jmap,JVM Memory Map命令用于生成heap dump文件
- jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
- jstack,用于生成java虚拟机当前时刻的线程快照。
- jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
Minor GC与Full GC分别在什么时候发生?
新生代内存不够用时候发生MGC,JVM内存不够的时候发生FGC
你知道哪些JVM性能调优
- 设定堆内存大小
-Xmx:堆内存最大限制。 - 设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比 - 设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
简述Java垃圾回收机制?
在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
你有没有遇到过OutOfMemory问题?
常见的原因:
- 内存加载的数据量太大:一次性从数据库取太多数据;
- 集合类中有对对象的引用,使用后未清空,GC不能进行回收;
- 代码中存在循环产生过多的重复对象;
- 启动参数堆内存值小。