【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结

原博

文章目录

      • JVM
        • JVM 的作用
        • JVM 的主要组成及其作用
        • JVM 的运行时内存
        • 关于深拷贝和浅拷贝
      • 内存泄漏异常
        • Java会存在内存泄漏吗?请简单描述
      • JVM 内存溢出
        • OOM 可能发生在哪?怎么调优?
      • 垃圾回收机制
        • 简述下Java的垃圾回收机制 GC
        • 强引用、软引用、弱引用、虚引用
        • 怎么判断对象是否可以被回收?
        • 常见的垃圾回收算法
          • 标记-清除算法
          • 复制算法
          • 标记-整理法
          • 分代收集算法
        • JVM 的垃圾回收器
        • 对象优先分配在伊甸区
        • 大对象和长期存活的对象直接进入老年代
      • 虚拟机的类加载器
        • 什么是类加载器?类加载器有哪些?
        • 双亲委派原则

JVM

JVM 的作用

在早期的 Java 编程中,同一个项目在不同的操作系统运行开发需要不同的版本的 Java 代码,为了解决这一问题,开发了 JVM 即 Java 虚拟机,不同的平台有自己的虚拟机,因此只要将编译好的字节码文件加载到不同平台上的 JVM 虚拟机上,然后由JVM 负责将这些字节码文件翻译成特定平台下的机器码然后运行。

也就是说,只要在不同的平台安装对应的 JVM,就可以运行字节码文件,执行我们的 Java 代码,从而实现 Java 的跨平台特性。

JVM 的主要组成及其作用

JVM 的组成有类加载器、运行时内存、执行引擎、本地库接口。

可以通过执行 “java Main” 程序来看其组成及重要作用:

对于 OS 来说,执行这个指令只是开启了一个进程,这个指令它得先用 JVM 的类加载器将磁盘中的字节码文件加载 JVM 的运行时内存中,而字节码文件只是 JVM 的一套指令集规范,并不能交给底层的 OS 去执行,因此需要由特定的执行引擎将字节码文件翻译成特定平台下的底层系统指令,再交给操作系统的 CPU 去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。
【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第1张图片

JVM 的运行时内存

JVM 在执行 Java 程序的时候,会把它所管理的运行时内存这一区域划分为若干个不同的数据区域。这些区域有各自的用途、以及创建和销毁的时间。有的区域会随着 JVM 启动而存在,有些区域则是依赖线程的启动和结束而创建和销毁。

JVM 规范并没有具体规定运行时内存的实现,以下用 HotSpot 举例,运行时内存 可划分为以下几个区域:

  • 程序计数器(PC):PC 是一块比较小的内存,每个线程都会有一个独立的程序计数器,用来存放当前线程的下一个指令的位置,当前线程被调用的时候,就填充到 CPU 的 PC 寄存器中(注:PC 是在 JVM 中唯一一块没不会发生内存溢出的区域);
  • :栈区域是每个线程独立拥有的区域,每个方法在执行时都会创建一个栈帧,栈帧中用于存储局部变量表、操作数栈、动态链接、方法出口等信息。在其他 JVM 中栈还可被分为Java虚拟机栈、本地方法栈;
  • :Java虚拟机中内存最大的一块,是所有线程共享的,几乎所有的对象实例都会在堆中分配内存;
  • 方法区:用于存储被虚拟机加载的类信息、静态变量、即时编译后的代码等;
  • 字符串常量池:用于存储常量、基本类型引用等,在一些JVM区域划分中,字符串常量池被划分到了方法区。

运行时内存中的数据主要有两个来源

  • 类文件加载进行的数据(放在方法区、常量池区为主);
  • 运行期间产生的数据(动态的)(在栈 + 堆区为主)
    【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第2张图片

其中,程序计数器和栈是每个线程所私有的,堆、方法区和线程池的所有线程共享的

关于深拷贝和浅拷贝

浅拷贝:只是增加了一个引用指向已存在的内存地址;生成一个引用,引用指向和拷贝的目标都指向堆中同一个对象

深拷贝:是增加了一个指针,并申请了一个新的内存,使这个增加的指针指向这个新的内存;先在堆中生成一个新的对象,这个对象和拷贝目标所指向的对象完全一致,再生成一个引用,用来指向这个对象

