你的位置:首页 > Java教程

[Java教程]JVM内存结构、垃圾回收那点事


翻看电脑的文件夹,无意看到了9月份在公司做的一次分享,浏览了一下"婆婆特",发现自己在ppt上的写的引导性问题自己也不能确切的回答出来,哎,知识这东西,平时不常用的没些日子就生疏了。于是,本小白决定把他整理下来,不敢班门弄斧,对于入门的同学可以快速了解虚拟机的大概,有错误的地方请批评指正。

 

一、java虚拟机的内存结构

   

方法区:线程共享,存放已被虚拟机加载的常量,静态变量,类信息,即时编译后的代码等数据。
 
前不久有个同事聊天的时候说起他们也做了个关于虚拟机的分享,顺便考考我,他问我Class文件的存放地点,我说不知道,忘了,不好瞎猜,他说在永久代,我当时将信将疑。查了一下书籍和相关资料,很多人把hotSpot虚拟机的方法区叫做永久代。对于其他虚拟机如BEA JRockit,IBM J9根本不存在永久带的概念。所以这样说其实很不精确,恐怕他自己也只是记住了永久代这个词,但并不知道永久代指什么。这给我的启示是不要被别人一知半解,似对似错的知识信以为真。也不要不是特别明白就加个“好像”无意识的忽悠了别人。更不要被忽悠后不去查证,下次谈论起来,人家如果反驳,自己也会犹豫。
 
在后面讲到的java堆,可以看到堆分为新生代,老年代,和永久代(方法区),跟上图有冲突(上图中方法区与永久代是分开的),这是因为java虚拟机把他作为堆的一个逻辑部分,但实际上他叫Non-Heap(非堆),目的是与堆区分开来。再细说就是方法区有一个运行时常量池,专门存放编译器生成的字面量和符号引用。
 
虚拟机栈:虚拟机栈和程序计数器一样是程序私有的,程序计数器下面会讲到。它和线程的生命周期相同。每个方法被执行时都会创建一个新的栈帧,用于存储本地变量,动态链接,方法出口,等信息。每一个方法调用直至执行完成就对应着一个栈帧从入栈到出栈的过程。通常所说的堆栈信息中的栈即是java虚拟机栈。这个内存区会出现OOM异常
 
本地方法栈:本地方法栈与java虚拟机栈相似,但本地方法栈中执行的是本地方法, 啊?啥叫本地方法?就是通过该方法java程序可以调用非java程序写的方法,这也是java可以跨平台的一个重要原因,它屏蔽了一个普通程序员对底层的操作的繁琐,比如java 多线程中存在一个方法可以调整线程优先级,其实在windows平台下,它调用的是操作系统的方法,其他的平台它则调用其它平台相关的方法。具体的怎样写一个本地方法,怎样去编译,网上搜一搜就会有完整的例子,如同“hello world” 一样简单。
 
程序计数器:是一小块内存,当前线程所执行字节码的行号指示器,字节码解释器就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要计数器来完成,这是java运行时区唯一没有定义OutOfMemory方法的区域。
 
堆:堆是java内存中最大的一块区域,是被所有线程所共享的一块区域。几乎所有的创建的对象都分配在堆。java堆分为新生代和老年代,细分为Eden空间,From Survior空间和to Survior空间
 
2)对象是如何进行访问的?
对象访问有两种方式:
方式一:使用句柄的访问方式
 
 
 句柄的访问方式,java堆中会划分一块内存作为句柄池,java虚拟机栈帧中的引用对象存储的是句柄池中的引用地址,句柄池中存放着到实例对象的指针和到对象类型数据的指针分别指向堆中的实例,和方法区中对象类型数据。对象的类型数据指对象实现的接口,继承的父类,对象的类型等
 
方式二:直接指针的方式

 
直接指针的方式:虚拟机栈帧中存储的是对象实例的地址,而到对象类型数据的指针则存放在对象的实例数据中
 
两种对象访问方式的比较:
1使用句柄的方式,不论对象的实例数据还是类型数据都存在堆中的句柄池,当对象实例位置改变(垃圾回收时经常发生)则需要修改指向实例的指针,修改句柄池中的引用,而虚拟机栈中的引用则不用修改
2从图中可以看出,图一使用3个指针,图二使用2个指针,明显少了一次寻址,所以它的明显优势就是访问速度快,减少了一次指针定位的时间开销,程序执行时对象的访问非常频繁,微小的开销将变得非常可观。HotSpot虚拟机采用的是第二种访问方式。
 
二、java中的垃圾回收策略
首先要问几个问题,如何判断一个对象已经死亡?是否对象只要被引用就没有死亡就不会被回收?什么是根搜索算法?
请看下面一段程序:

public class ReferenceCountGc {

    public Object field = null;

    public static void main(String[] args) {

        ReferenceCountGc objA = new ReferenceCountGc();

        ReferenceCountGc objB = new ReferenceCountGc();

        objA.field = objB;

        objB.field = objA; 

        objA = null;

        objB = null;    

        System.gc();

    }

}

