java面试题之JVM基础


JVM是运行在操作系统上的,与硬件没有直接的交互。 JVM从软件层面屏蔽不同OS在底层硬件与指令上的区别

1. 类加载器ClassLoader

负责加载class文件,class文件在文件开头有特定的文件标识(cafe babe),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,至于他是否可以运行,由Execution Engine(执行引擎负责解释命令,提交操作系统执行)决定
请添加图片描述
虚拟机自带的加载器

  • 启动类加载器 Bootstrap :如果是JDK自带的默认类(java/jre/lib/rt.jar),使用**getclass().getClassLoader()**得到的是Bootstrap类加载器
  • 扩展类加载器 Extension:对于javax为开头的类和包为扩展包(java/jre/lib/ext.jar)
  • 应用程序类加载器 AppClassLoader:对于自定义的类,使用**getclass().getClassLoader()**得到的是AppClassLoader类加载器

系统类加载器,加载当前应用的classpath的所有类

public class MyObject {

    public static void main(String[] args) {

       Object object = new Object();
       // Bootstrap类加载器:输出null 获取其父类加载器会报错NullPointerException
       System.out.println(object.getClass().getClassLoader());
      // System.out.println(object.getClass().getClassLoader().getParent());
       System.out.println("============================================");

       MyObject myObject = new MyObject();
       // 自定义的类全属于:sun.misc.Launcher$AppClassLoader@18b4aac2
       System.out.println(myObject.getClass().getClassLoader());
       // ExtClassLoader的父类加载器为null
       System.out.println(myObject.getClass().getClassLoader().getParent().getParent());
       // AppClassLoader的父类加载器为sun.misc.Launcher$ExtClassLoader@1b6d3586
       System.out.println(myObject.getClass().getClassLoader().getParent());
       
    }
}


用户自定义加载器

继承Java.lang.ClassLoader抽象类,用户可以定制类的加载方式


类加载的双亲委派机制:

