去阿里面试前的准备:JVM(内存,类加载,GC)

目前主流的 Java 虚拟机有哪些?

HotSpot VM

HotSpot VM是绝对的主流。大家用它的时候很可能就没想过还有别的选择,或者是为了迁就依赖了Oracle/Sun JDK某些具体实现的烂代码而选择用HotSpot VM省点心。

J9 VM

J9是IBM开发的一个高度模块化的JVM。

JVM内存

JAVA虚拟机在执行JAVA程序过程中会把他管理的内存划分为若干个不同的数据区域,这些区域都有各自不同的用途以及创建和销毁时间。

程序计数器

1,它的作用可以看作是当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令。

2,虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。任何时刻一个处理器只会执行一条线程中的指令。

3,如果执行的是Natvie方法,这个计数器值则为空。

JAVA虚拟机栈

与程序计数器一样,虚拟机栈也是私有的,它的生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame) (栈帧是方法运行期的基础数据结构)每一个方法被调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

我们常说的java内存区分为堆内存(Heap)和栈内存(Stack Frame),这样划分比较粗糙,这里所指的栈就是虚拟机栈。

本地方法栈

虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务也有一些虚拟机(如 Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。

Native Method就是一个java调用非java代码的接口,java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,JVM有些部分是用C写的。

我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。

JAVA堆(Heap)

堆是虚拟机所管理的内存中最大的一块,堆是被所有线程共享的一块区域,在虚拟机启动时创建,此内存的唯一目的就是存放实例对象。

堆是垃圾收集器管理的主要区域,因此很多时候也叫做GC堆。由于现在的收集器基本都采用了分代收集算法,所以堆中还可以细分为:新生代和老年代。在细致点分有Eden空间,From Survivor空间,To Survivor空间等。

无论怎么划分,最终存储的都是对象实例,为了更好的GC,更快的分配内存,更方便管理。

根据Java虚拟机规范,堆可以处于物理上不连续的内存空间中。(默认新生代年龄到达15岁进入老年代)

方法区

方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载类的信息,常量,静态常量,即时编译器编译后的代码等数据。

垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区就永久存在了,这个区域的内存回收主要目标是针对常量池的回收和对类型的卸载(虚拟机规范中关于类型卸载的内容就这么简单两句话,大致意思就是:只有当加载该类型的类加载器实例(非类加载器类型) 为unreachable状态时,当前被加载的类型才被卸载。启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载。)

运行时常量池

运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项是常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机中规范定义的内存区域,但是这部分也被频繁的使用,也会导致内存溢出的错误,在1.4之后加入了NIO,引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存。

垃圾收集(Garbage Cllection,GC)

大部分人把这项技术当做Java语言的伴生产物,事实上,GC的历史远远比Java久远。

虚拟机中程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上在类的结构确定下来时就是已知的了,因此这几个区域的内存的分配和回收都具备确定性,在这几个区域不必过多考虑内存回收的问题。在方法结束或是线程结束,自然就跟着回收了。java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,我们只有在方法运行期间时才能知道会创建哪儿些对象,这部分内存分配和回收都是动态的,所以垃圾收集器是常关注这部分内存的。

下面两个算法,是判定对象是否已死,需要被回收:

1,引用计数算法

很多人是这样判断对象是否还存活的:给对象添加一个引用计数器,每当有一个地方引用到它时,计数器就加1,当引用失效时计数器就减1,任何时刻计数器为0的对象就是不可能再被使用的,JAVA语言没用选用引用技术算法来管理内存最主要的原因是它难解决对象间相互循环引用的问题。

例子:objA 和 objB都有字段c,赋值令objA.c=objB,objB.c=objA,这两个对象再无其他引用,由于他们相互引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC回收它们。

在JDK1.2之后java对引用进行了扩充,将引用分为强引用,软引用,弱引用,虚引用,这四种引用强度依次减弱。

强引用:类似 Object obj = new Object(); 这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

软引用用来描述一些还有用但是非必须的对象。

弱引用也是用来描述非必须的对象。

虚引用一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过需引用来取得一个对象的实例。

2,根搜索算法

该算法就是通过一系列的名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,就是GC Roots到这个对象不可达,则证明此对象是不可引用的。它们将会被判定为是可回收的对象。

在Java语言里,可作为GC Roots对象包括下面几种:

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

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

3,方法区中的常量引用的对象

4,本地方法栈中JNI(即一般说的Native方法)的引用对象

垃圾收集算法

1,标记-清除算法

最基础的收集算法,算法分为'标记'和'清除'两步,首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它的主要缺点有:一个是效率问题,标记和清除效率都不高,二是空间问题,标记和清除后系统会产生大量不连续的内存碎片,当程序分配较大的对象时无法找到足够的内存空间。

2,复制算法

为了解决效率问题。出现了复制算法。它将可用内存按容量划分出了大小相等的两块,每次只使用了其中一块。当一块用完了会将还存活的对象复制到另一块上面去,然后在将已使用过的内存空间一次性的清理掉。缺点是内存缩小为原来的一半。实际上如果对象存活率较高的化要执行较多的复制操作,效率将会变低。

现在商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%(IBM研究是89%,看了好多文章也有说80%以上)都是朝生夕死,所以两块内存不需要1:1的来划分将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当会收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间(HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区,默认比例为8:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中)

3,标记-整理算法

根据老年代的特点,有人提出了'标记-整理算法',标记过程和'标记-清除'算法一样,但后续操作不是对回收对象直接清理,而是让存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4,分代收集算法

当前商业虚拟机的垃圾收集都采用'分代收集'算法,这种算法没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据代的特点采用最适当的收集算法。新生代每次收集都发现大批对象已死去,只有少量存活就采用复制算法,而老年代中对象存活率高,没有额外空间对它进行分配担保,就可以采用'标记-清除算法'或'标记-整理算法'来进行回收。

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现,

上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。

1,Serial收集器:这是一个单线程收集器,它在执行垃圾收集时,必须要暂停其他所有的工作线程,直到它收集结束。

2,ParNew收集器:它是Serial的多线程版本。

3,Parallel Scavenge收集器:它是使用复制算法的收集器,又是并行的多线程收集器。

4,Serial Old收集器:它是一个单线程收集器,使用'标记-清理'算法。

5,Parallel Old收集器:使用多线程和'标记-清理'算法。JDK1.6后提供的。

6,CMS收集器:它是一个以获取最短回收停顿时间为目标的收集器,使用'标记-清除'算法,整个过程分为4个步骤:初始标记,并发标记,重新标记,并发清除。

7,G1收集器:基于'标记-清理'算法,G1将整个JAVA堆(新生代,老年代)划分为多个大小固定的独立区域,并且跟踪这些区域里面的堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域。

JVM类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类的生命周期:

1,加载

在加载阶段,虚拟机需要完成以下三件事情:

1) 通过一个类的全限定名来获取定义此类的二进制字节流