当程序执行到System.gc()时,是否会回收对象objA和objB?

引用计数方法:给对象一个计数器,当对象被引用时就+1,释放掉引用-1,当为0时及不会再被引用。但引用计数方法的Bug是无法解决对象循环引用的问题,但并不是此算法没有用武之地,在很多场景下会使用到这个算法。但java的垃圾回收并没有使用。上面的程序如果使用的是引用计数算法则不会被回收,但虚拟机却使用根搜索算法。

根搜索算法即设定一个对象称为GC root ,从这个节点向下进行搜索,搜索所走过的路径称为引用链,当GC root没有任何引用链相连即在图论中不可到达,则证明此对象不可用

由此来看看,对象死后,垃圾回收算法;

标记-清除算法                                  复制算法
         

灰色矩形框为可回收对象,标记-清除算法就是把可回收的对象进行标记,标记到一定次数则清除掉。从图中可以明显看出,该算法的弊端是会产生大量的磁盘碎片,没有一整片连续的空间,当遇到占用连续的内存空间较多的对象时,由于内存放不下该对象,会提前进行垃圾回收,致使虚拟机垃圾回收频繁,影响性能

为了规避标记清除算法的弊端,出现了复制算法,复制算法将内存一份为二,垃圾回收时将使用的内存中的存活对象,拷贝到另一半内存中,然后把左侧内存区域完全清除掉,上图只是演示了复制算法,但并非一分为二,使用和保留的空间是1:1,可以根据实际情况对虚拟机参数进行调整。此算法的弊端是要保留内存空间,会将可用内存变少。

标记整理算法:                              分代收集算法:

     

标记整理算法:绿色和蓝色区域都代表存活对象,当进行垃圾回收时把存活对象依次移到最左边,移动后将其余内存空间清空。

分代收集算法:如图,其实就是没有算法。。。把以上3种算法进行综合运用,前面说过堆是有划分的,简单分为新生代和老年代,分代收集就是根据不同代的特点应用不同的垃圾回收算法。

 

三、java内存分配

java的自动内存管理解决了两个问题,一是给对象分配内存,二是回收分配给对象的内存,前面我们讲了回收分配给对象的内存,下面我们来看看给对象分配内存

java堆分为新生代,老年代(终身代)和永久代。

新生代和老年代的默认比值为1:2即新生代占堆总内存的1/3,可以通过 –Xms(初始堆大小)、-Xmx (最大堆大小)来改变。

新生代又分为Eden区、From survivor区、To survivor区,Eden:From:To 为 8:1:1,可以通过–XX:SurvivorRatio 参数设定。

JVM每次只会使用From区和survivor区中的一块(Form survivor或To survivor),很明显是为了在垃圾回收的时候将存活对象移到另外一个空闲的survivor区(如果空间足够,否则直接进入老年代),因此垃圾回收所使用的算法是复制算法。

新生的对象会被分配到新生代,新生代的特点是朝生夕死,对象存活的时间短,迭代快。发生在新生代的垃圾回收叫minor GC,minor Gc进行的相对频繁,消耗较full GC少,而Full GC是发生在老年代的垃圾回收,采用的是标记-清除算法。老年代进行一次垃圾回收比新生代话费时间长,进行的也没有老年代频繁,同时要尽量减少老年代的垃圾回收,因为回收速度慢且在进行时影响虚拟机性能,使虚拟机响应变慢,最直接的感觉是应用程序的响应速度变慢。

什么情况下,对象会被分配到老年代?

 从上图中明显可以到3点:

大对象:什么是大对象,大对象就是需要连续占用很多内存空间的对象比如很长的字符串和数组。虚拟机通过-XX:PretrnureSizeThreshold设计大过指定大小的对象之间进入老年代,即时没有超过指定大小,在进行minor GC时通常也会因为survivor区不够用而被转移到老年代。

通过设置MaxTenuringThreshold参数: 这个参数是进行年龄设置的,超过这个年龄的会进入老年代。什么是年龄?在新生代进行minor GC的时候,每进行一次,存活下来的对象年龄+1,默认年龄超过15的会进入老年代。15这个数值也可以通过MaxTenuringThreshold参数改变。

Survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,所有大于或者等于该年龄的对象直接进入老年代,这句话比较拗口,但就是这个意思,看不懂的多看几遍就好了。。。

 

前面讲了这么多参数,如何设置虚拟机参数?可以通过IDE进行设置,不论是调整空间大小,还是设置对象的年龄进入老年代,如下图

 

三、我们开发中应注意的问题

从虚拟机上可以看出,主要是避免full GC的次数,减少朝生夕死的大对象,对虚拟机内存进行优化,在日常开发中写程序的主要注意的是

不要使用长字符串 如:String x = new String(“XXXXXXXXXXXX”) StringBuffer stringBuffer = new StringBuffer()StringBuffer对象的append()方法。当然,考虑到线程安全问题,使用StringBuilder.