JVM面试

关于JVM面试所必须知道的内容

一、在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类。

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

就是某个特定的类加载器在接收到加载类的请求后,首先将加载任务委托给父类加载器,一次追溯,直到最高的爷爷辈的,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成次加载任务时,才自己加载。

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

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

三、类加载机制:

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

类加载过程:

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了这7个阶段

加载、验证、准备、解析、初始化、使用、卸载

其中,验证、准备和解析这三个部分统称为连接(linking)

 

其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

(1)加载

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

(2)链接

将java类的二进制代码合并到jvm的运行状态之中的过程

    2.1 验证:确保加载的类信息符合jvm规范,没有安全方面的问题。

    2.2 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

    2.3 解析:虚拟机常量池内的符号引用替换为直接引用的过程。(比如String s ="aaa",转化为 s 的地址指向“aaa”的地址)

(3)初始化

初始化阶段是执行类构造器方法的过程。类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。

当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先进行其父类的初始化,虚拟机会保证一个类的构造器方法在多线程环境中被正确加锁和同步。

当访问一个java类的静态域时,只有真正声明这个静态变量的类才会被初始化。

四、类加载过程分为:类的主动引用和类的被动引用

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

  --new一个类的对象

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

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

  --当初始化一个类,如果其父类没有被初始化,则先初始化他的父类

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

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

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

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

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

五、java 中类的加载顺序

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

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

3、类实例创建过程:按照父子继承关系进行初始化,首先执行父类的初始化块部分,然后是父类的构造方法;再执行本类继承的子类的初始化块,最后是子类的构造方法

4、类实例销毁时候,首先销毁子类部分,再销毁父类部分

六、java程序执行过程

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

七、Jvm区域划分

整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

运行时数据区通常包括这几个部分:

程序计数器(Program Counter Register)

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

Java栈(VM Stack)
Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。

本地方法栈(Native Method Stack)

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

方法区(Method Area)

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

(Heap)

Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。堆是被所有线程共享的,在JVM中只有一个堆。

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

新生代 ( Young ) 又被划分为三个区域:Eden(伊甸园)、From Survivor(幸存者s0)、To Survivor(幸存者s1)。

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

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

我们知道,方法区主要存放类与类之间关系的数据,而这部分数据被加载到内存之后,基本上是不会发生变更的,Java堆中的数据基本上是朝生夕死的,我们用完之后要马上回收的,而Java栈和本地方法栈中的数据,因为有后进先出的原则,当我取下面的数据之前,必须要把栈顶的元素出栈,因此回收率可认为是100%;而程序计数器我们前面也已经提到,主要记录用户线程执行的行号等一些信息,这块区域也是被认为是唯一一块不会内存溢出的区域。

在SunHostSpot的虚拟机中,对于程序计数器是不回收的,而方法区的数据因为回收率非常小,而成本又比较高,一般认为是“性价比”非常差的,所以Sun自己的虚拟机HotSpot中是不回收的!但是在现在高性能分布式J2EE的系统中,我们大量用到了反射、动态代理、CGLIB、JSP和OSGI等,这些类频繁的调用自定义类加载器,都需要动态的加载和卸载了,以保证永久代不会溢出,他们通过自定义的类加载器进行了各项操作,因此在实际的应用开发中,类也是被经常加载和卸载的,方法区也是会被回收的!但是方法区的回收条件非常苛刻,只有同时满足以下三个条件才会被回收!
  1、所有实例被回收

  2、加载该类的ClassLoader被回收

  3、Class对象无法通过任何途径访问(包括反射)

八、引用

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

1、java内存管理分为内存分配和内存回收,都不需要程序员负责。

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

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

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

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

6、虚引用:软引用和弱引用可以单独使用,虚引用不能单独使用,必须关联引用队列。虚引用的作用是就跟踪对象被垃圾回收的状态,程序可以通过检测与虚引用关联的虚引用队列是否已经包含了指定的虚引用,从而了解虚引用的对象是否即将被回收。虚引用通过PhantomRefence类实现,它本身对对象没有影响,类似于没有引用,对象甚至感觉不到虚引用的存在,如果一个对象只有一个虚引用存在,那么它就类似没有引用存在。PlantomReference比较特殊,它的get方法总是返回null,所以你得不到它引用的对象。它保存ReferenceQueue中的轨迹。它允许你知道对象何时从内存中移除。

