微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Java - JVM - 内存模型&类加载机制

Java - JVM - 内存模型&类加载机制

目录

1 类加载机制

1.1 Java 代码如何运行起来的?

将写好的.java代码打包(jar包或war包),然后部署到线上机器

打包过程中会把.java 文件编译成 .class 字节码文件,字节码文件才可以运行起来

部署可以通过Tomcat等容器

1.1.1 编译好的字节码 .class 文件如何运行起来?

使用命令如 java -jar,使用命令之后会启动一个JVM 进程,类加载器将字节码文件加载到 JVM中,JVM基于字节码执行引擎执行加载到内存里的类

1.2 JVM 在什么情况下会加载一个类?
1.2.1 类从加载到使用的简单过程?
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
1.2.2 什么时候从.class 字节码文件中加载一个类到JVM 内存中?

简单来当在代码中用到该类时加载,

具体的话首先加载代码中包含 main() 方法的主类,该主类们会在JVM 进程启动之后被加载到内存,然后加载在main() 方法中使用到的类

1.2.3 验证阶段的作用是什么及为什么需要验证?
  • 作用:根据 Java 虚拟机规范校验加载进来的字节码文件内容是否符合规范

  • 为什么需要?

    • 字节码文件可能被修改导致不符合规范进而导致 JVM没法执行该字节码文件
  • 不符合规范的将会抛出java.lang.VerifyError 错误

  • 低版本的JVM 无法加载一些高版本的类库

1.2.4 准备阶段的作用?
  • 装备阶段会给类变量(即static 修饰的变量)分配内存空间并赋认初始值
  • 准备阶段类的实例对象还没有分配内存,所以给类变量初始化为认值发生在方法区上(java 8之后堆区)

下列代码的区别即原因?

//code 1
public class A {
         static int a ;
         public static void main(String[] args) {
             System.out.println(a);
         }
     }

//code2
public class A {
     public static void main(String[] args) {
         int a ;
         System.out.println(a);
     }
 }     

区别:代码1将正常执行并输出0,代码2将无法通过编译

原因:局部变量不像类变量一样存在准备阶段,如果没有赋初值便不能使用。类变量有两次赋值的过程,一次在准备阶段赋予认初值,另一次在初始化阶段赋定义的值

1.2.5 解析阶段的作用?

将符号引用替换为直接引用的过程,且解析阶段保证了相互引用的完整性,把继承与组合推进到进行时

符号引用是一种定义,可为任何字面上的含义。而直接引用就是直接指向目标的指针,相对偏移量

解析阶段的大致流程:

  • 类或接口的解析
  • 方法的解析
  • 接口方法解析
  • 字段解析

与解析阶段相关的常见异常

java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错

java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误

java.lang.NoSuchMethodError 找不到相关方法时的错误
1.2.6 初始化阶段的作用?
  • 执行初始化代码,为成员变量进行赋值(如果有类变量的赋值语句也会执行)

初始化时的两个规则:

(1)static 语句块,只能访问到定义在static 语句块之前的变量,static 语句块之后的变量可以赋值但不可以访问

//下面代码输出结果为 1 0,原因是初始化阶段初始化的顺序为代码排列的顺序,所以a的值先为0再为1,b的值先为1再为0
public class teatA {
    static int a = 0;
    static {
        a = 1;
        b = 1;
    }
    static int b = 0;

    public static void main(String[] args){
        System.out.println(a);
        System.out.println(b);
    }
}

//将代码做以下改动后结果为 1 1
public class teatA {
    static int a = 0;
    static {
        a = 1;
        b = 1;
    }
    static int b;

    public static void main(String[] args){
        System.out.println(a);
        System.out.println(b);
    }
}

// 以下代码编译都会报错非法的前向应用
 static {
        b = b+1;
        System.out.print(b);
    }
    static int b;

(2)JVM 会保证在初始化一个类的时候如果其父类还没有初始化必须先初始化其父类

  • JVM 第一个执行类初始化方法的一定是 java.lang.Obejct
  • 父类中的 static 语句块 要优先于子类
  • 有什么区别?(类的初始化和对象的初始化的区别?)
    • static 字段(类变量)和static代码块是属于类的,在类加载的初始化阶段已经执行完毕,类信息存放在方法区中,只会执行一次,对于 方法
    • 对象初始化,在new 对象调用构造方法就是,用来初始化对象属性,而每次新建对象都会执行
