Java虚拟机在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。
这些区域都有各自的用途,以及建立和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而创建和销毁。
线程私有区域(生命周期与线程相同)面试
a) 虚拟机栈算法
虚拟机栈描述的是Java方法执行的内存模型:每一个方法在执行的同时都会建立一个栈帧(Stack Frame[1])用于存储局部变量表、 操做数栈、 动态连接、 方法出口等信息。 每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。性能优化
虚拟机栈中有一个局部变量表,存放了编译期可知的各类基本数据类型(boolean、 byte、 char、 short、 int、float、 long、 double)、 对象引用(reference类型,它不等同于对象自己,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或其余与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。多线程
b)本地方法栈架构
本地方法栈(Native Method Stack)与虚拟机栈所发挥的做用是很是类似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。并发
c) 程序计数器分布式
因为Java虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)都只会执行一条线程中的指令。 所以,为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。微服务
共享数据区高并发
a)堆源码分析
对于大多数应用来讲,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。 此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存。
根据Java虚拟机规范的规定,Java堆能够处于物理上不连续的内存空间中,只要逻辑上是连续的便可,就像咱们的磁盘空间同样。 在实现时,既能够实现成固定大小的,也能够是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(经过-Xmx和-Xms控制)。
b)方法区
方法区(Method Area)与Java堆同样,是各个线程共享的内存区域,它用于存储已被虚
拟机加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并不是数据进入了方法区就如永久代的名字同样“永久”存在了。 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,通常来讲,这个区域的回收“成绩”比较难以使人满意,尤为是类型的卸载,条件至关苛刻,可是这部分区域的回收确实是必要的。
Java对象建立过程
a) 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已被加载、 解析和初始化过。 若是没有,那必须先执行相应的类加载过程。
b) 为对象分配内存(对象所需内存大小在类加载完成后便彻底肯定),对象所需内存的大小在类加载完成后即可彻底肯定(如何肯定将在2.3.2节中介绍),为对象分配空间的任务等同于把一块肯定大小的内存从Java堆中划分出来。
1. 引用计数法
每一个对象都有一个引用计数的属性,用来保存该对象被引用的次数。当引用次数为0时,就意味着该对象没有被引用了,也就不会在使用这个对象了,能够断定为垃圾对象。可是,这种方式有一个很大的Bug,就是没法解决对象间相互引用或者循环引用的问题:当两个对象相互引用,他们两个和其余任何对象也没有引用关系,它俩的引用次数都不为0,所以不会被回收,但实际上这两个对象已经再也不有用了。
2. 可达性分析(根搜索法)
在主流的商用程序语言(Java、 C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称经过可达性分析(Reachability Analysis)来断定对象是否存活的。这个算法的基本思路就是经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来讲,就是从GC Roots到这个对象不可达)时,则证实此对象是不可用的。 如图3-1所示,对象object 五、 object 六、 object 7虽然互相有关联,可是它们到GC Roots是不可达的,因此它们将会被断定为是可回收的对象。
这里的GC Roots对象包括如下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即通常说的Native方法)引用的对象。
注: 这里涉及Java中到四种引用,再也不细说。
在此我向你们推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
在肯定了哪些垃圾能够被回收后,垃圾收集器要作的事情就是开始进行垃圾回收,可是这里面涉及到一个问题是:如何高效地进行垃圾回收。因为Java虚拟机规范并无对如何实现垃圾收集器作出明确的规定,所以各个厂商的虚拟机能够采用不一样的方式来实现垃圾收集器,因此在此只讨论几种常见的垃圾收集算法的核心思想。
1.Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之因此说它是最基础的是由于它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出全部须要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程以下图所示:
从图中能够很容易看出标记-清除算法实现起来比较容易,可是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会致使后续过程当中须要为大对象分配空间时没法找到足够的空间而提早触发新的一次垃圾收集动做。
2.Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程以下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,可是却对内存空间的使用作出了高昂的代价,由于可以使用的内存缩减到原来的一半。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,若是存活对象不少,那么Copying算法的效率将会大大下降。
3.Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep同样,可是在完成标记以后,它不是直接清理可回收对象,而是将存活对象都向一端移动,而后清理掉端边界之外的内存。具体过程以下图所示:
4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。
目前大部分垃圾收集器对于新生代都采起Copying算法,由于新生代中每次垃圾回收都要回收大部分对象,也就是说须要复制的操做次数较少,可是实际中并非按照1:1的比例来划分新生代的空间的,通常来讲是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另外一块Survivor空间中,而后清理掉Eden和刚才使用过的Survivor空间。
而因为老年代的特色是每次回收都只回收少许对象,通常使用的是Mark-Compact算法。
注意,在堆区以外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部份内容:废弃常量和无用的类。
你们以为文章对你仍是有一点点帮助的,你们能够点击下方二维码进行关注。 《Java烂猪皮》 公众号聊的不只仅是Java技术知识,还有面试等干货,后期还有大量架构干货。你们一块儿关注吧!关注烂猪皮,你会了解的更多..............