简聊JVM

112 阅读8分钟

一.类的装载过程

1).装载:

  • 定义:

    通过javac把xxx.java文件编译编译成xxx.class文件,xxx.class属于二进制文件,每一个java类都有一个引用指向它的类加载器,除了数组以外,数组是由JVM直接创建的;

  • 装载步骤:

    1. 通过全限定类名获取到.java文件的二进制流形成.class;
    2. 将字节流转化为运行时数据区的方法区中(JDK1.8以后叫做元空间);
    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的方法区的访问入口;
  • 类加载器 :一共分为三个加载类信息

    1. 引导类加载器-Bootstrap-classLoader,基于C++实现,负责加载java的核心类库,JAVA_HOME/jre/lib/rt.jar,bootstrap-classloader是不继承classcloader抽象类,并且只加载包名为,java/sun/javax等开头的类,起到对核心资源的保护;
    2. 扩展类加载器-Extension-ClassLoader,基于java语言实现,负载加载JDK安装目录jre\lib\ext目录下的类;
    3. 系统加载类-application-classloader,基于java语言,负责加载环境变量classpath下制定的类库,如果应用程序中没有自定义的加载类,一般是作为程序中默认的加载类

2).链接:链接过程包含三步:验证/准备/解析/

  • 验证:

    1. 文件格式验证:基于二进制文件,魔数0xCAFEBABE开头验证,class版本信息;
    2. 元数据验证:对类,字段,方法进行验证是否符合JVM规范,未深入方法中;
    3. 字节码验证:主要对方法体内的前后逻辑进行验证,深入方法中;
    4. 符号引用验证:发生符号引用向直接引用转化的过程,这一过程发生在解析阶段;
  • 准备:

    1. 为static变量分配空间
    2. 设置默认值;
    3. 如果static变量是final修饰在准备阶段赋初始值;
    4. 如果static变量是final修饰,但是是引用对象,在初始化阶段赋值;
  • 解析:

    1. 将引用符号转化为直接引用的过程,

3).初始化:赋初始值操作


二.JVM运行数据区:堆/虚拟机栈/虚拟机方法区/本地方法区/程序计数器

1).堆:

  • 是否为线程共享:线程共享的区域;但是在多线程同时分配的时候。有一块区域叫做TLAB是线程独享的一块区域,使用TLAB能解决线程安全问题,同时能提升内存分配的吞吐量,TLAB空间很小仅仅站Eden区1%,每个线程在分配对象到堆空间时候,会优先分配到自己的私有的TLAB空间,如果分配失败,会使用加锁机制,保证数据的原子行,直接分配到Eden空间;

  • 存储什么:从实际角度上看,所有的对象都在这里分配内存;这里一般指是在hotSpot虚拟机上,但是有一种特殊情况,那就是经过逃逸分析后这个对象没有逃出法以外。那么可能被优化分配到栈地上;

  • 空间分配:包含老年代和年轻代两个区域,年轻代又可分为Eden区和from区和to区比例为8:1:1;老年代:年轻代=2:1;

2).虚拟机栈:

  • 是否为线程共享:线程私有的; 栈都是由一个一个栈帧组成的,每一个栈帧都包含:局部变量表、操作数栈、动态链接、方法出口信息 弹出方式有两种一个是return 一个是异常抛出; 局部变量表、操作数栈的最大深度在编译器就确定; 通过 -Xss 设置栈的大小 递归很容易出现栈溢出;

  • 存储什么:

    • 局部变量表:存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。以slot槽形式存储,4个字节占用一个槽;
    • 操作数栈:操作数栈保存计算过程中的中间结果,同时作为计算过程中的变量临时的存储空间
    • 动态链接:每一个栈帧内都包含一个引用地址,这个引用的地址是运行时常量池中改该栈帧所属的方法的引用;这个引用就是为了支持动态连接,在java源文件被编译成字节码文件时,所有变量和方法引用都作为符号引用,保存在class文件的常量池中; 那么动态连接就是将符号引用转换为调用的直接引用
    • 方法出口:略