//测试类1
public class testB {
    static {
        System.out.println("1");
    }

    public testB(){
        System.out.println("2");
    }
    
}

//测试类2
public class B extends testB{
    static {
        System.out.println("a");
    }

    public B() {
        System.out.println("b");
    }

    public static void main(String[] args) {
        testB tb = new B();
        tb = new B();
    }
}

//输出结果为1 a 2 b 2 b
1.2.7 什么时候会初始化一个类?
  • new 来实例化类的对象时
  • 包含 main() 方法的主类也是立马初始化的
1.2.8 Java 有哪些类加载器?

(1)启动类加载器(Bootstrap ClassLoader)

  • 主要负责加载 Java 安装目录下 lib目录里的 Java 最核心的类库,即rt.jar,resources.jar,charsets.jar 等
    • 加载 jar包的路径可以指定,通过命令
-Xbootclasspath 参数
  • 随着JVM的启动就加载

(2)扩展类加载器(Extension ClassLoader)

java.ext.dirs

(3)应用程序类加载器(Application ClassLoader)

  • 负责加载 Classpath 环境变量所指定路径中的类(自己写好的java代码

(4)自定义类加载器(Custom ClassLoader)

  • 根据需求去加载类
1.3 双亲委派机制

概念:

  • 除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载

优点:

  • 避免多层级的加载器结构重复加载类
  • 即使重写多个核心类,最终也是系统认加载器加载

代码

逻辑:

先检查该类是否已经加载过了,若没有加载则调用父加载器的的loadClass方法,如父加载器为空则认使用启动类加载器作为父类加载器。若父类加载失败则抛出异常再调用自己的findClass 方法加载

1.3.1 如何打破双亲委派机制?

(1)Tomcat

  • 为什么tomcat需要打破双亲委派机制?

tomcat是一个web容器,可能会部署多个应用程序,不同应用程序可能会依赖同一个第三方类库的不同版本(如Spring4 和 Spring5),在这些不同版本的类库中会存在类名一样实现不一样的类,如果都是交给父加载器会冲突

  • tomcat 容器需要解决的问题有哪些?

    • 如何保证每个应用程序的的类库都是独立的,相互隔离的
    • 如何保证一个web容器中相同类库的同一个版本可以共享
    • 如何让web容器自己依赖的类库和应用程序的类库隔离
    • 如何支持热部署,让jsp修改后web容器不用重启
  • tomcat自定义类加载器

    • commonClassLoader: tomcat最基本的类加载器, 加载路径中的class可以被tomcat容器本身和各个webapp访问
    • catalinaClassLoader: tomcat容器中私有的类加载器, 加载路径中的class对于webapp不可见
    • sharedClassLoader: 各个webapps共享的类加载器, 加载路径中的class对于所有的webapp都可见, 但是对于tomcat容器不可见
    • webAppClassloader:各个webapp私有的类加载,加载路径中的class只对当前webapp可见

  • 如何打破双亲委派机制的?

项目打war包,tomcat会自动生成该war包专门的WebAppClassLoader,对于需要加载的非基础类,由WebAppClassLoader优先加载,加载不到再找上层类加载器进行加载。它可以使用sharedClassLoader所加载的类,实现了共享和分离的功能

  • 在应用目录下重写核心类会覆盖吗?(比如 ArrayList)

不会覆盖,WebAppClassLoader 的执行原理:

(1)先从当前类加载器的本地缓存中加载类,如果找到便返回

(2)本地缓存没有,调用ClassLoader的findloadedClass方法查看JVM是否已经加载过该类,如果加载过直接返回

(3)通过系统类加载器(Sysytem ClassLoader)加载该类,防止应用中的类覆盖J2SE的类,如果没有这一步自己写的类会替换Tomcat容器Lib包中的类

(4)先判断是否需要parent加载,通常delegateLoad == false即不让parent先加载

(5)delegateLoad == false 或者 parent加载失败,调用自身的加载机制

(6)自己加载失败,则请求parent代理加载

  • JSP 如何实现热部署?

tomcat为每一个jsp生成类加载器,当jsp文件修改时,tomcat定义了一个thread通过update time变化来监听不同文件夹中文件内容是否修改,如果修改了便替换掉JsperLoader实例,再建立一个新的Jsp类来实现热部署

(2)SPI

  • 什么是SPI?

SPI (Service Provider Interface),是一种服务发现机制。它通过在Classpath路径下的meta-inf/services文件查找文件自动加载文件里所定义的类

SPISPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载

//1. 首先需要创建meta-inf 目录, idea中不会自动生成,直接再Resources目录下创建即可

//2.编写测试接口及其实现类

//3.在meta-inf 目录下创建services目录,并创建一个以测试接口全路径名(reference)命名的文件内容为该接口实现类的全路径名,多个实现类使用换行符隔开

//4.通过ServiceLoader.load 方法获取实例
ServiceLoader<SPIService> services = ServiceLoader.load(xxxService.class);
        Iterator<xxxService> iterator = services.iterator();
        while (iterator.hasNext()){
            xxxService service = iterator.next();
            service.xxx();
        }     
  • SPI 在 JDBC 中的应用
//1. 通常情况需要手动注册驱动
Class.forName("com.MysqL.jdbc.Driver")

//2. 删除代码也能自动成功注册驱动

//3. 在MysqL驱动代码中存在该文件
mysql-connector-java-8.0.15.jar!/meta-inf/services/java.sql.Driver

//4. 该文件内容为
com.MysqL.cj.jdbc.Driver

//5. SPI 可自动加载并实现该类
//1. 通过DriverManager 通过 SPI 实现数据库驱动初始化
//1.1 DriverManager类ensureDriversInitialized()方法初始化
//1.2 JDBC_DRIVERS_PROPERTY为 jdbc.drivers
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty(JDBC_DRIVERS_PROPERTY);
                    }
                    
