java面试(五)JVM

目录

JVM的组成(类加载器、运行数据区、解释器、本地接口各自的含义与作用)

java程序在JVM中的执行过程

加载

链接

初始化

类加载的时机

类加载器的种类(Bootstrap 、Extension 、App 、自定义四个类加载器的工作责任)

类加载器的工作过程(双亲委派机制)

类加载的详细过程(加载-连接-初始化-使用-卸载 五个步骤的含义)

加载

链接

初始化

卸载

JVM内存模型(程序计数器、堆、虚拟机栈、本地方法栈、方法区)

GC(垃圾回收机制)

判断对象存活的方法(引用计数法和可达性分析的区别)

1、引用计数算法

 2、可达性分析算法:

可达性分析中可以作为GC Roots的对象

垃圾回收算法(标记/清除算法、复制算法、标记/整理算法的区别以及适用环境)

GC时间

什么情况下出现内存溢出,内存泄漏?

GC回收器

JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。​

JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代



JVM的组成(类加载器、运行数据区、解释器、本地接口各自的含义与作用)

JVM 整体组成可分为以下四个部分:

  1. 类加载器(ClassLoader)

  2. 运行时数据区(Runtime Data Area)

  3. 执行引擎(Execution Engine)

  4. 本地库接口(Native Interface)

各个组成部分的用途:

程序在执行之前先要把java代码转换成字节码(class文件),jvm首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是jvm的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。

java程序在JVM中的执行过程

加载

JVM将java类的二进制形式加载到内存中,并将他缓存在内存中,以便后面使用,如果没有找到指定的类就会抛出异常classNotFound,进程在这里结束。没有错误就继续在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。

链接

这个阶段做三件事:验证、准备和解析(可选)。

验证是JVM根据java语言和JVM的语义要求检查这个二进制形式。确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

虚拟机规范:如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常

准备是指准备要执行的指定的类,准备阶段为变量分配内存并设置静态变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:

public static int num = 8;

那么在这个阶段,num的值为0,而不是8。 final修饰的类变量将会赋值成真实的值。

解析是检查指定的类是否引用了其他的类/接口,是否能找到和加载其他的类/接口。这些检查将针对被引用的类/接口递归进行,JVM的实施也可以在后面阶段执行解析,即正在执行的代码真正要使用被引用的类/接口的时候。

初始化

在这最后一步中,JVM用赋值或者缺省值将静态变量初始化,初始化发生在执行main方法之前。在指定的类初始化前,会先初始化它的父类,此外,在初始化父类时,父类的父类也要这样初始化。这个过程是递归进行的。

简而言之,整个流程是将类存进内存中,检查类的对应调用的类和接口是否可正常使用,再对类进行初始化的过程。

类加载的时机

 

  • 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 java 代码场景是:使用 new 关键字实例化对象的时候读取或设置一个类的静态字段(被 final 修饰 以及 已在编译期把结果放入常量池的静态字段除外)的时候以及调用一个类的静态方法的时候
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟器会先初始化这个主类。
  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要出发其初始化。
  •  

类加载器的种类(Bootstrap 、Extension 、App 、自定义四个类加载器的工作责任)

类加载器 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。

启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

类加载器的工作过程(双亲委派机制)

如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

为什么需要双亲委派模型?

在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码

怎么打破双亲委派模型?

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。

 

类加载的详细过程(加载-连接-初始化-使用-卸载 五个步骤的含义)

加载

JVM将java类的二进制形式加载到内存中,并将他缓存在内存中,以便后面使用,如果没有找到指定的类就会抛出异常classNotFound,进程在这里结束。没有错误就继续在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。

链接

这个阶段做三件事:验证、准备和解析(可选)。

验证是JVM根据java语言和JVM的语义要求检查这个二进制形式。确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

虚拟机规范:如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常

准备是指准备要执行的指定的类,准备阶段为变量分配内存并设置静态变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:

public static int num = 8;

那么在这个阶段,num的值为0,而不是8。 final修饰的类变量将会赋值成真实的值。

解析是虚拟机将 常量池中的符号引用 转化为 直接引用 的过程,在 Class 文件中它以 CONSTANT_Class_info、CONSTANT_FIeldref_info、CONSTANT_Methodref_info 等类型的常量出现。

初始化

