JVM+GC基础部分

一、JVM的内存结构(体系结构

JVM是Java程序得以运行的平台,也是Java程序可以跨平台的底层支撑,从整体上来看,JVM的主要功能可以分为加载和执行两大块。其中类加载器负责.class文件的寻址与加载,执行引擎负责字节码指令执行及内存的管理等等。下面是JVM一个经典的体系结构图:

1.jpg

1、Class Loader类加载器:将.class文件加载进内存;class文件在文件的开头会有特定的文件标识,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定;

2、Execution Engine 执行引擎:负责解释命令,提交给操作系统执行

3、Native Interface本地方法接口:调用C/C++语言编写的方法;本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序;

JAVA诞生的时候是C/C++的天下,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码;

它的具体做法是:在Native Method Stack本地方方法栈中登记native方法,在Execution Engine执行时加载native libraies(本地方法库);

目前该方法的使用越来越少,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见了;

因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,直接调服务,这里不多作介绍;

4、**Runtime Date Area运行时数据区:核心重点,所谓的JVM调优,也就是调这一块内容:

    • Method Area 方法区:方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法(如构造函数,接口代码也在此定义。

      简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;

      静态变量+常量+类信息+运行时常量池====>存在方法区中

      *实例变量存在====>堆内存中*

    • Program Count Register 程序计数器:

      每个线程都有一个程序计数器,也就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计;

    • Native Method Stack 本地方法栈:它的具体做法是在 Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies;(这个在上面讲解Native Interface时候已经说过)

注意:JVM优化,永远指的是优化共享区域,而且几乎99%的优化都是优化堆,而剩下的1%就是优化方法区!

栈管运行,堆管存储

JVM优化,永远指的是优化共享区域,而且99%的优化都是优化堆,而剩下的1%是优化方法区,栈是不需要优化的

5、Stack 栈是什么?

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期的,线程结束栈内存也就释放,对于栈来说,不存在垃圾回收问题

只要线程一结束,它的栈内存也就Over,生命周期和线程一致,是线程私有的。

基本类型的变量和对象的引用变量都是在函数的栈内存中分配。

5.1、栈存储什么?

栈帧中主要保存3类数据:

本地变量(Local Variables):输入参数和输出参数,以及方法内的变量;

栈操作(Operand Stack):记录出栈、入栈的操作;

栈帧数据(Frame Data):包括类文件、方法等等;

形象比喻:栈就好比是弹夹,每一个方法,就是它的子弹;————后进先出,先进后出

image.png

image.png

5.2、栈运行原理

栈中的数据,都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行时数据的数据集;

当一个方法A被调用时就会产生一个栈帧F1,并被压入到栈中;

方法A又调用了方法B,浴室又产生了栈帧F2,也被压入到栈中;

方法B又调用了方法C,浴室又产生了栈帧F3,也被压入到栈中;

……

执行完毕后,先弹出F3栈帧,在弹出F2栈帧,再弹出F1栈帧……

遵循“先进后出”/“后进先出”原则。

image.png


二、JVM优化是指优化哪儿?

根据上面部分的内容,我们已经知道JVM优化其实就是优化“线程共享的数据区”,即下图中圈出的部分——堆(99%)+方法区(1%)

3.jpg

1、JVM(Java)有3种:

  • Sun公司的HotSpot(默认的,最多在用的)———— 被Oracle收购了

  • BEA公司的JRocket  ———— 也被Oracle收购了,一山不容二虎,没干的过HotSpot

  • IBM公司的J9 VM ————太贵,Oracle买不起了

2、正式开始JVM优化——堆

熟悉三区结构后方可学习——JVM垃圾收集

2.1、是什么?

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了class文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行;

堆内存分为三部分:

  • Young Generation Space 新生区            Young

  • Tenure Generation Space 养老区            Old

  • Permanent Space             永久区            Perm

image.png

?问题:new 一个对象是new在哪儿?

new在堆内存中的新生区的“伊甸园”,在这个区里面,对象基本上是伴随着对象的一生,就这么结束了,朝生夕死(因为GC回收机制);

2.2、新生区(Young)

新生区是类的诞生、成长、消亡的区域(个别会幸存);一个类在这里产生、应用、最后被垃圾回收器收集、结束生命。

新生区又分为两部分:

  • 伊甸园区(Eden Space):

  • 幸存者区(Survivor Space):幸存者区有两个0区(Survivor 0 Space)和1区(Survivor 1 Space)。

当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存者0区;

当幸存者0区也满了的时候,再对该区进行垃圾回收(Minor GC),将剩余对象移动到幸存者1区;

那幸存者1区也满了的时候呢?再移动到养老区;

如果养老区也满了,那么这个时候将产生一次大的垃圾回收Major GC(FullGC),进行养老区的内存清理;

若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常(OutOfMemory Error 内存溢出错误)

如果出现了java.lang.OutOfMemoryError:Java heap space异常,说明Java虚拟机的堆内存不够,原因有二:

  • Java虚拟机的催内存设置不够,根本就不够new 大对象,可以通过参数-Xms、Xmx来调整;默认出厂设置,JVM的最大内存只能用到物理内存的1/4

  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

注意:垃圾回收(GC)有两个:

  • Minor GC:较小收集,一般我们所说的GC都是指这个较小的GC;

  • Full GC:较大收集。

代码创建一个大对象,制造OOM错误;

使用 System.out.println(Runtime.getRuntime().maxMemory()/1024/1024); 查看本机JVM的最大内存;

使用byte[] byteArray = new byte[1024*1024*1024*4];就可以创建一个4G的大对象;

2.3、养老区(Old)

养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃;

2.4、永久区

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class、Interface的元数据,

也就是说,它存储的是运行环境所必须的类信息(如Object类等),被装载进此区域的数据时不会被垃圾回收器回收掉的,只有关闭JVM才会释放此区域所占用的内存。

如果出现java.lang.OutOfMemoryError:PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。

一般出现这种情况,都是程序启动需要加载大量的第三方jar包。

例如,在一个Tomcat下部署了太多的应用,或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。

JDK1.6及之前: 有永久代,常量池1.6在方法区;

JDK1.7:    有永久代,但已经逐步“去永久代”,常量池1.7在堆;

JDK1.8及之后:    无永久代,常量池1.8在元空间

Java7和Java8最大的一个特点(区别):Java7叫永久代,Java8叫元空间

重申一遍:只有熟悉了三区结构后,才可以学习JVM垃圾收集GC!!!


三、做一个小练习——程序内存划分小总结

代码如下:

package com.jiguiquan.www;

/**
 ** 程序内存划分小总结
 * @author jiguiquan
 *
 */
public class MemoryTest {
	public static void main(String[] args) {	//Line1
		int i = 1;	//Line2
		Object obj = new Object();	//Line3
		MemoryTest mem = new MemoryTest();	//Line4
		mem.foo(obj);	//Line5
	}	//Line9
	
	private void foo(Object param) {	//Line6
		String str = param.toString();	//Line7
		System.out.println(str);	
	}	//Line9
}

划分效果图:

image.png

注意:为什么这里会有一个String Pool(字符串池)?

答案:这就跟JDK的版本有关系了,我们知道:JDK6、7、8三个版本都一样,只需要记住 String对象是在字符串常量池中(String Pool)中,而:

  • JDK6中,常量池在方法区;

  • JDK7中,常量池在堆;

  • JDK8中,常量池在元空间;

严格来说,堆(Heap)和方法区(Method Area)是分开的,所以在JDK6中的图示如下:

image.png

程序内存划分小总结:

实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等;

虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要将它和堆区分开;

对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Permanent Gen)”,但严格本质上来说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,JDK1.7的版本中,已经将原本放在永久代的字符串常量池移走(即JDK7中,常量池已经不再方法区了,而是移到了堆中

常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在“类加载器加载后进入方法区的运行时常量池”中存放;image.png


四、正式开始垃圾回收GC

image.png

在JDK1.7中:

  • 堆实际而言只是从“新生区”到“养老区”,而右边是“永久代”;

  • Minor GC(轻量级GC)发生在“伊甸园区”,而Major GC(重量级GC)发生在“养老区”;

image.png

在JDK1.8中:

  • JDK1.8之后将最初的“永久代”取消了,由“元空间”取代;

  • 目的:将HotSpot与JRockit两个虚拟机标准合2为1,诞生了新的Java规范——JDK1.8;

如果遇到这样的问题:Java7和Java8有什么区别?

第一点,最重要的一点:Java7有“永久代”,Java8没有,由永久代改为了“元空间”;


五、堆内存调优简介

-Xms 设置初始分配大小,默认为物理内存的“1/64”
-Xmx
最大分配内存,默认为物理内存的“1/4”
-XX:+PrintGCDetails 输出详细的GC处理日志

首先使用以下代码,打印MAX_MEMORY和TOTAL_MEMORY:

package com.jiguiquan.www;

public class MemoryCheck {
	public static void main(String[] args) {
		long maxMemory = Runtime.getRuntime().maxMemory();  //返回Java虚拟机试图使用的最大内存量, 约等于1/4 = 4000M
		long totalMemory = Runtime.getRuntime().totalMemory();	//返回Java虚拟机中的内存总量,约等于1/64 = 250M
		
		System.out.println("MAX_MEMORY = "+maxMemory+"(字节)、"+(maxMemory/(double)1024/1024)+"MB");
		System.out.println("TOTAL_MEMORY = "+totalMemory+"(字节)、"+(totalMemory/(double)1024/1024)+"MB");
	}
}

我们来运行查看结果:

MAX_MEMORY = 3790077952(字节)、3614.5MB
TOTAL_MEMORY = 257425408(字节)、245.5MB

和我们预期的相差不大;

使用 -Xmx1024m -Xms1024m -XX:PrintGCDetails 对JVM进行重新配置:

修改完设置后再次运行的结果为:(由于没有虚拟机,所以使用的是别人的截图)

image.png

而且可以很容易的计算出, 堆内存大小  = PSYoungGen新生区 + ParOldGen养老区,注意在JDK8中“PSPermGen永久代”会变为“Metaspace元空间”;


六、模拟自动触发垃圾回收

image.png

注意:

  • 只有在养老区才会包OOM,因为上面有一句话:“若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常(OutOfMemory Error 内存溢出错误)”;

  • 所以,只要报OOM错误,正常上一行一定是Full GC;

    image.png


七、常见面试题

1、StackOverFlowError和OutOfMemoryError,谈谈你的理解?

2、一般上面时候回发生GC?如何处理?

答:Java中的GC会有两种回收:年轻代的Minor GC,和老年代的Full GC;

    新对象创建时如果伊甸园空间不足会触发MinorGC,如果此时老年代的内存空间不足,会触发FullGC;如果FullGC之后,还是空间不足,那么就会抛出OutOfMemoryError错误。

3、GC回收策略,谈谈你的理解?

答:年轻代(伊甸园区+2个幸存区),GC回收策略为“复制”;

    老年代的保存空间一般较大,GC的回收策略为“整理-压缩”;

  • 频繁收集    Young区

  • 较少收集    Old区

  • 基本不动    Perm区


八、GC的三大算法(其实是4大)

JVM在进行GC回收时,并非每次都对上面的三个内存区域进行一起回收的,大部分时候回收的都是指“新生代”;

因此,GC按照回收的区域又分为了两种类型,一种是普通GC(Minor GC),一种是全局GC(Major GC或者Full GC);

    普通GC(MinorGC):只针对新生代区域的GC;——较小收集

    全局GC(MajorGC or FullGC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC;——较大收集

1、GC总共有4大算法,但是其中的引用计数法已经被淘汰了

  • 引用计数法:因为解决不了 双端循环引用 的问题,所以已经被淘汰了

  • 复制算法:年轻代中使用的GC是MinorGC,这种GC使用的算法就是复制算法(Copying)

  • 标记清除;

  • 标记整理/压缩

2、复制算法详解(新生代):

复制之后要交换,谁空谁是TO

image.png

Minor GC会把Eden中的所有(剩下的)活的对象都移动到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old Generation中,也即一旦收集后,Eden是就变成空的了(死了的就死了,没死的也得搬家)。

当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另一块Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用 复制算法 将这些仍然还存活的对象复制到另外一块Survivor区域(即TO区),然后清理所使用过的Eden以及Suivivor区域(即FROM区域),并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄 +1,当对象的年龄达到某个值时(默认是15岁,通过 -XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。

两个Survivor区,轮流充当FROM区和TO区。

复制算法的优点是:不会产生内存碎片,缺点是浪费了一点点内存空间!

HotSpot JVM把年轻代分为了三部分:1个Eden和2个Survivor区(分别叫from和to),三者默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Suivivor区,对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当年龄达到一定程度时(默认为15岁),就会被移动到老年代中。

因为年轻代中的对象基本都是朝生夕死(80%以上)所以在年轻代中的垃圾回收算法使用的是复制算法复制算法不会产生内存碎片

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的,紧接着进行GC,Eden中所有存活下来的对象都将会被复制到“To”区:

而在“From”区中,仍然存活的对象,会根据他们的年龄值来决定他们的去向:年龄值达到一定值(年龄阈值,可以通过 -XX:MaxTenuringThreshold来设置)的对象将会被移动到老年代中,没有达到阈值的对象将会被复制到“To”区域。

经过这次GC,Eden区和From区已经被清空。这个时候“From”和“To”区会交换他们的角色,也就是新的“To”就是上次GC的“From”,新的“From”就是上次GC的“To”。

不管怎样,都会保证名为“To”的Survivor区域是空的。

Minor GC会一直重复这样的过程,直到“To”区也被填满,“To”区被填满之后,会将所有对象移动到老年代中。

下图中:黄色的是已经被GC回收(杀死的),红色是存活的,绿色是剩余空间(因为不可能Eden区达到100%才进行GC的,所以肯定回味剩余空间(绿色))

进行了一次GC复制算法后,很显然,Eden区和刚刚的“From”区就空了(绿色),那么自然的,此From区就会变成下一次GC过程中的“To”区;————这就是所谓的“复制之后要交换,谁空谁是To”

image.png

因为Den区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。

一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲区间。接下来,将之前90%的内存全部释放,以此类推!

(Eden:From:To = 8:1:1)

重申复制算法的优缺点:

优点:不会产生内存碎片,完整度极高;

缺点:浪费了那10%的内存空间,因为总要有10%的空区间是作为“To”区的必备条件。

复制算法弥补了标记/清除算法中,内存布局混乱的缺点,不过与此同时,它的缺点也是相当的明显:

1、它浪费了一般的内存,这太要命了(其实是8:1:1中的Survivor);

2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有的对象都复制一遍,并将所有的引用地址重复一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变得不可忽视。

所以从以上的描述中,不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是:我们必须要克服50%内存的浪费(是指Survivor区)。

3、标记清除(Mark-Sweep)和标记整理(Mark-Compact)

老年代 一般是由标记清除或者是标记清除与标记整理的混合实现

image.png

注意:标记 是指对 活着的对象进行标记,回收哪些没被标记的对象。

优点:不需要额外的空间。

缺点:两次扫描,耗时严重;会产生内存碎片,

1、首先,它的缺点就是效率比较低(递归与全堆对象遍历——一次标记+一次清除),而且在进行GC的时候,需要停止应用程序,这会导致用户体验感非常差劲!

2、其次,主要的缺点就是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随机的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

为了解决内存碎片的问题,自然而然地就引出了第3中算法(标记/整理算法)

image.png

标记/整理算法的唯一缺点就是效率也不高不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法

4、3中算法的对比:

  • 内存效率:复制算法 > 标记清除算法 > 标记整理算法(此处的效率只是简单地对比时间复杂度,实际情况不一定如此);

  • 内存整齐度:复制算法 = 标记整理算法 > 标记清除算法;

  • 内存利用率:标记整理算法 = 标记清除算法 > 复制算法;

结合上面3中算法的优缺点,是不是应该出现一个完美的算法(解决办法)?

答:无,没有最好的算法,只有最合适的算法————>分代收集算法!!!

九、思考一下几个问题(百度面试原题)

1、JVM内存模型以及分区,需要详细到每个区放什么?

2、堆连的分区:Eden,Survivor,from,to,老年代,各自的特点?

3、GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点?

4、Minor GC和Full GC分别在什么发生?

jiguiquan@163.com

文章作者信息...

2 Comments

  • 大神写的太好了,受益匪浅,嘤嘤嘤

  • 呀,欢迎大神光临,盆币生辉!

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