//1.3 因为在驱动代码中路径为meta-inf/services/java.sql.Driver,获得的实例为Driver 接口的实现类(该文件中所声明的具体实现类,这里为com.MysqL.cj.jdbc.Driver)
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    
//2. 因为通过next方法DriverManager获取MysqL driver的实例,完成了向Drivermanger注册自身的实例,向registeredDrivers集合中加入实例
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws sqlException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (sqlException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

//3. 创建Connection 连接,通过DriverManager.getConnection 方法获取上下文类加载器,确保驱动已经初始化,从registeredDrivers获取MysqL的Driver实例,调用connection方法建立连接
callerCL = Thread.currentThread().getContextClassLoader();
ensureDriversInitialized();
for (DriverInfo aDriver : registeredDrivers) {
Connection con = aDriver.driver.connect(url, info);
}
  • 为什么JDBC使用SPI需要打破双亲委派机制?

(1)从meta-inf/services/java.sql.Driver 文件得到实现类

(2)要获取该类的实例需要SPI 源码中Class.forName("xxxxx")方法来加载该实例

(3)Class.forName 方法认使用当前类的ClassLoader,DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的。它们的类加载器是 Bootstrap ClassLoader

(4)Bootstrap ClassLoader去加载非rt.jar包里的xxxDriver会找不到

(5)要加载xxxDriver 需要调用 Application Classloader或 其他自定义类加载器

(6)问题在于如何在父加载器中调用子加载器去加载类

  • SPI 如何打破双亲委派机制
//1. ensureDriversInitialized中获得驱动类的实例
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    
//2. 进入ServiceLoader.load方法查看如何使用类加载器加载的,当前类加载器设置为了上下文类加载器,继续查找启动main方法的的类加载      
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }
    
//3. 进入Luancher类(jre用来启动函数main的类),上下文的类加载器为Application ClassLoader,可以用来加载第三方类库   
public Launcher() {
 Launcher.ExtClassLoader var1;
 try {
     var1 = Launcher.ExtClassLoader.getExtClassLoader();
 } catch (IOException var10) {
     throw new InternalError("Could not create extension class loader", var10);
 }
 
 try {
     this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
 } catch (IOException var9) {
     throw new InternalError("Could not create application class loader", var9);
 }
 Thread.currentThread().setContextClassLoader(this.loader);
 ...
 }
1.4 JVM 内存区域
1.4.1 存放类的方法