用一个例子来讲,现在有一个路由器类,这个类中,有一个属性为String类型的 name,又有一个电脑类,类中有一个属性为 路由器类型的route。

先使用代码,创建一个路由器对象a,它的属性name为”hello“,再创建一个电脑对象b,这个对象中的属性 route 指向之前创建的路由器对象a。

进行浅拷贝,新生成一个电脑对象b1,这个对象中的属性route,指向之前创建的路由器对象a,此时在JVM 内存中只有一个路由器对象,有两个引用b 和 b1 都指向它,这个过程就叫做浅拷贝。

进行深拷贝,还是之前的路由器对象a,电脑对象b,新生成一个路由器对象a1,它的属性name 为“world”,再生成一个电脑对象b1,它的属性route指向新生成的路由器对象a1。此时在 JVM 中有两个路由器对象a 和 a1,他们的 name 属性分别为“hello”“world”,两个电脑对象b 和 b1,这个过程就叫做深拷贝。

内存泄漏异常

Java会存在内存泄漏吗?请简单描述

内存泄漏指的是不再被使用的对象或者变量一直被占据在内存中,导致该对象不能被回收。从理论上讲Java有GC 垃圾回收机制,也就是说不再使用的对象会被回收掉,从内存中清除。

但是,即使如此也还是会有内存泄漏的情况,比如:长生命周期的对象持有短生命周期对象的引用,即使短生命周期的对象已经不再需要,但是因为长生命周期对象的持有导致他不能被回收,这就是Java中内存泄漏发生的场景。

常见的内存泄漏:

1)单例造成的内存泄漏:单例模式属性的静态特征使得其生命周期与应用的声明周期一样长,如果一个对象已经不再需要使用了,而单例模式的类还持有该对象的引用,就会造成该对象的不正常回收。

2)非静态内部类创建静态外部类实例造成的内存泄漏:非静态内部类默认会持有外部类的引用,而该非静态内部类中又创建了一个静态的外部类实例,该实例的生命周期和应用的生命周期一样长,这就导致了该静态实例会一直持有Activity的引用,从而导致该内存资源不能被正常回收。

3)资源未关闭造成的内存泄漏。

JVM 内存溢出

OOM 可能发生在哪?怎么调优?

JVM 中除了程序计数器外,其他各个内存都可能会抛出OOM。常见的有一下三种:

  • Java 堆内存溢出,这种情况一般是由内存泄漏或者堆的大小设置不当引起的。对于内存泄漏,需要通过内存监控软件查找程序中的内存泄漏代码,而堆大小可以通过虚拟机参数进行修改。
  • Java 永久代溢出,也就方法区溢出了,一般是由于出现了大量的、jsp 页面或用了反射机制引起的情况,因为上述原因可能会产生大量的类信息存储于方法区。此次情况可以通过更改方法区的大小来解决。
  • Java 虚拟机栈溢出,并不会抛OOM 异常,一般是由于程序中存在死循环或者深度递归调用而造成的,栈区域设置太小会出现此种溢出。

垃圾回收机制

简述下Java的垃圾回收机制 GC

在 Java 中,程序员是不需要显示的释放一个对象的内存的,而是由虚拟机自行执行释放。在 JVM 中,有一个垃圾回收机制,它的优先级特别低,在正常情况下是不会被执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描并清除那些没有任何引用的对象。

本来 GC 的对象是JVM 的内存,但是程序计数器和栈,他们是线程出现的时候就需要这块内存,线程结束的时候,这块内存就可以回收了,所以它的分配回收时机非常明确,就不需要GC 花费大量精力,常量池和方法这两块内存占比比较小,且里面的数据很少失去作用,回收这块内存的性价比不高,因此也不是 GC 重点考虑的点,JVM 的运行时内存就只剩下堆了,所以 GC 重点考虑的就是如何管理堆上的内存,堆上存放的又基本是对象,所以GC 回收不再使用内存的问题,就转换成了,回收死去对象的问题!

有了垃圾回收机制可以让Java程序员在编写程序时不再考虑内存管理的问题,有效的防止了内存泄漏,可以有效的使用可使用的内存。

强引用、软引用、弱引用、虚引用

