上海携程 java 高级面试题(二)
一、什么是 tomcat 类加载机制?
在 tomcat 中类的加载稍有不同,如下图:
当 tomcat 启动时, 会创建几种类加载器:
1、Bootstrap 引导类加载器
加载 JVM 启动所需的类, 以及标准扩展类(位于 jre/lib/ext 下)
2、System 系统类加载器
加 载 tomcat 启 动 的类 , 比 如 bootstrap.jar , 通 常在 catalina.bat 或 者 catalina.sh 中指定。位于 CATALINA_HOME/bin 下。
3、Common 通用类加载器
加载 tomcat 使用以及应用通用的一些类,位于 CATALINA_HOME/lib 下,比如 servlet-api.jar
4、webapp 应用类加载器
每个应用在部署后, 都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib 下的 jar 文件中的 class 和 WEB-INF/classes 下的 class 文件。
当应用需要到某个类时, 则会按照下面的顺序进行类加载:
使用 bootstrap 引导类加载器加载
使用 system 系统类加载器加载
使用应用类加载器在 WEB-INF/classes 中加载
使用应用类加载器在 WEB-INF/lib 中加载
使用 common 类加载器在 CATALINA_HOME/lib 中加载
二、类加载器双亲委派模型机制?
什么是双亲委派模型(Parent-Delegation Model)?为什么使用双亲委派模型?
JVM 中加载类机制采用的是双亲委派模型, 顾名思义, 在该模型中, 子类加载器收到的加载请求, 不会先去处理, 而是先把请求委派给父类加载器处理, 当父类加载器处理不了时再返回给子类加载器加载;
为什么使用双亲委派模型?
因为安全。使用双亲委派模型来组织类加载器间的关系,能够使类的加载也具有层次关系,这样能够保证核心基础的 Java 类会被根加载器加载,而不会去加载
用户自定义的和基础类库相同名字的类,从而保证系统的有序、安全。
三、Java 内存分配?
1、 基本概念
每运行一个 java 程序会产生一个 java 进程, 每个 java 进程可能包含一个或者多个线程, 每一个 Java 进程对应唯一一个 JVM 实例, 每一个 JVM 实例唯一对应一个堆, 每一个线程有一个自己私有的栈。进程所创建的所有类的实例( 也就是对象) 或数组( 指的是数组的本身, 不是引用) 都放在堆中,并由该进程所有的线程共享。
Java 中分配堆内存是自动初始化的, 即为一个对象分配内存的时候,会初始化这个对象中变量。
虽然 Java 中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在栈中分配,也就是说在建立一个对象时在堆和栈中都分配内存, 在堆中分配的内存实际存放这个被创建的对象的本身, 而在栈中分配的内存只是存放指向这个堆对象的引用而已。局部变量 new 出来时, 在栈空间和堆空间中分配空间, 当局部变量生命周期结束后, 栈空间立刻被回收, 堆空间区域等待 GC 回收。
具体的概念:JVM 的内存可分为 3 个区:堆(heap)、 栈(stack)和方法区(method,也叫静态区):
堆区:
存储的全部是对象, 每个对象都包含一个与之对应的 class 的信息(class 的目的是得到操作指令) ;
jvm 只有一个堆区(heap), 且被所有线程共享, 堆中不存放基本类型和对象引用,只存放对象本身和数组本身;栈区:
每个线程包含一个栈区, 栈中只保存基础数据类型本身和自定义对象的引用;每个栈中的数据(原始类型和对象引用)都是私有的, 其他栈不能访问;
栈分为 3 个部分:基本类型变量区、 执行环境上下文、 操作指令区(存放操作指令);
方法区(静态区) :
被所有的线程共享, 方法区包含所有的 class(class 是指类的原始代码, 要创建一个类的对象, 首先要把该类的代码加载到方法区中, 并且初始化) 和 static 变量。
方法区中包含的都是在整个程序中永远唯一的元素, 如 class, static 变量。
2、 实例演示
AppMain.java
运行该程序时, 首先启动一个 Java 虚拟机进程, 这个进程首先从 classpath 中找到 AppMain.class 文件, 读取这个文件中的二进制数据, 然后把 Appmain 类的类信息存放到运行时数据区的方法区中, 这就是 AppMain 类的加载过程。
接着, Java 虚拟机定位到方法区中 AppMain 类的 Main()方法的字节码, 开始执行它的指令。这个 main()方法的第一条语句就是:
该语句的执行过程:
1、 Java 虚拟机到方法区找到 Sample 类的类型信息, 没有找到, 因为 Sample 类还没有加载到方法区(这里可以看出, java 中的内部类是单独存在的, 而且刚开始的时候不会跟随包含类一起被加载, 等到要用的时候才被加载) 。Java 虚拟机立马加载 Sample 类, 把 Sample 类的类型信息存放在方法区里。
2、 Java 虚拟机首先在堆区中为一个新的 Sample 实例分配内存, 并在 Sample 实例的内存中存放一个方法区中存放 Sample 类的类型信息的内存地址。
3、 JVM 的进程中, 每个线程都会拥有一个方法调用栈, 用来跟踪线程运行中一系列的方法调用过程, 栈中的每一个元素就被称为栈帧, 每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、 局部变量和运算过程中的临时数据。
4、 位于“=”前的 Test1 是一个在 main()方法中定义的一个变量(一个 Sample 对象的引用) ,因此, 它被会添加到了执行 main()方法的主线程的 JAVA 方法调用栈中。而“=”将把这个 test1 变量指向堆区中的 Sample 实例。
5、JVM 在堆区里继续创建另一个 Sample 实例, 并在 main 方法的方法调用栈中添加一个 Test2 变量, 该变量指向堆区中刚才创建的 Sample 新实例。
6、JVM 依次执行它们的 printName()方法。当 JAVA 虚拟机执行 test1.printName()方法时, JAVA 虚拟机根据局部变量 test1 持有的引用, 定位到堆区中的 Sample 实例, 再根据 Sample 实例持有的引用, 定位到方法去中 Sample 类的类型信息, 从而获得 printName()方法的字节码,接着执行 printName()方法包含的指令, 开始执行。
评论