从字节码文件 ".class" 加载进来的类

  • Java 8 之前,类放在堆区的的永久代(Perm Gen),还区域有大小限制,容易造成 JVM 内存溢出,从而造成JVM 崩溃

    • Java 8 之后-XX:PermSize 和 -XX:MaxPermSize 参数调优没有意义
  • Java 8之后,Perm 区彻底废除,使用在非堆上的元数据空间(@R_502_5635@space),因为在非堆上,可以使用操作系统的内存,JVM不会出现方法区内存溢出,但可能会造成操作系统的死亡

    • 使用 -XX:Max@R_502_5635@spaceSize来控制大小

字符串常量存放在什么地方?

  • Java 8 之前存放在Perm 区,Java 8之后移动到堆区,即执行intern方法后存的地方
1.4.2 程序计数器
  • 为什么需要字节码执行引擎?

代码需要被编译成字节码文件,机器通过字节码文件编译出来的代码指令去执行

  • 程序计数器的作用?

用来记录当前执行的字节码指令的位值,即记录目前执行到了哪一条字节码指令

  • 为什么需要程序计数器?

JVM 支持多线程,代码可能开启多个线程并发执行不同的代码代码指令,每个线程都有一个程序计数器,记录当前线程执行到哪一条字节码指令

  • 程序计数器还存储了运行流程

包括正在执行的指令,跳转,分支,循环,异常处理等

1.4.3 Java 虚拟机栈
  • Java 虚拟机栈是基于线程的,Java 代码在执行时一定是线程来执行方法中的代码,哪怕只有main方法,也会由main 线程来执行main()方法里的代码

  • 每个线程都会有对应的Java 虚拟机栈

  • 线程如果执行了一个方法便会创建一个对应的栈帧并压入该线程的Java虚拟机栈

  • 流程为:调用方法创建栈帧入栈,方法执行完毕出栈

栈帧包含:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口(返回地址)

返回地址:

  • returnAddress,该类型只存在于字节码层面
  • 对于 JVM 来说,程序就是存储在方法区的字节码指令,而 returnAddress 类型的值就是指向特定指令内存地址的指针
  • 两层的栈。第一层是栈帧,对应着方法;第二层是方法的执行,对应着操作数
    • 线程方法栈(栈)->栈帧(元素)=>方法级别的操作
    • 栈帧里的操作数栈(栈)->操作数(元素)=> 字节码指令级的操作
1.4.3 堆
  • JVM 上最大的内存空间
  • 对象存放在堆区
  • 垃圾回收的操作对象为堆
  • 堆区一般程序启动便申请但并不一定全部使用
  • 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)
  • 由于对象的大小不一,在长时间运行后,堆空间会被许多细小的碎片占满,造成空间浪费。所以,仅仅销毁对象是不够的,还需要堆空间整理

对象创建的时候,到底是在堆上分配,还是在栈上分配呢?

  • 与对象的类型和在 Java 类中存在的位置有关
  • 对于普通对象(引用数据类型),JVM 先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中
  • 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况:
    • 方法体内声明了基本数据类型的对象,它就会在栈上直接分配
    • 其他情况,都是在堆上分配
1.4.4 其他内存区域

JDK 底层API中 native 方法,比如 public native int hashCode()

  • 调用native方法会有线程对应的本地方法栈,与java虚拟机栈类似,存放native 方法的局部变量表之类信息

堆外内存分配

  • NIO 中 allocateDirect API可以在堆外分配内存空间
  • Java 虚拟机里的 DirectByteBuffer 可引用和操作堆外内存空间
1.4.5 堆、非堆、本地内存的关系?

操作系统有8G。-Xmx分配了4G(堆内内存),@R_502_5635@space使用了256M(堆外内存) 剩下的 8G-4G-256M ,就是操作系统剩下的本地内存。具体有没有可能变成堆外内存,要看情况。 比如: (1)netty的direct buffer使用了额外的120MB内存,那么现在JVM占用的堆外内存就有 256M+120M (2)使用了jni或者jna,直接申请了内存2GB,那么现在JVM占用的堆外内存就有256M+120M+2GB (3)网络socket连接等,占用了操作系统的50MB内存 这个时候,留给操作系统的就只剩下了:8GB-4GB-256M-120M-2GB-50M。具体“堆和堆外”一共用了多少,可以top命令,看RSS

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