JVM常见问题

Posted lbjlakers

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM常见问题相关的知识,希望对你有一定的参考价值。

(一)java类加载过程?

Java类加载需要经过七个过程。

  • 加载

  加载是类加载的第一个过程,在这个阶段,需要完成三件事情:

    • 通过一个类的全限定名获取该类的二进制流
    • 将该二进制流中的静态存储结构转化为方法去运行时数据结构
    • 在内存中生成该Class的对象,作为该类的数据访问入口
  • 验证

  验证的目的是为了确保Class文件流的信息不会危害到虚拟机,在这个阶段又需要完成四件事。

    • 文件格式验证:验证字节流是否符合Class文件的规范,如,主次版号是否在虚拟机范围内,常量池中的常量是否有不被支持的类型
    • 元数据验证:对字节码描述的信息进行语义分析,如是否有父类,是否继承了不被继承的类,是否继承了不被继承的类
    • 字节码验证:通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对的是方法体的验证,如:方法中的类型转换是否正确,跳转指令是否正确
    • 符号引用的验证:符号引用的验证的这个动作是在后面的解析过程的发生的,主要是为了确保解析的过程动作的正确执行
  • 准备

  此阶段是为类的静态变量分配内存并将其初始化为默认值,而这些内存都将在方法区中进行分配。

  准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化的时候随着对象分配到堆中。

  例如:

  public static int number=666;
  在准备阶段number的初始值是为0的,直到在初始化阶段才会变为666
  • 解析

  这个过程就是在上面验证过程中的符号引用的验证过程。解析的动作并不一定是在初始化的动作完成之前,也有可能是在完成之后

  • 初始化

  初始化是类加载的最后一个步骤,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,

  其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中的定义的Java程序代码

  • 使用
  • 卸载

(二)、JVM加载class文件的原理机制又是什么呢?

  在Java中只有将类(class)文件加载到JVM中才能  运行。

  当运行指定程序时,JVM会将编译生成的.class文件按照需求和一定的规则加载到内存中,并组织成为一个完整的Java应用程序,这个过程是由类加载器完成的,具体来说是由classloader和它的子类来实现的。

  类加载器本身也是一个类,其实质就是把类文件从硬盘读取到内存中。

 

  

  类加载方式为隐式加载和显示加载。

  隐式加载指的是程序在使用new等方式创建对象时,会隐式的调用类的加载器把对应的类加载到JVM中。

  显示加载指的是通过直接调用class.forName()方法来把所需的类加载到JVM中。

  

  任何一个工程都是由许多的类组成的,在程序启动时只需要把用到的类加载到JVM中,其他的类只是在需要用到的时候才会被加载。

  这一方式的优点是加快加载速度,节约程序运行时对内存的开销。

 

  每个类或接口都对应一个 .class 文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。

 

  类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到 JVM 中,至于其他类,则在需要的时候才加载。

  类加载的主要步骤是什么呢?

  • 装载:根据查找路径找到相对应的class文件,然后导入
  • 链接:
    • 检查:检查待加载的class文件的正确性  
    • 准备:给类中的静态变量分配存储空间  
    • 解析:将符号引用转换为直接饮用  
  • 初始化:对静态变量和静态代码块初始化

Java内存的分布

  • 寄存器:无法控制
  • 静态域:static的静态成员变量
  • 常量池:编译时被确定并保存在.class文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)
  • 非RAM存储:硬盘等永久存储空间
  • 堆内存:new 创建的对象和数组,有Java虚拟机自动垃圾回收器管理,存取速度慢,
  • 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性

Java堆的结构是什么样子的?什么是堆中的永久代?

  JVM中的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被加载。对象所占的堆内存是由自动内存管理系统也就是垃圾回收器。

 

  堆内存是由存活和死亡的对象组成的。

  存活的对象是应用可以访问的,不会被垃圾回收。

  死亡的对象是应用不可访问尚且还没有被垃圾回收器回收掉的对象。一直到垃圾回收器将这些对象回收掉之前,他们会一直占有堆的内存空间。

GC是什么?为什么会有GC?

  GC(Garbage Collection)垃圾回收器,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,

  Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。

简述Java垃圾回收机制。

  在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,

  在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

怎么判断一个对象是否被引用?

两种方式:1、引用计数法    2、可达性算法

引用计数法: 引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。

      当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.

      但是引用计数法的一个弊端就是:也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A、B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,这样就会造成一个循环引用的问题。

      所以主流的虚拟机都没有采用这种算法。

可达性算法:

      该算法的思想是:(上帝视角)从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。

  在 Java 中可以作为 GC Roots 的对象有以下几种:

  • 虚拟机占中引用的对象
  • 方法区类静态属性引用的对象
  • 方法区常量池引用的对象
  • 本地方法栈JNI引用的对象

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。

当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记.

 

如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选筛选的条件是是否有必要执行finalize() 方法。

当对象没有覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没必要的。

 

如果该对象有必要执行finalize() 方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,

虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize() 执行缓慢或者发生了死锁,

那么就会造成 F-Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除” 即将回收”集合,等待回收。

 

 

垃圾回收的原理和优点。

Java 语言中一个显著的特点就是引入了垃圾回收机制,它使得 Java 程序员在编写程序的时候不再需要考虑内存管理。

 

由于有个垃圾回收机制,Java 中的对象不再有“作用域”的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。

 

垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。

 

回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。 

以上是关于JVM常见问题的主要内容,如果未能解决你的问题,请参考以下文章

分析定位占用CPU资源高的JVM线程

详解Jvm内存结构

详解Jvm内存结构

详解Jvm内存结构

JVM性能分析工具jstack介绍

微信小程序代码片段