JavaJVM(2)垃圾回收、类文件结构及类加载

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 标记 —— 清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

两个明显的问题:

  1. 效率不高
  2. 空间会产生大量碎片

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) 收集器是一种以获取最短回收停顿时间为目标的并发收集器。使用“标记-清除”算法。非常符合在注重用户体验的应用上使用。

运作过程:

  1. 初始标记(CMS initial mark):标记 与GC Roots 关联的对象
  2. 并发标记(CMS concurrent mark):开启 GC 和用户线程,用一个闭包结构去记录可达对象
  3. 重新标记(CMS remark):修正并发标记期间的变动部分
  4. 并发清除(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件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
在这里插入图片描述

4.1.2 验证

### 4.1.1 加载

4.1.3 准备

这个阶段正式为类分配内存并设置类变量初始值,内存在方法区中分配(含 static 修饰的变量不含实例变量)。

基本数据类型的零值:
在这里插入图片描述

4.1.4 解析

这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

  • 符号引用:以一组符号来描述所引用的目标,符号可以使任何形式的字面量。 直接引用
  • 直接引用:可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

4.1.5 初始化

真正执行类中定义的 Java 程序代码

对于初始化阶段,虚拟机规范了5种情况下,必须对类进行初始化(只有主动去使用类才会
初始化类):

  1. 遇到 new、getstatic、putstatic 或 invokestatic 这 4
    条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final
    修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
  4. 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
  5. 当一个接口中定义了JDK8新加入中被default关键字修饰的接口方法时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

4.2 卸载

卸载类即该类的Class对象被GC。

卸载类需要满足3个要求:

  1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被GC

5. 类加载器

通过一个类的全限定名来获取描述此类的二进制字节流。

5.1 类加载器总结

VM 中内置了三个重要的 ClassLoader,BootstrapClassLoader 由C++实现,其他类加载器均Java
实现且全部继承自 java.lang.ClassLoader :

  1. BootstrapClassLoader(启动类加载器) :由C++实现,加载 lib 下或被 -Xbootclasspath 路径下的类
  2. ExtensionClassLoader(扩展类加载器) :加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,加载用户路径上所指定的类库。

5.2 双亲委派模型

5.2.1 双亲委派模型介绍

每一个类都有一个它对应的类加载器。系统中的 ClassLoder在协同工作的时候会默认使用双亲委派模型。即在类加载的时候,首先系统会判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。

加载的时候,首先会把该请求发给父类加载器的 loadClass() 处理,因此所有的请求都传送到顶层的启动类加载器BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

5.2.3 双亲委派模型的好处
  1. 保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),
  2. 保证了 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不能进行回收;
  • 代码中存在循环产生过多的重复对象;
  • 启动参数堆内存值小。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值