当一个类收到了类加载请求,首先不会尝试自己去加载这个类,而是把这个委派交给父类去完成,每一层次的类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器Bootstrap中。只有当父类加载器反馈自己无法完成这个请求的时候(在其类路径下找不到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的好处:比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,保证了使用不同的类加载器最终得到的结果都是同一个Object对象。(沙箱安全机制)

2. 本地方法接口Native Interface

对于同一个线程的start方法,仅能被调用一次,如果调用两次start方法,则会报错:IllegalThreadStateException

在start源码中调用了start0方法, 是一个用native关键字修饰的方法,没有方法的实现,表示需要调用C,第三方库或者OS。

本地接口的作用是融合不同的编程语言为Java所用,融合C/C++程序,JVM中的本地方法栈专门用于处理标记为native的代码。具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies(本地方法库)


PC寄存器:线程私有的,存储指向下一条指令的地址,将要执行的指令代码,如果执行的是一个Native方法,这个计数器是空的

3. 方法区Method Area

各线程共享的运行时内存区域,存储每一个类的结构信息(模板),例如运行时的常量池(Runtime Constant Pool)、字段和方法数据、构造函数、普通方法以及静态方法的字节码的内容。其实就是一个规范,在不同的虚拟机是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。从硬盘上的.class文件通过类加载器加载到方法区中成为.Classs

举例:List list = new ArrayList<>()

方法区 f = new 永久代

方法区 f = new 元空间

而实例变量存在堆内存中,和方法区无关

4. Java虚拟机栈(线程栈)

栈负责运行,堆负责存储

栈即栈内存,主管java程序的运行,是在线程创建时创建,其生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题。只要线程一结束栈就被销毁,生命周期和线程一样,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配的。

栈帧主要保存3种数据:

  • 本地变量Local Variables:输入输出参数以及方法内的变量
  • 栈操作Operator Stack:记录出栈,入栈的操作
  • 栈帧数据Frame Data:类文件,方法等

栈运行原理:

栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集。当一个方法A被调用时就产生一个栈帧F1,并被压入栈中,A方法又调用B方法,于是产生栈帧F2也被压入栈;B方法又调用了C方法,于是产生了栈帧F3被压入栈…执行完毕后,先弹出F3,再弹出F2,最后弹出F1。

每个方法执行的同时都会创建一个栈帧,用于存储局部变量,操作数栈,动态链接,方法出口等信息,每一个方法从调用直至执行完毕的过程,对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,约等于1Mb

public class NativeInterfaceDemo {

    public static void recursive(){
        recursive();
    }
    public static void main(String[] args) {
        System.out.println("=============main begin==============");
        recursive();
        System.out.println("=============main end================");
    }
}

当应用程序递归太深而发生堆栈溢出时,抛出该错误:StackOverflowError


栈+堆+方法区的交互关系
请添加图片描述
HotSpot是指使用指针的方式来访问对象,Java堆中会存放访问类元数据的地址(类的结构信息,通过类加载器加载进来的.Class),reference存储的就是对象的地址

5. 堆heap

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载读取了类文件后,需要把类、方法、常变量方法到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存逻辑上分为三部分,物理上只有前两部分:

  • Young Generation Space 新生区
  • Tenure Generation Space 老生区
  • Permanent Space 永生区(JDK8以后为元空间)

请添加图片描述

新生区是类的诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集。新生区又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是在伊甸区被创建,幸存者区有两个:0区和1区
当伊甸取得空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸区进行垃圾回收(Minor GC),将伊甸区中的不再被其他对象所引用的对象进行销毁。然后将伊甸区中剩下的对象移动到幸存者0区。若幸存者0区也满了,再对该区进行GC,然后移动到1区,那如果1区也满了,再对该区进行GC。那如果1区也满了呢?
再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。一旦养老区执行了FGC之后发现依然无法进行对象的保存,那么会出现:

java.lang.OutOfMemoryError:java heap space异常

说明java虚拟机的堆内存不够,原因有二:

  • java虚拟机的堆内存设置不够,可以通过参数 -Xms、-Xmx来调整
  • 代码中创建了大量对象,并且长时间不能被垃圾回收机器收集(存在被引用)

java堆中GC的角度可以细分为:新生区(Eden Space,From Survivor和To Survivor)和老年区

默认情况下,新生区中分为Eden:Form:To =8:1:1; 整个新生区:老年区 = 1:2
请添加图片描述

Minor GC的过程:复制 -> 清空 -> 互换

  • eden、SurvivorFrom复制到SurvivorTo,年龄+1

    首先,当Eden区满的时候会触发第一次GC,将存活着的对象拷贝到SurvivorFrom区;当Eden区再次触发GC的时候会扫描Eden区和SurvivorFrom区,对这两个区域进行垃圾回收,经过这次GC后还存在的对象,则直接复制到To区(如果有的对象年龄已经达到了老年区的标准,则赋值到老年代区),同时将这些对象的年龄+1

  • 清空Eden,SurvivorFrom

    然后将Eden和SurvivorFrom区中的对象,全部清空(也就是复制之后会有交换,谁空谁是To区)

  • SurvivorTo和SurvivorFrom互换

最后,SurvivorTo和SurvivorFrom互换,原来的SurvivorTo变成了下一次GC时的SurvivorFrom区。部分对象会在From区和To区中复制来复制去,如此交换15次(由于JVM参数MaxTenuringThreshold决定,这个参数默认是15),如果还是有对象存活,则直接复制到老年代


实际而言,方法区和堆一样,是线程共享的内存区域,用于JVM加载的:类信息+普通常量+静态常量+编译器编译后的代码等。虽然JVM规范将方法区描述为堆的一部分,但是还有一个别名叫做Non-Heap,目的就是要和堆区分开

对于HotSpot虚拟机,可以将方法区称之为“永久代”或者"元空间",但是永久代只是方法区的一个实现(相当于一个interface)

永久代:永久区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,即存储的是运行环境必须的类信息,被转载到该区域的数据是不会被垃圾回收器GC的,关闭JVM才会释放该区域所占用的内存


堆内存调优

-Xms:设置初始分配大小,默认为物理内存的1/64

-Xmx:最大分配内存,默认为物理内存的1/4

-XX:+PrintGCDetails:输出详细的GC处理日志

可以设置最大分配内存和初始分配内存一样大,避免GC和应用程序争抢内存,导致峰值和峰谷忽高忽低,下面分配初始化内存和最大内存为1024MB:
请添加图片描述

public class JVMHeapDemo {

    public static void main(String[] args) {
      
        long maxMemory = Runtime.getRuntime().maxMemory(); // JVM试图使用的最大内存量--heap的上限
        long totalMemory = Runtime.getRuntime().totalMemory(); // JVM中的内存总量

        System.out.println("-Xmx: MaxMemory = " + maxMemory + "字节 " + (maxMemory/(double)1024/1024) + "MB");
        System.out.println("-Xms:totalMemory = " + totalMemory + "字节 " + (totalMemory/(double)1024/1024) + "MB");
		
        System.out.println("================测试OOM===========================");
        String str = "JVMHeadDemp";
        while(true){
            str += str + new Random().nextInt(888888) + new Random().nextInt(66666666);
        }
    }
}

请添加图片描述测试OOM异常:
请添加图片描述如何看GC的日志信息?

新生代的GC日志

[GC (Allocation Failure) [PSYoungGen: 3883K->483K(4608K)] 3883K->1331K(15872K), 0.0012389 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

新生代的GC日志说明

[GC类型 GC默认为Minor GC [Young区 GC前的Young区占用内存->GC后的Young区占用内存(Young区占用总大小)] 新生代GC前堆的大小 -> 新生代GC后堆的总大小(堆的总大小) 新生代GC耗时] [YoungGC用户耗时 YoungGC系统耗时 YoungGC实际耗时]

老年代的GC日志

[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(3584K)] [ParOldGen: 5376K->5356K(11264K)] 5376K->5356K(14848K), [Metaspace: 3347K->3347K(1056768K)], 0.0040237 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

老牛代的GC日志说明

【GC类型【Young区 GC前的大小 -> GC后的内存大小(Young区占用大小)】【Old区:GC前Old区内存占用 -> GC后Old区内存占用(Old区占用内存大小)】GC前堆的大小 -> GC后堆的大小(堆的占用总大小),【元空间区 GC前内存占用 -> GC后内存占用(元空间总大小),GC耗时】【用户时间 系统时间 实际时间】


GC的种类

  • 普通GC(minor GC):只针对新生代的GC,指的是发生在新生代的垃圾收集动作,因为大多数Java对象都存活不到老年代,所以minor GC非常频繁,一般回收速度较高
  • 全局GC(major GC/Full GC):指的是发生在老年代中的垃圾收集动作,出现了Full GC,经常会伴随至少一次的MinorGC(也并非绝对)。Major GC速度一般要比MinorGC慢上十倍以上
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

从现在开始壹并超

你的鼓励,我们就是hxd

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值