JVM运行时内存区
image
一、线程私有数据区
1、程序计数器
在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能相互干扰,否则就会影响到程序的正确执行次序。程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址,字节码的解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。程序计数器是每个线程私有的。
2、虚拟机栈
虚拟机栈也就是我们常说的栈。虚拟机栈是Java方法执行的内存模型。Java栈中存放的是一个个栈帧。并且是线程私有的,生命周期与线程相同,描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的执行就是对应栈帧在虚拟机栈中的入栈、出栈的过程。下图表示了一个Java栈的模型:
image
3、本地方法栈
本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈为Native方法服务。
二、线程共享区域
1、Java堆
Java堆可以说是虚拟机中最大的一块内存了。它是所有线程共享的内存区域,几乎所有的实例对象都是在这块区域中存放。堆可以处理物理上不连续的内存空间,只要逻辑上连续的就可以。当然,随着JIT(just in time,及时编译技术) 编译器的发展,所有对象在"堆"上分配也变得不那么"绝对"了。同时Java堆也是垃圾收集器管理的主要区域。由于现在收集器基本上采用的都是分代收集算法,所有Java堆又可以细分为:"新生代"和"老年代"。再细致分就是把新生代分为:Eden空间、From Survivor空间、To Survivor空间。
2、方法区
方法区在JVM中也是一个非常重要的区域,在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。它与堆一样,是被线程共享的区域,很容易理解,我们在写Java代码时,每个线程都可以访问同一个类的静态变量。在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
垃圾回收
哪些对象需要回收
1、引用计数法:判断对象的引用数量
引用计数法是通过判断对象的引用数量来决定对象是否可以被回收
给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就+1,;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能在被使用。
优点:
判定效率很高
确定:
不会完全准确,因为如果出现两个对象相互引用的问题就不行了,如下图所示:
image
如上图对象A和对象B相互引用,导致他们的引用计数都不为0,那么垃圾收集器就永远不会回收他们。
2、可达性分析算法:判断对象的引用链是否可达
通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
image
上图中,ObjD和ObjE都是不可用的,可以被GC回收掉。
在Java中,可作为 GC Root 的对象包括以下几种:
虚拟机栈(栈帧中的局部变量表)中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中Native引用的对象
垃圾收集算法
1、标记清除算法
标记清除即Mark-Sweep,是一种最简单的收集算法。在经历过对象判活以后,我们把需要回收的对象标记出来,然后在统一时刻回收所有被标记的对象。如图所示:
image
黑色标记的可回收对象在回收后全部变成未使用空间,但是这样回收后有木有发现空间碎片很多,碎片太多就会导致再分配稍微大点的空间时,找不到这样的连续内存,从而导致GC会被频繁调用,所以标记清除是一种基础的垃圾收集算法,其它算法基本都是以它为基础优化产生。
2、复制算法
复制算法的思想就是把内存分为两块,每次只在一边分配内存,当一边的内存用完了,就把所有还存活的对象复制到另一半去,这时候把原来使用过的这一边的所有空间一次性清理掉,所以也就不存在内存碎片的问题了。缺点就是会浪费一半的内存空间。基本思路如图:
image
其实分代GC算法在新生代区域就用了复制算法,并且也没有分成1:1,而是8:1,也就是所谓的Eden区和survivor区,新生代中大多数对象都是“朝生夕死”的,所以在minorGC时,只把存活下来的对象全部复制到survivor区。
3、标记整理算法
上面提到的复制算法也有它的弱点,就是当对象存活率很高的时候,就会存在很多的复制操作,从而影响了效率。所以这种算法运用在老年代的话很明显不合适,于是又有了标记整理算法,这种算法的主要思路就是把活跃对象标记出来,之后再向内存的一侧移动,然后直接清理掉边界以外的内存,具体思路如下:
image
4、分代收集算法
新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。实践中会将新生代分为一块较大的Eden空间和两块较小的Surivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1:1。也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被"浪费"。
image
对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对象也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的,故而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高JVM的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就采用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久带三个模块。如下所示:
image
1、新生代(Young Generation)
新生代的目标是尽可能快速收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代内存按照8:1:1的比例分成一个eden区和两个Survivor(s0,s1)区,大部分对象在Eden区中生成。在进行垃圾回收时,先将eden区存活对象复制到s0区,然后清空eden区,当这个s0也满了时,则将eden区和s0区存对象复制到s1区,然后清空eden和s0。此时s0区是空的,然后交换s0区和s1区的角色(即下次垃圾回收时会扫描Eden区和s1区),即保持s0区为空,如此往返。特别地,当s1区也不足以存放eden区和s0区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫MinorGC,MinorGC发生频率比较高,不一定等到Eden区满了才触发。
2、老年代(Old Generation)
老年代存放的都是一些生命周期长的对象,就像上面的所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多,大概比例是(1:2),当老年代满时会触发Major GC/Full GC,老年代对象存活时间比较长,因此Major GC/Full GC发生的频率比较低。
3、永久代(Permanent Generation)
永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、GCLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。
4、小结
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。垃圾回收有两种类型,Minor GC 和Major GC/Full GC。
Minor GC:对新生代进行回收,不会影响到老年代。因为新生代的Java对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
Major GC/Full GC:对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代要被写满、永久代被写满和System.gc()被显式调用等。
作者:瓶子里的王国
链接:https://www.jianshu.com/p/f7a53d7f3d5e