Java的引用对象类在包java.lang.ref下。其中包含了三种显式的引用类型(也即是Reference类的三个子类):SoftReference、 WeakReference、PhantomReference。

三种类型的引用定义了三种不同层次的可达性级别,由强到弱排列如下:SoftReference > WeakReference > PhantomReference,越弱表示对垃圾回收器的限制越少,对象越容易被回收。

九、垃圾回收

1、在JDK1.2之前,使用的是引用计数器算法

当这个类被加载到内存以后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!这种算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题,当我们的代码出现下面的情形时,该算法将无法适应
a) ObjA.obj = ObjB
b) ObjB.obj = ObjA
这样的代码会产生如下引用情形 objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。

2、在JDK1.2之后都使用根搜索算法

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

目前java中可作为GC Root的对象有

  虚拟机栈中引用的对象(本地变量表)

  方法区中静态属性引用的对象

  方法区中常量引用的对象

  本地方法栈中引用的对象(Native对象)

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

(1)标记-清除算法

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

(2)复制算法(用于新生代)

复制算法采用从根集合扫描,并将存活对象复制到一块新的、没有使用过的空间中,这种算法当内存中存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。

(3)标记-整理算法(用于老年代)

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,这些垃圾回收器又分为串行回收方式、并行回收方式和并发回收方式执行,分别运用于不同的场景。

4、分代回收机制

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

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

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

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

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

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

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

  仔细观察这些步骤就会发现,其中一个幸存者空间必须保持是空的。如果两个幸存者空间都有数据,或者两个空间都是空的,那一定标志着你的系统出现了某种错误。

  对象刚刚被创建之后,是保存在伊甸园空间的。那些长期存活的对象会经由幸存者空间转存在老年代空间。

老年代(Old generation): 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为”major GC“(或者”full GC“)。

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

十、java中的垃圾回收器

1.GC是垃圾回收器。垃圾收集器会自动进行内存管理,所以Java程序员不用担心内存管理。

2.垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

3.程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。

4.要请求垃圾收集,可以调用下面的方法之一,通知GC运行,但是Java语言规范并不保证GC一定会执行。

  System.gc()

  Runtime.getRuntime().gc()

5.垃圾回收器的基本原理

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。(根搜索算法)通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。

十一、JVM调优

JVM调优主要是针对内存管理方面的调优,包括控制各个代的大小,GC策略。由于GC开始垃圾回收时会挂起应用线程,严重影响了性能,调优的目是为了尽量降低GC所导致的应用线程暂停时间、 减少Full GC次数。

代大小调优

最关键参数:-Xms、 -Xmx 、-Xmn 、-XX:SurvivorRatio、-XX:MaxTenuringThreshold、-XX:PermSize、-XX:MaxPermSize

-Xms、 -Xmx 通常设置为相同的值,避免运行时要不断扩展JVM内存,这个值决定了JVM heap所能使用的最大内存。

-Xmn 决定了新生代空间的大小,新生代Eden、S0、S1三个区域的比率可以通过-XX:SurvivorRatio来控制(假如值为 4  表示:Eden:S0:S1 = 4:3:3 )

-XX:MaxTenuringThreshold 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC时有效。

-XX:PermSize、-XX:MaxPermSize 用来控制方法区的大小,通常设置为相同的值。

1.避免新生代设置过小

当新生代设置过小时,会产生两种比较明显的现象,一是minor GC次数频繁,二是可能导致 minor GC对象直接进入老年代。当老年代内存不足时,会触发Full GC。

2.避免新生代设置过大

新生代设置过大,会带来两个问题:一是老年代变小,可能导致Full  GC频繁执行;二是 minor GC 执行回收的时间大幅度增加。

3.避免Survivor区过大或过小

-XX:SurvivorRatio参数的值越大,就意味着Eden区域变大,minor GC次数会降低,但两块Survivor区域变小,如果超过Survivor区域内存大小的对象在minor GC后仍没被回收,则会直接进入老年代。

-XX:SurvivorRatio参数值设置过小,就意味着Eden区域变小,minor GC触发次数会增加,Survivor区域变大,意味着可以存储更多在minor GC后仍然存活的对象,避免其进入老年代。

4.合理设置对象在新生代存活的周期

新生代存活周期的值决定了新生代对象在经过多少次minor GC后进入老年代。因此这个值要根据自己的应用来调优,Jvm参数上这个值对应的为-XX:MaxTenuringThreshold,默认值为15次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值