Java 中引用的本质作用是指向某个对象,但是在GC 中应用还能影响对象的生死,所以Java的引用类型,根据 GC 时引用指向的对象是否被回收划分了几个不同的类型:

  • 强引用:强引用可以指向某个对象,且强引用指向的对象在发生 gc 的时候不会被回收;
  • 软引用:软引用可以指向某个对象,软引用指向的对象可以 GC,但不是必须GC的对象,即将这类对象列入进行 GC 的第二梯队,第一遍 GC 不回收,第二次 GC 时一定回收;
  • 弱引用:软引用可以用来指向一个对象,但它指向的对象在下次发生GC 时一定会被回收;
  • 虚引用:无法通过该引用获得对象,它的作用就是在对象被回收前,可以收到一波通知,去办理一些善后事宜。

怎么判断对象是否可以被回收?

没有引用指向的对象就可以被回收,怎么判断一个对象没有引用指向它?

  • 引用计数法:在每一个对象内部创建一个属性rc,用来表示指向它的引用数,当有引用指向该对象时,rc++,每当有一个引用离开作用域时,意味着引用失效了,该引用不再指向该对象,rc–,当 rc == 0时,意味着再也没有引用指向该对象了,就可以被回收了。但是他有一个缺点不能解决循环引用的问题,即就是,有两个对象,没有其他引用指向他们两个,但是他们两个相互引用,就无法就绪回收。
  • 可达性分析法:在堆中的对象,他们之间可能相互引用形成一张图结构,在这个结构中,有一些对象必须活着,这些对象就叫做GC Roots,从这些对象出发可以到达的对象,也必须活着。

常见的 GC Roots:栈上引用类型的局部变量指向的对象、常量池中引用指向的对象、静态属性中引用指向的对象

常见的垃圾回收算法

标记-清除算法

标记无用的对象,然后进行清除回收。它是一种常见的基本的垃圾回收算法,分为两个阶段:

  • 标记阶段:标记出可以回收的对象;
  • 清除阶段:回收被标记对象所占用的空间

优点:实现简单,不需要对象移动;
缺点:标记、清除过程的效率低,产生了大量不连续的内存碎片

标记-清除法的执行过程如下:
【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第3张图片

复制算法

为了解决标记-清除算法效率不高的问题,产生了复制算法。他把内存的空间划分为两个相等的区域,每次只使用其中一个区域。当发生垃圾回收时,遍历当前区域,把存活的对象复制到另外一个区域中,然后把使用的内存区域一次性清理掉。

优点:复制对象时按顺序分配内存即可,实现效率高、运行简单,不会产生内存碎片;
缺点:可用空间减小为原来的一半,对象存活率高时会发生频繁进行复制

复制算法的执行过程如下:
【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第4张图片

标记-整理法

为了解决标记-清除法中内存碎片的问题,产生了标记-整理法。在标记可回收对象之后,将所有存活对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,将已用和未用的内存都各自放在一边 —— 就是原地整理的意思。

  • 优点:解决了标记-清除中内存碎片的问题;
  • 缺点:仍需要对内存进行移动,一定程度上降低了效率。

标记-整理算法的执行过程如下:
【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第5张图片

分代收集算法

先介绍一下一个普通对象可能的一生,分为新生代、老年代。且经过发现,大部分的对象,寿命都是非常短的,但对象的岁数一旦比较大,寿命就比较长了。即就是大量的对象可能都会在新生代被回收,只有少量的对象可能会存活到老年代。对象每经过一次 GC,它的年龄就会 +1。

所以我们根据对象的存活周期将内存划分为伊甸区、生存区、老年区。新生代的对象刚开始都在伊甸区,经过一次 GC 后,98%的对象已经死了,存活的对象都会进入到生存区,如果在生存区经过了 15 次 GC,对象还存活着,就会进入到老年区。大量的对象在伊甸区就会被回收掉,因此生存区中的对象就比较少,生存区中的对象在经过 GC 时,采用的算法是复制算法,而一旦进入到老年代,它的寿命比较长,存活率比较高,因此在GC 的时候,可以采用标记-整理法。
【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第6张图片
GC 不是每次都全区域 GC,根据 GC 的区域可以分为:

  • Partial GC:部分GC
  • Full GC:全区域GC