3).本地方法区:1.7叫做永久代 1.8后改为元空间

  • 是否为线程共享区域:线程共享区域

  • 存放什么: 方法区存着类的信息,常量和静态变量,即类被编译后的数据。这个说法其实是没问题的,只是太笼统了;更加详细一点的说法是方法区里存放着类的版本,字段,方法,接口和常量池。常量池里存储着字面量和符号引用。

4)程序计数器:

  • 是否为线程共享区域:线程私有区域,每一个线程都有自己的执行过程都要存储自己下一步要执行的字节码指令地址,各个线程是独立计算的,从而不会出现互相干;

  • 存储什么:存储下一条指令地址,也就是即将要执行的指令代码。cup是时间片切换的,执行到该线程后要知道从哪里开始执行,这是需要程序计数器来寄存的;

  • CPU时间片:宏观并发,微观并行

5)本地方法区

  • 是否为线程共享区域:否,是线程私有区域

  • 存储什么:native方法


二.双亲委派的定义,作用,以及如何破坏?

  • 双亲委派:
  1. 双亲委派是为了程序在加载类时候提供安全可靠的机制,
  • 双亲委派的过程:

    1. 加载前会判断这个类是否已经加载,如果加载过直接返回class对象;
    2. 如果没有加载过会委托父类加载,如果父类不能加载就会继续委托上级的父类加载器加载;
    3. 如果最顶层的类加载器依然不能加载就会用当前类的加载器加载;
    4. 如果当前类加载器不能加载,会抛出ClassNotFoundException异常;
  • 双亲委派有什么好处:

    保证着java核心类不会被篡改;

  • 如何破坏双亲委派模式:

    首先需要继承classLoad,如果不想打破就实现findClass()方法,这样无法被父类加载器加载的类就会通过这个方法进行加载,如果打破就需要重新ClassLoad()方法;因为类在加载时候就是通过调用classLoad()方法让父类加载器加载类;


三.年轻代什么时候晋升到老年代?

  1. 晋升:当年轻代的内存空间不足时,JVM 会将年轻代中的对象晋升到老年代。晋升的对象通常是那些经过多次垃圾回收仍然存活的对象。
  2. 动态年龄判断:当年轻代中的对象达到一定的年龄时,JVM 会根据对象的年龄和大小来判断是否将其晋升到老年代。这个年龄可以通过参数 -XX:MaxTenuringThreshold 来设置。
  3. 大对象直接进入老年代:如果一个对象的大小超过了年轻代的内存空间,那么该对象会直接进入老年代。这个大小可以通过参数 -XX:PretenureSizeThreshold 来设置。
  4. Survivor 空间不足:当年轻代中的 Survivor 空间不足以容纳晋升的对象时,JVM 会将这些对象直接晋升到老年代。

四.空间分配担保机制

空间分配担保机制:在发生Young GC之前,虚拟机会检查老年代最大可用的连续空间 是否大于 新生代所有对象的总空间。如果大于,则此次Minor GC是安全的,如果小于,则虚拟机会查看 -XX:HandlePromotionFailure 设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续 检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小 如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的,比如晋升对象大于平均值; 如果小于,则进行一次Full GC。如果HandlePromotionFailure=false,则进行一次Full GC

五.1.7永久代和1.8元空间区别?

  1. 存储位置:永久代是在 Java 堆中的一个特殊区域,而元空间是在本地内存中的。 大小调整:永久代的大小是有限制的,并且必须在启动时指定,而元空间可以根据需要自动调整大小。
  2. 垃圾收集:永久代使用 Java 堆的垃圾收集器进行垃圾回收,而元空间使用本地内存的垃圾收集器。 . 存储内容:永久代主要存储类的信息(如类名、方法名、字段名等),而元空间存储的是类的元数据(如类的结构、方法表、字段表等)。
  3. 类信息的存储方式:永久代中的类信息是使用永久代专用的类加载器加载和卸载的,而元空间中的类信息是使用与应用程序类加载器相同的类加载器加载和卸载的。