在这最后一步中,JVM用赋值或者缺省值将静态变量初始化,初始化发生在执行main方法之前。在指定的类初始化前,会先初始化它的父类,此外,在初始化父类时,父类的父类也要这样初始化。这个过程是递归进行的。

简而言之,整个流程是将类存进内存中,检查类的对应调用的类和接口是否可正常使用,再对类进行初始化的过程。

卸载

  1.     执行了System.exit()方法
  2.     程序正常执行结束
  3.     程序在执行过程中遇到了异常或错误而异常终止
  4.     由于操作系统出现错误而导致Java虚拟机进程终止

JVM内存模型(程序计数器、堆、虚拟机栈、本地方法栈、方法区)

程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。

Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。

Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。

Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。

方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享

GC(垃圾回收机制)

Java内存

堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区。

GC的主要任务:
1.分配内存
2.确保被引用对象的内存不被错误的回收
3.回收不再被引用的对象的内存空间

垃圾回收机制的主要解决问题
1.哪些内存需要回收?
2.什么时候回收?
3.如何回收?

 

判断对象存活的方法(引用计数法和可达性分析的区别)

1、引用计数算法


每当一个地方引用它时,计数器+1;引用失效时,计数器-1;计数值=0——不可能再被引用。

判定效率很高。不会完全准确,因为如果出现两个对象相互引用的问题就不行了。

//举例:
        Test test1 = new Test();
        Test test2 = new Test();
        test1.obj = test2;
        test2.obj = test1;
        //test1 ,test12能否被回收?
        System.gc();

 2、可达性分析算法:

通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

举例:一颗树有很多丫枝,其中一个分支断了,跟树上没有任何联系,那就说明这个分支没有用了,就可以当垃圾回收去烧了。

可达性分析中可以作为GC Roots的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中JNI(就是native方法)引用的对象

垃圾回收算法(标记/清除算法、复制算法、标记/整理算法的区别以及适用环境)

标记清除法:标记所有的可达对象,则未标记的对象就是不存在引用的垃圾对象,GC时清除所有未标记的对象;

【先标记再清除】

  • 优点:只标记正常引用的对象,不标记循环引用这样的垃圾对象以及没有引用的对象,解决了循环引用的问题;
  • 缺点:可能会产生空间碎片(不连续的内存空间),不连续的内存空间在内存分配时的工作效率低于连续的内存空间,尤其时堆大对象的内存分配;

复制算法:将内存空间分为两块相同的存储空间,每次只使用一块,GC时将正在使用的内存中的存活对象复制到另一块存储空间中,然后清除正在使用的空间的所有对象;【先复制再清除】

  • 优点:存活对象相对少时,效率很高(需要复制的对象少),存活对象复制到另一空间时,解决了空间碎片的问题;
  • 缺点:系统内存只能使用一半的内存空间,而且如果存活对象过多时,比较耗时;
  • 应用场景:java新生代串行垃圾回收器(优点:保证了内存空间的连续性,又避免了大量的空间浪费)

标记压缩法(标记清除压缩法):同标记清除算法一样首先标记存活的对象,但是再标记之后还要将所有标记的对象压缩到内存空间的一端后再清理边界外的所有空间;【标记+压缩+清除】

  • 优点:解决了标记清除法带来的空间碎片问题,也不需要折损可使用空间;
  • 缺点:压缩过程的处理提升了垃圾回收过程的复杂度
  • 应用场景:老年代的回收算法

分代算法:根据对象的特点堆内存空间进行划分,选择合适的垃圾回收算法。

  • 优点:提交了垃圾回收的效率;
  • 缺点:不同区域是使用不同的垃圾回收算法提升了垃圾回收过程的复杂度?
  • 应用场景:虚拟机的垃圾回收器

分区算法:将整个堆空间划分为连续的不同小区间,每一个小区间都独立使用、独立回收

  • 优点:可以控制一次回收多少个小区间,从而减少1GC的停顿时间;
     
  1. GC时间

       ①在程序空闲的时候。这个回答无力吐槽

  ②程序不可预知的时候/手动调用system.gc()。关于手动调用不推荐

  ③Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。就是

  这时候如果你们讲出新生代和老年代的话或许会更细的了解一下Minor GC、Full GC、OOM什么时候触发!

  创建对象是新生代的Eden空间调用Minor GC;当升到老年代的对象大于老年代剩余空间Full GC;GC与非GC时间耗时超过了GCTimeRatio的限制引发OOM。

 

什么情况下出现内存溢出,内存泄漏?