在分代收集算法中,GC 一般可以分为Minor GC和 Major GC

  • Minor GC:进行新生代 GC,因为Java对象大都朝生夕死,所以Minor GC 比较频繁,且速度比较快;
  • Major GC:老年代的GC,但由于老年代 GC 往往是由新生代 GC 引起的,所以进行老年代 GC 时,往往可以看作是 Full GC;Major GC 的速度比 Minor GC 的速度慢至少 10 倍。

JVM 的垃圾回收器

【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第7张图片
其中,新生代垃圾回收器有Serial、PraNew、Parallel Scavenge,老年代的垃圾回收器有 Serial Old、Parallel Old、CMS,还有两个都可以用的 G1。

  • Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程的,优点是简单高效;
  • ParNew(复制算法):新生代并行收集器,是 Serial 的多线程版本,在多核环境下表现更好;
  • Parallel Scavenge收集器(复制算法):新生代并行收集器,追求高吞吐量,高效利用 CPU;
  • Serial Old(标记-整理算法):老年代单线程收集器,是 Serial 收集器的老年版本;
  • Parallel Old(标记-整理算法):老年代并行收集器,是Parallel Scavenge收集器的老年版本;
  • CMS收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。
  • G1(标记-整理算法):整堆回收器

对象优先分配在伊甸区

对象一般都分配在伊甸区。当伊甸区分配时没有足够空间时,虚拟机会调用一次 Minor GC,如果有空间了就分配,如果还是没有,就启用分配担保机制在老年代分配内存。

大对象和长期存活的对象直接进入老年代

  • 大对象就是需要大量连续内存空间存储的对象,若直接生在伊甸区,会导致在内存中还有不少空间却因为没有连续的空间,而提前触发 GC 以获取足够的连续空间来安置新对象。还有,因为新生代的对象 GC 时采用复制算法,如果大对象直接在新生代分配就会导致伊甸区和存活区直接发生大量的内存复制。
  • 新创建的对象在伊甸区,经过一次GC后,存活的对象就进入到存活区,对象的年龄也+1, 在存活区经过了15次(默认是15次,可以修改)GC后,存活的对象进入老年代,经过 15 次 GC,对象的年龄也 +15。长期存活的对象肯定是寿命比较大的对象,所以可以直接放到老年代。

虚拟机的类加载器

什么是类加载器?类加载器有哪些?

类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象,主要有三种类加载器:

  • 启动类加载器(BootstrapClassLoader):用来加载Java的核心类库,所有的应用都会需要的,并且是基础部分,例如String类库;
  • 扩展类加载器(ExtensionClassLoader):加载Java的扩展类库,这些类有的应用可能会需要,例如DNSNameClassLoader类库;
  • 用户自定义类加载器(ApplicationClassLoader):用户自己创建的类,通过继承java.lang.ClassLoader 类的方式来实现。
public class PrintClassLoader{
	public static void main(String[] args){
		// 启动类加载器
		// 打印 null,表示是由BootstrapClassLoader 加载
		// 存在:jre.1.8.0_141\lib\rt.jar下的类库
		ClassLoader classLoader1 = String.class.getClassLoader();
		System.out.println(classLoader1);

		// 扩展类加载器
		// 打印,ExtensionClassLoader
		// 存在:jre.1.8.0_141\lib\ext\*.jar
		ClassLoader classLoader2 = DNSNameService.class.getClassLoader();
		System.out.println(classLoader2);

		// 用户自定义类加载器
		// 打印 ApplicationClassLoader
		ClassLoader classLoader3 = PrintClassLoader.class.getClassLoader();
		System.out.println(classLoader3);
}

双亲委派原则

【面试复习】—— JVM、GC(垃圾回收机制)、类加载器 学习并总结_第8张图片
先说一下类加载器是什么?它的分类?

用户自定义的类加载器是扩展类加载器,扩展类加载器的双亲是启动类加载器。

双亲委派原则就是如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类去完成,每一层的类加载过程都是如此,这一所有的类加载请求都会被传送到顶层的启动类加载器中,只有当父类加载器无法完成加载请求时,子类才会尝试去加载类。

你可能感兴趣的:(jvm,java,面试,内存泄漏)