Java是一种跨平台的语言,当初其设计初衷也是为了解决各个平台编译环境具备差别,对程序移植性问题形成困难这一痛点,因而推出了Java语言。这么多年Java受业界追捧的缘由除了其面向对象的特性之外就是其可移植性强,而可移植性这一特性正式创建在JVM虚拟机这一基础上的,JVM在其内存模型和垃圾回收机制的设计上堪称神做,了解JVM虚拟机是每个Java开发工程师必备的技能。面试
要说清楚内存,首先要提计算机程序是如何运行的。计算机程序指的就是可让计算机运行的一些指令集合,简单地说就是咱们平时写的代码,而真正在计算机中运行的是进程,进程=代码+数据,而要操做数据,则应该先将数据加载进内存中,才能对其进行进一步的操做。而内存就是一系列地址空间,地址空间又分为内核空间和用户空间,内核空间是计算机操做系统运行时所需的空间,如虚拟内存、联网、操做系统调度等所需的空间,而Java进程实际运行时使用的空间是咱们的用户空间。算法
类装载器(ClassLoader):依据特定格式,class文件加载到内存。
执行引擎(Execution Engine):对命令进行解析。
本地库接口(Native Interface):融合不一样开发语言的原生库为Java所用。
内存区域(Runtime Data Area):JVM内存空间结构模型。
数组
从Java的内存区域中能够看到,其分为五个区,分别是 1.程序计数器、2.虚拟机栈、3.本地方法栈、4.堆区、5.方法区(元空间)而在这五个区中,分为线程私有和共享区域。缓存
1.程序计数器(Program Counter Register):当前线程所执行字节码(class文件)行号指示器(逻辑),它改变自身的计数器的值来选取下一条须要执行的字节码指令,为了程序执行不互相冲突,因此每一个线程必须私有程序计数器,保证程序运行不冲突。注:若是是执行Native方法,则计数器值为Undefined。 程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变, 因此此区域不会出现 OutOfMemoryError 的状况。
2.Java虚拟机栈(Stack):Java虚拟机栈即咱们平时所说的Java内存模型里的栈内存,其存放的最小单位为栈帧,Java虚拟机栈中的每一个栈帧主要存储局部变量表、操做数栈、动态连接、返回地址,当方法调用结束时,该栈帧随即被销毁,栈帧内的局部变量也随即被销毁。这里说一下局部变量表和操做栈,局部变量表包含了方法执行过程当中的全部变量,而操做数栈主要实现入栈、出栈、复制、交换、产生消费变量等。该区域会产生两种异常,即 当线程请求的栈大小超过栈的总深度,抛出StackOverflowError异常(例如递归),当栈进行扩展时没法获得足够的内存,则抛出OutOfMemoryError异常。
3.本地方法栈(Native Method Stack):与虚拟机栈类似,主要存非Java语言的方法。一样会抛出StackOverflowError和OutOfMemoryError异常
bash
1.方法区(Method Area):方法区主要存储Class的相关信息,包括Method和field等等,说这个以前首先说元空间(MetaSpace)和永久代(PermGen)的区别,在Java1.7后,将方法区中的字符串常量池移动到Java堆中,而且Java1.7以后将永久代变为元空间,它们两个最大的区别就是元空间使用本地内存而永久代使用JVM内存,这一改变最大的变化就是,不会再看到ParmGen出现内存溢出的异常了,并且字符串常量池存在永久代中,容易出现性能问题和内存溢出,类和方法的信息大小难以肯定,给永久代的大小指定带来困难。
2.Java堆(Heap):该区域是Java内存模型中最大的一块,该区域存储全部对象的实例,即咱们在写代码时new出来的对象,都存在堆区,当堆没法再分配内存时,将会抛出OutOfMemoryError异常。该区域是GC管理的主要区域,所以Java堆又被称为GC堆,因为GC在垃圾回收的时候使用分代收集,因此堆内存也能够被分为新生代和老年代,老年代占堆内存的2/3,新生代占1/3,新生代又能够细分为Eden区、From区、To区,Eden区的Eden即伊甸园的意思,圣经记载,亚当和夏娃在伊甸园偷食禁果,因此伊甸区是人类的起源地,名字也就来源于此,咱们在程序中new出的对象(除大对象,大对象直接进入老年代),都存在于Eden区,当屡次GC后没有被回收,则会进入老年代,这一块在说垃圾回收机制的时候会细说,这里只要知道Java堆大概分为这几个区域便可。
网络
答: 1.-Xss:规定了每一个线程虚拟机栈(堆栈)的大小。
2.-Xms:堆的初始值。
3.-Xmx:堆能达到的最大值。一般将堆的初始值和最大值设为相同值,防止堆扩容时产生内存抖动问题。
多线程
静态存储:编译时肯定每一个数据目标在运行时的存储空间需求。
栈式存储:数据区需求在编译时未知,运行时模块入口前肯定。
堆式存储:编译时或运行时模块入口都没法肯定,动态存储。
堆和栈的联系,引用对象、数组时,栈里定义变量保存堆中对象目标的首地址。
堆和栈的不一样,栈中的变量在方法运行结束后当即被清除(自动释放),而堆中的对象即便失去引用变为不可达对象,也需等待GC才会被清除,即清除时间时不肯定的(须要GC)。
栈的空间较堆空间小,且栈产生的碎片远小于堆。
栈的效率比堆高。
架构
JDK6:当调用intern方法时,若是字符串常量池先前已建立出该字符串对象,则返回池中的该字符串的引用。不然,将此字符串对象添加到字符串常量池中,而且返回该字符串对象的引用。函数
JDK6+:当调用intern方法时,若是字符串常量池先前已建立出该字符串对象,则返回池中的该字符串的引用,不然,若是该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,而且返回该引用;若是堆中不存在,则在池中建立该字符串并返回其引用。性能
注:Java1.8中 已经将字符串常量池已经从方法区移动到堆中。
在垃圾回收机制中,把没有被其它对象引用的对象断定为垃圾,而垃圾回收机制的各类算法也是基于这一标准,主要的中心即放在如何断定一个对象是否被引用和如何被回收。
在引用计数算法中,主要是经过计算一个对象的引用数量来判断对象是否为垃圾,是否应该被回收。其实现方式是对存在于堆中的每个对象都置一个引用数量计数器。当建立一个对象时,将该对象实例分配给一个引用对象,则将该对象的引用数量计数器的值加一,完成引用则减一。所以,当该实例对象的引用计数器值为0时,则能够将该对象视为垃圾,在GC调用时,则将会回收该对象的空间。
引用计数算法的优劣:引用计数算法其优势是执行效率高,程序执行受影响较小,由于其运行时只需将引用数量计数器的值加一或减一,运算量极小,效率极高,能够交织在程序运行中。其缺点也是十分明显的,引用计数算法有一个致命的缺陷,就是它没法处理循环引用的状况,所谓循环引用就是当A引用B,B又引用A,两个对象互相引用,实际上这两个对象是能够被回收的,但因为其引用计数器的值均为1,因此形成了此种算法断定这两个对象为不可回收,致使内存泄漏。因此Java中的GC并不会采用此种算法。
可达性分析算法是经过判断对象的引用链是否可达,来决定对象是否能够被回收,该算法从离散数学中的图论引入,程序之间的引用关系能够看做是一个十分复杂的图,经过一系列的名为GC Root的节点做为起始点,向下搜索,搜索中走过的路径就被称为引用链(Reference Chain),当一个对象从GC Root没有任何的引用链,则证实该对象是不可达的,该对象就会被标记为垃圾。
例如图中Object五、Object六、Object7均为不可达,因此这三个对象将会在下一次GC中被清除。
可做为GC Root的对象: 1.虚拟机栈中引用的对象(栈帧中的局部变量表)
2.方法区中常量引用的对象
3.方法区中类静态属性引用的对象
4.本地方法栈中Native方法中的引用对象
5.活跃线程的引用对象
简单来讲:就是全部被引用的对象(包括静态对象和非静态对象)+线程+Native方法中的对象,均可以做为GC Root的对象。
这里可能有人会蒙,刚才不是谈了垃圾回收算法了吗,怎么又开始说垃圾回收算法了,其实从这里开始,才是真正的垃圾回收算法,上面的两个算法能够算是垃圾回收前的准备工做,即对要回收的对象进行标记判断。这个对象是否应该被回收,是上面那两个算法的工做,而这个对象应该怎么被回收,回收后要对内存作哪些工做,这就是垃圾回收算法所要考虑的事情。
标记-清除算法将算法分为两个步骤,即标记和清除,所谓标记,就是从根节点进行扫描,对存活的对象进行标记。所谓清除,就是对堆内存中从头至尾进行线性遍历,回收未被标记的对象内存,即不可达对象内存,最后将原来作过标记对象的标记清空,为下一次GC作准备。
标记-清除算法的优缺点:标记-清除算法的优点是其效率高,仅需扫描一遍内存便可将全部的垃圾进行回收。可是其缺陷也是十分的明显,在标记-清除算法中,只要某对象被标记为垃圾,则调用GC时就会直接进行回收,这势必会带来一个问题,就是内存的碎片化。所谓内存碎片化,即在GC过程当中,因为垃圾所处的内存空间并不连续,致使回收事后会存在不少的不连续的内存空间。
举个例子,有两个对象A and B,A占用1B内存,B占用1B内存,他们两个所处的位置并不连续,而当它们被同时标记为垃圾并被回收了以后,就会产生两块1B的内存,此时来了一个2B的对象,可是它就没法使用这两块不连续的1B存储空间了。若是此时内存已满,将会抛出OutOfMemoryException,这就是内存碎片所形成的后果。
复制算法,将内存分为对象面和空闲面,对象只存在于对象面上。当复制算法运行时,首先会像标志-清除算法同样,对存在引用的对象作标记,而后将带有标记的对象复制到空闲面上,而且按照内存顺序存储,当所有带标记的对象都被移动到空闲面上后,将对象面的全部对象一并清除,而后将空闲面和对象面进行互相转换,即此时对象面变为空闲面,空闲面变为对象面。因为复制操做也存在效率问题,因此这种算法适用于对象存活率低的场景,由于这样就不会有不少的对象须要复制。实际上这种算法是应用在堆内存中的新生代中的,由于在大量的实践中证实,在新生代区的对象,最后存活下来的比例大概只有10%,因此至关适合这种算法。至于在新生代中这种算法的运行步骤是怎样的,放在下文中说。
因为复制算法对复制后的对象按照内存顺序存储,因此它解决了标记-清除算法中内存碎片化的问题。
标记-整理算法采用和标记-清除算法同样的步骤,从根集合进行扫描,对存活的对象进行标记,但在清除时,这个算法会移动全部存活的对象,且按照内存地址次序依次进行排列,而后将末端内存地址之后的内存所有进行回收。因为此种算法在标记-清除的基础上,加之对对象进行整理,因此其效率更低,但解决了内存碎片化的问题。
该算法因为一次GC会有较高的资源消耗,因此该算法适用于存活率高的场景,例如堆内存中的老年代。
有了上述三种的垃圾回收算法,有些同窗可能心存疑虑,到底JVM中使用的是哪种算法来对垃圾进行回收呢?其实JVM使用了上述几种算法的组合拳,即分代收集算法。从严格意义上来讲,分代收集算法并非一种新的算法,它只是将上述几种算法进行了一个整合。按照对象生命周期的不一样划分区域,采用不一样的垃圾回收算法。
这里先说一下JVM内存模型和对象生命周期之间的关系,在咱们new一个普通对象时,这个对象会在Eden区被建立,假如在一次GC事后,这个对象没有被清除,则称这个对象是幸存者,将其年龄属性加一,然后将会移动到From 区或To区,这两个区域也被统称为Servivor区,(Eden区:From区:To区=8:1:1),当一个对象经历了15次GC后都没有被回收,则会直接被移动到堆区中的老年代,老年代中的对象被认为是回收可能性不大的对象,由于经历了15次GC都没有被回收的对象,经历150次GC被回收的可能性也不大。
因此了解了这个原理以后,再说分代收集算法就将会变得简单。上文提到,复制算法因为其复制对象到空闲区须要消耗资源,因此适合对象存活率不高的场景,而新生代就很好地知足了这个条件,因此新新生代一般使用复制算法进行垃圾回收。在屡次的实践中证实,一批被新建的对象,最终存活率大概在10%左右,因此这一批对象将会被复制到Servivor区,而复制完成后当即回收Eden区。而新生代中的From区和To区又和复制算法中的空闲区和对象区相对应。这就对复制算法的施行制造了很好的环境。
老年代因为其存储的对象具备不易被GC这个特色,因此上文中提到的标记-整理算法将会变得十分合适,标记-整理算法因为须要在清除后对存活的对象进行一次整理以消除内存碎片化,因此若是有大量的内存碎片,将很是不利于这种算法的运行,而老年代则给了适合这种算法的土壤。
在分代收集算法中还有两个重要的概念是不曾提到的Minor GC 和 Full GC,存在于新生代的GC因为其垃圾回收范围较小,被称为MinorGC,而在老年代的GC中一般伴随着全部内存的GC,因此其又被称为Full GC。Full GC效率低,可是不常被触发。
触发Full GC的条件
1.老年代空间不足
2.永久代空间不足(已移除)
3.CMS GC时出现Promotion failed,concurrent mode failure
4.Minor GC晋升到老年代的平均大小大于老年代的剩余空间
5.调用System.gc()
6.使用RMI进行RPC或管理的JDK应用,每小时执行一次Full GC
经常使用的调优参数:
1.-XX:SurvivalRatio:Eden和Servivor的比值,默认8:1
2.-XX:NewRatio:老年代和年轻代的内存大小的比例
3.-XX:MaxTenuriingThreshold:对象从年轻代晋升到老年代通过GC的最大阈值
在说垃圾收集器以前,先得明白两个概念
什么是Stop-the-World?JVM因为要执行GC,而中止应用程序的执行,这就是Stop-the-World,这种现象在任何一种GC算法中都会发生,因此如何让Stop-the-World发生的次数愈来愈少,以优化GC性能,是大多数垃圾收集器优化GC的策略。
这个词相对来讲很好理解,在GC过程当中,会有程序不断地产生垃圾对象,这会形成一边打扫一边扔的效果,因此GC是以快照方式进行垃圾回收的,在程序运行到特定位置时,例如跳转,会生成一个Safe Point,而GC将会根据这个Safe Point中的垃圾进行回收。
Object的finalize()方法做用是否与C++的析构函数做用相同
答:Object的finalize()方法不能保证在调用时当即回收目标对象,而是要等一次GC才能开始回收,所以它是不肯定的。而C++中的析构函数是肯定的。
Java中的强引用、软引用、弱引用、虚引用有什么用。
强引用:指该对象存在至少一个引用对象引用的状况,这时GC毫不会回收该对象,当内存不足时,即便报OutOfMemoryException也不会回收该对象。
软引用:对象处于有用但非必须的状态,只有当内存不足时,GC才会回收该引用的内存。可用来实现内存敏感的高速缓存,由于在内存不足就被回收这一特性,咱们不用太担忧OutOfMenoryException这一异常 用法:
String str = new String("abc");//强引用
SoftReference<String> softRef = new SoftReference<String>(str);//软引用
复制代码
弱引用:非必须的对象,比软引用更弱一些,GC时会被回收。被回收的几率也不大,由于GC线程优先级比较低,适用于偶尔被使用且不影响垃圾收集的对象。
用法:
String str = new String("abc");//强引用
WeakReference<String> weakRef = new WeakReferences<String>(str);//弱引用
复制代码
虚引用:不会决定对象的生命周期,在任什么时候候均可能被垃圾收集器回收,它能够跟踪对象被垃圾收集器回收的活动。必须与ReferenceQueue联用。
用法:
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
PhantomReference ref = new PhantomReference(str,queue);
复制代码
综上,强引用>软引用>弱引用>虚引用
在写这篇以前,我看过一篇文章,名字记不太清了,大体是,《面试官:求求大家了,再问大家Java内存模型不要再和我说堆区和栈区了》,当时我了解的JVM也仅限于此,看完那篇文后就有了去了解JVM的想法,只有本身实际了解过以后,才意识到本身是“学而后知不足,教而后知困”。也许在之后再回过头来看这篇文章,我依然会有这种感受,但我但愿能够有那个时候。
本文图片来自网络,侵删。
欢迎你们访问个人我的博客:Object's Blog