内存泄漏:

概念:由于java的JVM引入了垃圾回收机制,垃圾回收器会自动回收不再使用的对象;JVM是使用引用计数法和可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。

 

造成内存泄漏的方式:

1:静态集合类

2:各种连接没有关闭:数据库,io

3:变量不合理的作用域:变量定义的范围大于其使用的范围,如果没有及时把变量置为null,就容易内存泄露

4、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露

 

GC回收器

1)几种垃圾收集器:

  • Serial收集器: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
  • ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。
  • Parallel Scavenge收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
  • Serial Old收集器: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
  • Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
  • CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
  • G1收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。

2)CMS收集器和G1收集器的区别:

  1. CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
  2. G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
  3. CMS收集器以最小的停顿时间为目标的收集器;
  4. G1收集器可预测垃圾回收的停顿时间
  5. CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  6. G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。

1)共享内存区划分

  • 共享内存区 = 持久带 + 堆
  • 持久带 = 方法区 + 其他
  • Java堆 = 老年代 + 新生代
  • 新生代 = Eden + S0 + S1

2)一些参数的配置

  • 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。
  • 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
  • Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)

3)为什么要分为Eden和Survivor?为什么要设置两个Survivor区?

  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

 

JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代
 

  1. 新建的对象,大部分存储在Eden中
  2. 当Eden内存不够,就进行Minor GC释放掉不活跃对象;然后将部分活跃对象复制到Survivor中(如Survivor1),同时清空Eden区
  3. 当Eden区再次满了,将Survivor1中不能清空的对象存放到另一个Survivor中(如Survivor2),同时将Eden区中的不能清空的对象,复制到Survivor1,同时清空Eden区
  4. 4重复多次(默认15次):Survivor中没有被清理的对象就会复制到老年区(Old)
  5. 当Old达到一定比例,则会触发Major GC释放老年代
  6. 当Old区满了,则触发一个一次完整的垃圾回收(Full GC)
  7. 如果内存还是不够,JVM会抛出内存不足,发生oom,内存泄漏。
     

Minor GC 和 Full GC的区别

       新生代GC(Minor GC):Minor GC指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC。

       老年代GC(Full GC/Major GC):Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。

 

什么是OOM,请你说说OOM产生的原因?如何分析?

OOM:当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时;
OOM产生的原因:内存泄露、内存溢出

  1. 分配的少了
  2. 应用用的太多

分析OOM:
1.java.lang.OutOfMemoryError: Java heap space

  • 产生原因:内存泄露或者堆的大小设置不当引起;
  • 解决办法:对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改;

2.java.lang.OutOfMemoryError: PermGen space(即方法区溢出)

  • 产生原因:产生大量的Class信息存储于方法区(大量Class或者jsp页面,或者采用cglib等反射机制)或者过多的常量尤其是字符串也会导致方法区溢出;
  • 解决办法:可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改;

3.java.lang.StackOverflowError:不会抛OOM error

  • 产生原因:程序中存在死循环或者深度递归调用或者栈大小设置太小;
  • 解决办法:通过虚拟机参数-Xss来设置栈的大小

 

JVM的常用调优参数有哪些?

 

  • -Xmx:指定java程序的最大堆内存, 使用java -Xmx5000M -version判断当前系统能分配的最大堆内存;
  • -Xms指定最小堆内存, 通常设置成跟最大堆内存一样,减少GC
  • -Xmn:设置年轻代大小。整个堆大小=年轻代大小 + 年老代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
  • -Xss:指定线程的最大栈空间, 此参数决定了java函数调用的深度, 值越大调用深度越深, 若值太小则容易出栈溢出错误(StackOverflowError)
  • -XX:PermSize:指定方法区(永久区)的初始值,默认是物理内存的1/64, 在Java8永久区移除, 代之的是元数据区, 由-XX:MetaspaceSize指定
  • -XX:MaxPermSize:指定方法区的最大值, 默认是物理内存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元数据区的大小
  • -XX:NewRatio=n:年老代与年轻代的比值,-XX:NewRatio=2, 表示年老代与年轻代的比值为2:1
  • -XX:SurvivorRatio=n:Eden区与Survivor区的大小比值,-XX:SurvivorRatio=8表示Eden区与Survivor区的大小比值是8:1:1,因为Survivor区有两个(from, to)


https://blog.csdn.net/qq_40655613/article/details/104737246

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值