2) 将整个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

2,验证

这个阶段的目的时为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

1) 文件格式验证

2) 元数据验证

3) 字节码验证

4) 符号引用验证

3,准备

准备阶段时正式为变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,需要注意的是,这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。数据的初始值一般都是为零,在编译时才会去赋值比如:

public static int value=123;

在准备阶段value是为0,只有编译javac时才会赋值给value=123。

4,解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要保证唯一就可以了。

直接引用:直接引用是可以直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。

5,初始化

类初始化是类加载过程的最后一步,除了在加载阶段用户应用程序可以通过自定义类加载器与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。

类加载器

类加载阶段中的'通过一个类的全限定名来获取描述此类的二进制字节流'这个动作是放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块被称为'类加载器'。

绝大部分java程序都会使用到以下三种系统提供的类加载器:

1) 启动类加载器:这个类加载器负责将存放在JAVA_HOME/lib目录中的或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。

2) 扩展类加载器:它负责加载JAVA_HOME/lib/ext目录中的或者是被java.ext.dirs系统变量所指定的路径中的所有类库。

3) 应用程序类加载器:由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称他为系统类加载器。它负责加载用户类路径(ClassPath)上所定的类库,开发者可以直接使用这个类加载器。也是默认的类加载器。

欢迎加入Java高级架构学习交流群:375989619
本群提供免费的学习指导 架构资料 以及免费的解答
不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导 进群修改群备注:开发年限-地区-经验 方便架构师解答问题
免费领取架构师全套视频!!!!!!!!

猜你喜欢

转载自blog.csdn.net/J_java1/article/details/82899994
今日推荐