1. 类的生命周期?
一个类完整的生命周期一般有5个阶段,分别为加载,验证,准备,解析,初始化,使用,卸载。其中连接又分为(验证,准备,解析)三个步骤。
-
加载:简单一句话概括:找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆中实例化一个java.lang.class对象,作为方法区中这个类的入口
类的加载方式:
- 根据类的全路径名找到对应的class文件,然后从class文件中读取文件内容
- 从jar中读取
- 从网络中获取
- 根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应类的自动生成它的代理类
- 从非class文件中获取,其实这与直接从class文件中获取的方式本质一样
-
连接
- 验证:进行类的合法性校验。比如对字节码格式,变量与方法的合法性,数据类型的有效性
- 准备:为类的静态变量分配内存空间,并初始化为默认值(零值)
- 解析:解析阶段是将符号引用转换为直接引用的过程。在这个阶段进行符号引用转换,例如将类、方法、字段等符号引用转换为内存地址等直接引用
-
初始化
- 创建类的实例
- 访问类的静态变量或静态方法
- 反射调用类的方法
- 初始化类的子类
-
使用:类加载完成并初始化后,类可被实例化、调用方法等,处于可用状态,可以使用类的方法和属性
-
卸载:在Java虚拟机运行时,当一个类不再被引用(没有任何实例存在,没有静态引用),并且不被任何线程引用,且虚拟机认为内存不足时,该类可能会被卸载并释放内存。
2. Jvm类加载机制?
双亲委派机制(Parent Delegation Model)是Java类加载机制中的一种设计模式,用于保证类在被加载时的唯一性和安全性。在双亲委派模型中,类加载器在尝试加载一个类时,会先将该请求委派给其父类加载器,直到最终被委派到最顶层的启动类加载器(Bootstrap ClassLoader)为止。如果所有父类加载器都无法找到需要加载的类,才会由当前类加载器尝试加载。
双亲委派机制的工作流程通常可以概括为以下步骤:
- 当一个类加载器收到加载类的请求时,首先检查自身是否已经加载过这个类。如果已经加载过,直接返回已加载的类。
- 如果当前类加载器没有加载过这个类,则将加载请求委派给父类加载器。
- 父类加载器接收到委派请求后,会依次执行类加载的步骤,检查自身是否已加载过这个类。如果父类加载器已加载过这个类,直接返回已加载的类。
- 如果所有父类加载器都无法找到需要加载的类,最终会由当前类加载器尝试加载这个类。
- 如果当前类加载器成功加载了这个类,则将类返回给请求的类加载器。
通过双亲委派机制,避免了同一个类被多个类加载器加载,保证了Java类的唯一性和一致性。另外,该机制也确保了Java核心类库能够被启动类加载器加载,避免了用户自定义的类与Java核心类库类的冲突。
双亲委派机制是Java类加载机制的重要组成部分,也是保证Java安全性和类加载的稳定性的重要机制之一
3. 说一下jvm内存区域?
在Java虚拟机(JVM)中,内存区域主要分为以下几个部分:
- 程序计数器(Program Counter Register) :程序计数器是一块较小的内存空间,是线程私有的。在多线程环境下,每个线程都有自己的程序计数器,用于存储当前线程正在执行的字节码指令地址或下一条将要执行的指令地址,用于线程切换和恢复。
- Java虚拟机栈(Java Virtual Machine Stacks) :每个线程在创建时都会被分配一个栈,用于存储局部变量、部分方法参数、方法返回值以及方法调用的相关信息。栈帧在方法调用时被创建,并在方法返回时被销毁。
- 本地方法栈(Native Method Stack) :本地方法栈用于支持虚拟机调用本地(Native)方法的内存空间。类似于Java虚拟机栈,但是用于执行本地方法时使用。
- Java堆(Java Heap) :Java堆是Java虚拟机中最大的一块内存区域,用于存储对象实例和数组。在堆中分配的对象实例,由垃圾收集器进行垃圾回收。
- 方法区(Method Area) :方法区用于存储类的结构信息、静态变量、常量、方法字节码等数据。在JVM规范中,方法区被认为是堆的一部分,但通常会独立出来作为一块独立的内存区域。
- 运行时常量池(Runtime Constant Pool) :运行时常量池是方法区的一部分,用于存储编译期生成的常量、符号引用、字面量等。在类加载完成后,这些符号引用会转换成直接引用。
- 直接内存(Direct Memory) :直接内存并不是虚拟机规范中定义的内存区域,但是在一些虚拟机的实现中会将NIO框架中的ByteBuffer分配的内存空间归为直接内存。直接内存通常是由操作系统直接分配的内存,有时候会被用于特定的I/O操作,避免了在Java堆和本地内存之间来回复制数据。
4. 对象创建过程?
对象创建在虚拟机中是非常频繁的操作,即使仅仅修改一个指针所指向的位置,在并发场景下也会引起线程不安全。 jvm采用以下两种方案解决线程安全问题:
- 采用cas分配重试的方式来保证更新操作的原子性
- 每个线程在java堆中预先分配一小块内存,也就是本地线程分配缓存(Thread Local AlloactionBuffer, TLAB),要分配内存的线程,先在本地内存中分配,只有当本地内存用完了,分配新的内存才需要同步锁定
虚拟机1.8默认是使用TLAB模式分配内存的,如果想要使用cas方式,可以通过设置-XX:-UseTLAB 参数来关闭TLAB功能即可,默认情况下,TLAB空间内存占用非常小,仅占用整个Eden空间的1%,我们可以通过设置 -XX:TLABWasteTargetPerCent设置TLAB空间所占用的百分比大小。如果通过TLAB分配失败时,则采用cas方式进行分配
5. 对象的内存结构?
对象的内存结构通常可以分为三部分:对象头(Headers)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头(Headers) :对象头是对象在内存中的元数据,用于存储对象自身的运行时数据,包括对象的哈希码、GC(垃圾回收)信息、锁状态、数组长度等。对象头的大小在32位JVM和64位JVM中可能会有所不同,通常包括Mark Word、Klass Pointer等信息。
- 实例数据(Instance Data) :实例数据包含对象的各个实例字段(成员变量)的值,即对象的属性值。当我们在Java代码中定义一个类并实例化对象时,这些实例字段的值会存储在实例数据部分。
- 对齐填充(Padding) :对齐填充通常是由于内存对齐的需要而引入的。在Java内存分配中,对象的起始地址一般会被对齐到8字节、16字节或更大的倍数。对齐填充可以确保对象的内存地址在对齐的边界上,以提高内存访问的效率。
6.内存泄漏可能是由那些原因造成的?
内存泄漏是指程序在动态分配内存后,由于某种原因程序未能释放已经不再需要的内存空间,导致系统不能再次利用被占用的内存,最终导致系统内存不足。内存泄漏可能由以下一些原因造成:
- 未释放资源:最常见的内存泄漏是由于程序未释放分配的内存、文件句柄、数据库连接等资源。比如,打开文件、数据库连接、网络连接等资源后,如果程序没有正确关闭或释放资源,就会导致资源泄漏。
- 使用数据库连接时,如果在使用完连接后未显式关闭连接(connection.close()),将会导致数据库连接的资源泄漏,占用的资源不会被释放。
- 循环引用:在使用垃圾回收的语言中(如Java),如果对象之间存在循环引用,而这些对象又被程序外部引用,导致垃圾回收器无法回收这些对象,就会发生内存泄漏。
- 两个对象相互引用,而外部仍然保留对其中一个对象的引用。即使这两个对象不再被程序需要,由于相互引用,Java垃圾回收器无法回收它们,造成内存泄漏。
- 静态集合引用:如果静态变量引用了对象,而且这些对象不会被回收(例如缓存数据),那么这些对象和它们引用的其他对象也不会被回收,可能导致内存泄漏。
- 一个静态集合(如静态Map)保存了大量对象引用,并且一直保持这些对象的引用。即使在程序运行过程中这些对象已经不再需要,但由于静态集合的引用,这些对象也不会被回收,导致内存泄漏。
- 内存溢出:虽然内存溢出并非内存泄漏的定义,但仍然可能表现为程序持续占用大量内存。内存溢出通常是由于程序中存在大量数据对象、循环体过深、递归调用未受控制等情况导致的。
- 如果程序中使用递归调用或其他导致栈溢出的情况,可能会导致内存溢出。例如,无限递归调用的场景可能导致栈溢出,虽然不是典型的内存泄漏,但会对内存占用产生类似的影响。
- 缓存未过期处理:缓存数据有时会一直保留在内存中,即使数据已经过期或失效。没有正确处理缓存数据的过期和更新,可能导致内存泄漏。
- 非标准写法:一些不规范的写法,如频繁创建对象、不合理的循环造成内存泄漏,例如在循环中不断创建对象但没有正确释放的情况。
- 未优化的循环操作可能导致频繁创建对象,但这些对象未被及时释放,最终导致内存泄漏。
7. 如何判断对象是否存活?
判断对象是否存活通常是由垃圾回收器(Garbage Collector)来完成的。垃圾回收器会在一定的时间间隔(或者在特定条件下)检查堆中的对象,将不再被引用的对象标记为可回收的垃圾对象,然后进行回收释放内存。但在一些特殊情况下,程序员也可以通过一些手段来判断对象是否存活:
- 引用计数算法:Java中并未使用引用计数算法进行垃圾回收,但可以通过创建对象的引用计数器自行判断对象是否存活。当对象的引用计数器为0时,该对象即可被判断为不再存活。
2. 可达性分析算法:这是Java垃圾回收常用的算法。从一组被称为“GC Roots”的对象作为起始点,通过一系列引用关系向下搜索,能够找到所有存活的对象。如果对象无法通过任何路径与GC Roots相连接,则被判断为不存活。所谓的根即是:所有的程序都是从main方法来执行,在main方法里面new出来的对象即为根对象