引言:更多相关请看 JVM+GC解析系列
jVM体系结构
JVM位置


方法区:存放加载的Class信息,可能会放运行时常量信息(包括字符串常量和数字常量),还有静态对象和方法。
堆:JVM启动时创建,Java程序最主要的内存工作区域,被所有线程共享,存放几乎所有的Java对象和成员变量。
直接内存:是在堆外,直接向系统申请的空间,读的速度比堆快,读写频繁的操作建议使用直接内存。理论来说大小不受限,但系统空间有限,堆和直接内存的大小总和受限于操作系统能给出的最大内存。
GC垃圾回收系统:可以直接对方法区、堆、直接内存进行内存回收,其中堆是GC的工作重点。java中所有的对象空间释放都是隐式的,GC在后台默默运行,自动查找、表示、清除垃圾对象,完成对堆、方法区、直接内存的自动化管理。
栈:帧,线程私有,线程启动时创建,存放局部变量,方法参数。与方法的调用创建(创建栈)返回(关闭栈)有关。和(数据结构)栈的特性有关,先进后出。
本地方法栈:和栈类似,不同是栈是由java方法调用,而本地方法栈是由本地方法native method(一个java调用非java代码的接口,用native修饰,通常由其它语言编写,如C)调用。
PC计数器(Program Counter):PC寄存器,线程私有,一个java线程在任意时刻都在执行一个方法(当前方法),如果执行不是本地方法,PC寄存器就指向当前执行的指令;是执行本地方法,那PC寄存器的值就是undefined。简单来说就是区分当前方法是否为本地方法或java方法。保存有当前正在执行的JVM指令的地址。
执行引擎:JVM最核心组件之一,负责执行字节码。为提高性能,JVM会使用即时编译JIT技术先将字节码编译成机器码之后再执行。引擎分为Server和Client。
Server:启动名为C2的重量级编译器,编译更彻底,性能更高。目的是在服务器环境中减少提高程序执行速度。
Client:启动名为C1的轻量级编译器,目的是为了在客户端环境中减少启动时间。
注意:在部分JDK1.6版本以及之后的64位版本中,已经不提供Client引擎了,Server成为唯一。重点是堆和GC。
类装载器
ClassLoader:负责加载class文件,class文件在文件开头有特定文件标示,且ClassLoader只负责class文件的加载,是否可以运行,则由 Execution Engine决定。使用echo %JAVA_HOME%、echo %PATH%、echo %CLASSPATH%(classpath在1.8之后不用配置)验证环境配置。

1.虚拟机自带加载器:启动类加载器(Bootstrap)C++、扩展类加载器(Extension)Java 、应用程序类加载器(AppClassLoader)Java(也叫系统类加载器,加载当前应用的 classpath的所有类。
2.用户自定义加载器,Java.lang.ClassLoader的子类,用户可以定制类的加载方式。)
Code案例:sun.misc.Launcher,是一个java虚拟机的入口应用。

堆体系结构
Heap堆:一个JVM实例只存在一个堆内存,被所有线程共享,堆内存大小是可调节。 类加载器读取类文件后,需把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
Young Generation Space新生区:Young/New
Tenure generation space养老区:Old/Tenure
Permanent Space永久区:Perm(Java8后被称为元空间metadata)
Heap堆(Java7之前) :一个JVM实例只存在一个堆内存,堆内存大小可调节。类加载器读取类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。堆内存逻辑上分为三部分:新生+养老+永久(默认经过15次GC才能进入)。

新生区
是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区 (Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区 (Survivor 0 space)和1区(Survivor 1 space)(有的也被称为一区和二区)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。如果1区也满了,再次垃圾回收,满足条件后再移动到养老区。若养老区也满了,那这个时候使用MajorGC(FullGC),进行养老区的内存清理。若养老区执行Full GC后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明 Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存 在被引用)。
Hotspot内存管理



永久区
永久存储区是一个常驻内存区域,存放JDK自身所携带的 Class,Interface 的元数据,它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。如出现java.lang.OutOfMemoryError: PermGen space,说明是Java 虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。如:一个Tomcat部署太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
Jdk1.6及之前:有永久代, 常量池1.6在方法区。
Jdk1.7:有永久代,但已经逐步“去永久代”,常量池1.7在堆。
Jdk1.8及之后:无永久代,常量池1.8在元空间。
实际上,方法区(Method Area)和堆一样,是各个线程共享的内存区域,用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法。 区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)”,严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接 口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、 接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。

GC分代收集算法
GC作用域

回收器是对算法的具体实现,算法是垃圾回收的方法论。
Minor GC(Scavenge):次收集,清理年轻代,Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区(两个区大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)。一般GC时间很短。
Major GC:清理年老代区域。
Full GC:全收集。清理年轻代、年老代区域。一般超过3-5秒时间就太长了。成本较高,会影响性能。性能优化主要是优化Full GC。
引用计数法

复制算法(Copying)
年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)

-XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数。
解释
年轻代GC,主要是复制算法(Copying) HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。


劣势
复制算法它的缺点也是相当明显的。(占用内存、对象存活率高会浪费时间)
1、它浪费了一半的内存,这太要命了。
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
标记清除(Mark-Sweep)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。

原理

劣势
效率低、空间不连续。
1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲。
2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
标记压缩(Mark-Compact)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
原理

劣势
效率低。
标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
标记清除压缩(Mark-Sweep-Compact)

小总结
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程。
难道就没有一种最优算法吗?
回答:无,没有最好的算法,只有最合适的算法。==========>分代收集算法。
年轻代(Young Gen)
年轻代特点是区域相对老年代较小,对像存活率低。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代(Tenure Gen)
老年代的特点是区域较大,对像存活率高。这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。
Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适。
基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。