您的位置:首页 > 职场人生

我以为我很了解JVM,直到我遇见了阿里面试官

lovethe 2020-07-28 19:48 85 查看 https://blog.51cto.com/1484943

1.在Java中主要有以下三种类加载器:

引导类加载器(bootstrap class loader)

  • 用来加载java的核心库(String,Integer,List…)在jre/lib/rt.jar路径下的内容。使用c代码来实现的,并不继承自java.lang.ClassLoader.

  • 加载扩展类加载器和应用程序加载器,并指定他们的父类加载器。

扩展类加载器(extensions class loader)

  • 用来加载java的扩展库(jre/ext/*.jar路径下的内容),java虚拟机的实现会自动提供一个扩展目录。该类加载器在此目录里面查找并加载java类。

应用程序类加载器(application class loader)

  • 它根据java应用的类路径(classpath路径),一般来说java应用的类都是由它来完成加载的。

自定义类加载器

  • 开发人员可以通过继承java.lang.ClassLoader类的方式实现在即的类加载器,以满足一些特殊的要求。扩展类加载器、应用程序类加载器和自定义类加载器都是由java实现,都继承java.lang.ClassLoader类。

2.类加载器的代理模式:双亲委托机制

当某个类加载器在接收到加载类的请求后,首先将加载任务委托给父类加载器,依次追溯,如果父类加载器能够完成类加载任务,就成功返回,只有父类加载器无法完成加载任务时,才自己加载。

双亲机制是为了保证java核心库的类型安全,不会出现用户能自定义java.lang.Object类的情况。

双亲委托机制是代理模式的一种,并不是所有类加载器都采用双亲委托机制,Tomcat服务器类加载器也使用代理模式,不同的是它是首先尝试自己去加载某个类,如果找不到再代理给父类加载器。

3.类加载机制

jvm把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成jvm可以直接使用的java类型的过程。

类加载过程:类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括7个阶段:加载、验证、准备、解析、初始化、使用、卸载(其中验证、准备和解析这三个部分统称为连接)。其中加载、验证、准备、初始化和卸载这五个阶段的顺序是一定的,而解析阶段不一定,在某种情况下,可以在初始化之后再开始,这是为了支持java语言的运行时绑定。

  1. 加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。

  2. 连接:将java类的二进制代码合并到jvm的运行状态之中的过程。验证:确保加载的类信息符合jvm规范,没有安全方面的问题。准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行。解析:虚拟机常量池内的符号引用替换为直接引用的过程。(比如Strings = “aaa”,转化为s的地址指向"aaa"的地址)。

  3. 初始化:初始化阶段是执行类构造器方法的过程,类构造器方法是由编译器自动收集类中的所有变量的赋值动作和静态语句块(static块)中的语句合并产生的。当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先进行其父类的初始化,虚拟机会保证一个类的构造器方法在多线程环境中被正确加锁和同步。当访问一个java类的静态域时,只有真正申明这个静态变量的类才会被初始化。

4.类的加载过程分为:类的主动引用和类的被动引用

类的主动引用(一定会发生类的初始化):

  • new一个类的对象

  • 调用类的静态成员(除了final常量)和静态方法

  • 使用java.lang.reflect包的方法对类进行反射调用

  • 当初始化一个类,如果父类没有被初始化,先初始化其父类

  • 当要执行某个程序时,一定先启动main方法所在的类

类的被动引用(不会发生类的初始化)

  • 当访问一个静态变量时,只有真正声明这个静态变量的类才会初始化(通过子类引用父类的静态变量,不会造成子类的初始化)

  • 通过数组定义类应用,不会触发此类的初始化A[] a = new A[10];

  • 引用常量(final类型)不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)

5.java中类的加载顺序

  1. 虚拟机在首次加载java类时,会对静态初始化块、静态成员变量、静态方法进行一次初始化

  2. 只有在调用new方法时,才会创建类的实例

  3. 类实例创建过程:首先执行父类的初始化块部分,然后是父类的构造方法,再执行子类的初始化块,最后是子类的构造方法

  4. 类实例销毁时,先销毁子类部分,再销毁父类部分。

6.java程序执行过程

首先java源代码文件(.java)会被java编译为字节码文件(.class),然后由jvm中的类加载器加载各个类的字节码文件,加载完毕之后,交由jvm执行引擎执行。

7.jvm区域划分

jvm区域可以根据线程分成线程隔离和线程共享两个部分,其中线程隔离即这些区域是线程独有的,每个线程都会分配这样的区域,包括程序计数器、Java栈和本地方法栈;线程共享的有方法区和堆。

程序计数器(Program Counter Register)

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行事件的,因此在任意具体时刻,一个CPU只会执行一个线程中的指令,为了能够使得每个线程都在线程切换或能够恢复到切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。所以程序计数器是每个线程所私有的。在jvm规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中保存的值是undefined。

java栈(vm stack)

java栈也称为虚拟机栈(java vitual machine stack),java栈中存放的是一个个栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(local variables)、操作数栈(perand stack)、指向当前方法所属的类的运行时常量池的引用、方法返回地址和一些额外的附加信息。

本地方法栈(native method stack)

本地方法栈与java栈的作用和原理非常相似,只不过java栈是为执行java方法服务,而本地方法栈是为执行本地方法服务的。在jvm规范中,并没有对本地方法的具体实现方法以及数据结构做强制规定,虚拟机可以自由实现它。在Hotsopt虚拟机中直接就把本地方法栈和java栈合二为一。

方法区(Method Area)

方法区在JVM中是一个非常重要的区域,与堆一样是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法、字段信息)、静态变量、常量以及编译器编译后的代码。在方法区有一个非常重要的部分就是运行时常量池它是每一个类或者接口的常量池的运行时表示形式,在类和接口被加载到jvm后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间,也可将新的常量放入运行时常量池中,比如String的intern方法。可以认为方法区就是永久代。

堆(Heap)

java中的堆是用来存储对象以及数组,数组的引用是存放在java栈中的。堆被所有线程共享,在jvm中只有一个堆。

在java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。

新生代又被划分为三个区域:Eden和两个幸存区。

这样划分的目的是为了使JVM能够更好地管理堆内存中的对象,包括内存的分配及回收。

新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)后仍然存活的对象。

方法区主要存放类与类之间关系的数据,这部分数据被加载到内存以后,基本上不会发生变更,但是后期方法区也会被回收,回收的条件非常的苛刻;java堆中的数据基本上是朝生夕死的,用完之后就会被回收;java栈和本地方法栈中的数据,满足先进后出的原则,当要获取栈低的元素,必须把栈顶的元素出栈,回收率为100%;程序计数器是唯一一块不会内存溢出的区域。

8.引用

java中如果一个对象,没有一个引用指向它,那么它就被认为是一个垃圾。

  1. java内存管理分为内存分配和内存回收,不需要程序员参与。

  2. 垃圾回收机制主要看对象是否有引用指向。java对象的引用包括强引用、软引用、弱引用和虚引用。

  3. 强引用:是指创建一个对象,并把这个对象赋给一个引用变量。强引用有引用变量指向时永远都不会被回收,即使内存不足时。

  4. 软引用:通过SoftReference类来实现,当系统内存充足时,系统不会进行软引用的内存回收,软引用的对象和强引用没有太多区别,但是内存不足时会回收软引用的对象。

  5. 弱引用:通过WeakReference类来实现,具有很强的不确定性,因为垃圾回收每次都会回收弱引用的对象。

  6. 虚引用:软引用和弱引用都可以单独使用,虚引用不能单独使用,必须关联引用队列。虚引用的作用就是跟踪对象被垃圾回收的状态,程序可以通过检测与虚引用关联的虚引用队列是否已经包含了指定的虚引用,从而了解虚引用对象是否即将被回收。它允许你知道对象何时从内存中移除。

java中引用越弱表示对垃圾回收器的限制越少,对象越容易被回收。

9.垃圾回收

1、引用计数器算法:当创建对象时,为这个对象在堆栈空间中分配地址,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁时,引用计数器-1,当引用计数器被减为0的时候,标志着这个对象已经没有引用了,可以被回收。但是当代码出现下面的情形时,该算法无法适用,objA指向objB,而objB又指向objA,这样其他所有引用都消失了之后,objA和ObjB还是有一个相互的引用,无法回收,但实际上这两个对象都已经没有额外的引用了,已经是垃圾了。

ObjA.obj = ObjB;ObjB.obj = ObjA;1

2、根搜索算法(GC Root):把所有的引用关系看做一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。java中可作为GC Root的对象有:虚拟机栈中的引用对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象。

3、收集后的垃圾通过什么算法来回收?

  • 标记-清除算法:采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但是由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

  • 复制算法(用于新生代):复制算法采用从根集合扫描,并将存活对象复制到一块新的、没有使用过的空间中,这种算法当内存中存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。复制算法中,新生代中每次只使用Eden区和一块幸存区存储数据,当幸存区达到饱和状态时,将幸存区的存活的对象移动到另一块幸存区。

  • 标记-整理算法(用于老年代):标记-整理算法和标记-清除算法采用一样的方式进行对象的标记,但是清除时不同,在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。解决了内存碎片的问题。

  • 分代回收机制:

新生代:绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象此区域消失的过程称为“minor GC”.

一共有三个空间,其中包含一个伊甸园区(Eden)和两个幸存区(survivor)。各空间执行顺序如下:

1、绝大多数刚刚被创建的对象会存放在伊甸园空间。

2、在伊甸园空间执行了一次 GC后,存活的对象被移动到其中一个幸存者空间。

3、此后,在伊甸园空间执行GC后,存活的对象会被堆积在同一个幸存者空间。

4、当一个幸存者空间饱和户,还在存活的对象会被移动到另一个幸存者空间,之后会清空已经饱和的那个幸存者空间。

5、在以上的步骤中重复几次依然存活的对象就会被移动到老年代。

老年代:对象没有变得不可达,并且从新生代中存活下来,就会被拷贝到这里,其所占的空间要比新生代多。也正是因为其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,称为“major GC”。

永久代:也被称为方法区,用来保存类常量以及字符串常量。因此这个区域不是用来永久的存储那些从老年代存活下来的对象。这个区域也可能发生GC,并且发生在这个区域上的GC事件也被称为major GC。


标签: