职场面试题总结(20)---Java 堆的结构、Java 内存分配、类加载器双亲委派模型机制

58 阅读11分钟

1、类加载器双亲委派模型机制?

什么是双亲委派模型(Parent-Delegation Model)?为什么使用双亲委派模型? JVM中加载类机制采用的是双亲委派模型,顾名思义,在该模型中,子类加载器收到的加载请求,不会先去处理,而是先把请求委派给父类加载器处理,当父类加载器处理不了时再返回给子类加载器加载。 为什么使用双亲委派模型? 因为安全。使用双亲委派模型来组织类加载器间的关系,能够使类的加载也具有层次关系,这样能够保证核心基础的Java类会被根加载器加载,而不会去加载用户自定义的和基础类库相同名字的类,从而保证系统的有序、安全。

2、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()方法包含的指令,开始执行。

(3)分析 在Java语言里堆(heap)和栈(stack)里的区别 :

栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享(详见下面的介绍)。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

Java中的2种数据类型: 一种是基本类型(primitive types), 共有8类,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。 栈有一个很重要的特性:存在栈中的数据可以共享。假设我们同时定义: int a = 3;  int b = 3; 编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,如果没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。 这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。   另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

3、Java 堆的结构

JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。

堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。 永久代是用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类,永久代中一般包含: 类的方法(字节码…) 类名(Sring对象) .class文件读到的常量信息 class对象相关的对象列表和类型列表 (e.g., 方法对象的array). JVM创建的内部对象 JIT编译器优化用的信息

虚拟机中的共划分为三个代: 年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系 不大。年轻代和年老代的划分是对垃 圾收集影响比较大的。

年轻代: 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

年老代: 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

持久代: 用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

注意: JDK1.8中,永久代已经从java堆中移除,String直接存放在堆中,类的元数据存储在meta space中,meta space占用外部内存,不占用堆内存。 可以说,在java8的新版本中,持久代已经更名为了元空间(meta space)。

4、简述各个版本内存区域的变化?

参考:https://blog.csdn.net/rainnnbow/article/details/50541079