Java 面试必会题Object类和JVM基础

面试题之Object对象的方法

getClass方法

可以返回这个实体的Class对象,可以用来获得这个类的元数据。在反射中经常使用。

clone方法

被用来拷贝一个新对象。在Java中使用等号只是拷贝对象的引用并不是对象,需要拷贝对象的时候,可以借助clone方法。

toString方法

toString提供对象的字符串表示形式。
类Object的默认toString()方法返回一个字符串,该字符串包括该对象的类名称,"@"字符以及该对象的哈希码的无符号十六进制表示形式。
当需要打印对象引用时,toString方法就会被调用。

hashCode方法

对于每个对象,JVM都会生成一个唯一的数字,即哈希码。它为不同的对象返回不同的整数。
这个方法为HashMap、HashSet等方法提供支持。具体原因可以参考HashMap的讲解文章。
针对上面的toString方法的演示代码中,添加一个hashcode方法,指定对象的哈希码。

equals方法

被用来比较两个对象是否相等。在重写equals的时候也需要重写hashCode方法。
Hashset中判断两个对象是否相等,首先比较hashCode,如果hashCode相等才会执行equals方法。

finalize方法

这个方法在垃圾回收之前被执行,可以通过重写finalize方法来重置系统资源,执行清理活动并且最大程度的减少内存泄露。

wait方法

调用线程放弃锁并且进入睡眠状态,直到其他线程进入同一个monitor并且执行notify唤醒线程。

notify,notifyAll 方法

和wait相反,用于唤醒线程。

 

本系列文章讲解 面试中常见的 JVM 问题。这些问题之所以常见,是因为很基础,对于一个有点逼格的程序猿来说, JVM 的相关特性和原理在工作也需要熟知。笔者也在面试的过程中屡屡受挫,屡败屡战,总结一些常见知识点,这些知识点既可以应付面试,也可以帮助读者深入了解 JVM 提供大纲。

 

在用 C 之类的编程语言时,程序员需要自己手动分配和释放内存。而 Java 不一样,它有垃圾回收器,释放内存由回收器负责。

 

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。那我们来简单看一下 Java 程序具体执行的过程:

首先 Java 源代码文件(.java 后缀)会被 Java 编译器编译为字节码文件(.class 后缀),然后由 JVM 中的类加载器加载各个类的字节码文件,加载完毕之后,交由 JVM 执行引擎执行。在整个程序执行过程中,JVM 会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为 Runtime Data Area(运行时数据区),也就是我们常说的 JVM 内存。因此,在 Java 中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

 

本文的主要内容:

  • JVM 内存划分

    • 方法区

    • 运行时常量池

    • Java 虚拟机栈

    • 本地方法栈

    • 程序计数器

    • 栈与堆

  • 直接内存

    • 堆外内存垃圾回收机制

 

02

JVM 内存划分

运行时数据区分为线程私有和共享数据区两大类。其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含 Java 堆、方法区,在方法区内有一个常量池。

下面我们依次介绍这些数据区:

堆用于存放对象实例,所有的对象和数组都要在堆上分配。是 JVM 所管理的内存中最大的一块区域。Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代。新生代具体划分有:Eden 空间、From Survivor、To Survivor 空间等,进一步划分的目的是更好地回收内存,或者更快地分配内存。

 

方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即编译器编译后的代码等数据。
HotSpot 虚拟机中方法区也常被称为永久代,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。相对而言,垃圾收集行为在这个区域是较少出现的,但并非数据进入方法区后就永久存在了。

 

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)。

 

Java虚拟机栈

Java 虚拟机栈是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。存储局部变量表、操作数栈、动态链接和方法出口等信息。

局部变量表主要存放了编译器可知的各种数据类型、对象引用。

 

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 一个 Native Method 就是一个 Java 程序调用非 Java 代码的接口。在定义一个 Native method 时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非 Java 语言在外面实现的。标识符native可以与所有其它的 Java 标识符连用,但是 abstract 除外。

我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的 list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public 等)等等。

如果一个方法描述符内有 native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些 DLL 文件内,但是它们会被操作系统加载到 Java 程序的地址空间。当一个带有本地方法的类被加载时,其相关的 DLL 并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些 DLL 才会被加载,这是通过调用 java.system.loadLibrary()实现的。

需要提示的是,使用本地方法是有开销的,它丧失了 Java 的很多好处。如果别无选择,我们可以选择使用本地方法。

 

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

 

栈与堆

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在 Java 中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
Java 的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过 new、newarray、anewarray 和 multianewarray 等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时 动态分配内存的,Java 的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类 型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄。

 

在 Java 中,类的加载、连接和初始化过程都在程序运行期间完成的,这种策略虽然会使类加载时增加一些性能开销,但是提供了高度的灵活性,Java 天生可以动态扩展的语言就是依赖于运行期动态加载和动态连接的特点实现的。

 

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是 Java 虚拟机的类加载机制。Class 文件是一串二进制的字节流。实际上,每个 Class 文件都有可能代表着 Java 语言中的一个类或者接口。

 

类的加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。

 

 

JVM 预定义的类加载器

  • 启动(Bootstrap)类加载器

引导类装入器是用本地代码实现的类装入器,它负责将 < JavaRuntimeHome >/lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。

  • 标准扩展(Extension)类加载器

扩展类加载器,负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

  • 应用程序类加载器(Application)

应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

 

除此之外,还有用户自定义类加载器,是 java.lang.ClassLoader 的子类。在程序运行期间,通过java.lang.ClassLoader 的子类动态加载 class 文件,体现 Java 动态实时类装入特性.

 

双亲委派模式

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把请求委托给父加载器去完成,依次向上。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器没有找到所需的类时,子加载器才会尝试去加载该类。

 

  • 双亲委派机制

当 AppClassLoader 加载一个 class 时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器 ExtClassLoader 去完成。

当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader 去完成。

如果 BootStrapClassLoader 加载失败,会使用 ExtClassLoader 来尝试加载;

若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,则会报出异常 ClassNotFoundException。

 

  • 双亲委派作用

通过带有优先级的层级关系可以避免类的重复加载;

保证 Java 程序安全稳定运行,Java 核心 API 定义类型不会被随意替换。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值