关于JVM内存模型的理解

一、概念
1、JVM运行时数据区
 

jvm内存模型图
jvm运行时数据区


       整个JVM占用的内存可分为两个大区,分别是线程共享区线程私有区,线程共享区和JVM同生共死,所有线程均可访问此区域;而线程私有区顾名思义每个线程各自占有,与各自线程同生共死。这两个大区内部根据JVM规范定义又分为以下几个区:

方法区(Method Area)
       方法区主要是放一下类似类定义、常量、编译后的代码、静态变量等,在JDK1.7中,HotSpot VM的实现就是将其放在永久代中,这样的好处就是可以直接使用堆中的GC算法来进行管理,但坏处就是经常会出现内存溢出,即PermGen Space异常,所以在JDK1.8中,HotSpot VM取消了永久代,用元空间取而代之,元空间直接使用本地内存,理论上电脑有多少内存它就可以使用多少内存,所以不会再出现PermGen Space异常。

堆(Heap)
      几乎所有对象、数组等都是在此分配内存的,在JVM内存中占的比例也是极大的,也是GC垃圾回收的主要阵地,平时我们说的什么新生代、老年代、永久代也是指的这片区域,至于为什么要进行分代后面会解释。

虚拟机栈(Java Stack)
      当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息(局部变量表、操作数栈、动态链接、返回值),一个方法一个栈帧,方法开始执行前就先创建栈帧入栈,执行完后就出栈。

本地方法栈(Native Method Stack)
      和虚拟机栈类似,不过区别是专门提供给Native方法用的。

程序计数器(Program Counter Register)
       占用很小的一片区域,我们知道JVM执行代码是一行一行执行字节码,所以需要一个计数器来记录当前执行的行数。

2、堆内存

        堆内存是JVM内存中占用较大的一块区域,对象都在此地分配内存。在堆中,又分为新生代及老年代,新生代中又分三个区域,分别是Eden,Survivor To,Survivor From。JVM调优主要是对堆内存进行调优。

3、提出问题

可能看到这里我们都会产生这样一个经典问题

为何堆内存要进行分代?

最简单的回收方式(标记-回收算法)

假设堆内存不进行分代,那么垃圾回收应该如何进行呢?我们可以大胆想象一下,在一大片内存空间中我们分配了若干个对象(假设图中一个黑色方块代表一个字节,分配的对象均占用两个字节) 


 
此时堆内存满了,是时候来一波垃圾回收操作了,通过某种分析算法,我们分析到某几个对象是需要进行回收(下述此类对象称之为回收对象),我们让无用对象就地清除,回收后的结果如下: 


 
我们可以看到,回收后的内存支离破碎的,虽然现在还有八个字节的内存空间,但只要有三个字节或以上的对象需要申请内存,那么这片支离破碎依旧无法为其分配内存,因为没有连续的空间

第一次演化(复制算法
既然我们没法回收出连续的空间,那我们可以从一开始就把内存分两个大区,平时只用其中一个区,如下图 


 
当左边内存区满时,就开始一波回收操作,找到那些无需回收的对象(下述称此类对象为存活对象),将它们工工整整地复制到右边的区域中,接着将左边的区域来次大清理,清理后的结果如下: 
 


这样当需要再分配内存给对象时,就使用右边的区域,而右边的区域此时也有8个字节的连续空间供分配,当右边满了,再如法炮制,将存活对象复制到左边再将右边回收。貌似这样就解决了问题了,但是总感觉有什么地方不对,是的没错,每次只使用一半的内存,未免也太浪费了!

第二次演化(标记-压缩算法
我们依旧不分区,将整个内存用满, 开始回收垃圾时,我们将存活对象全部移动到左边,然后对边界外的内存进行清理,如下图 
 


这算是一种折中的做法,起码比第一次演化中的做法更加充分使用内存,也比最开始的做法的空闲内存更加连续。 
在这里我们可以再往下思考,在第一次演化中,如果每次回收对象特别多,而存活对象特别少,那么只需要通过少数的复制操作和一次清除就可以实现回收,此时效率会特别高。而在第二次演化中,如果每次回收对象较少,而存活对象较多,则可以采取此策略进行回收确保最终剩余的空间是连续的空间。 
        到这里其实并不足够完善,毕竟上述几种演化都有缺点和优点,有没有办法可以取长补短呢? 
在开发中,其实我们可以发现,大多数对象都是在方法体中new出来,new完使用后就不再使用了,此时该对象即可进行回收。所以这一类的对象有个特点就是朝生夕死。假如在方法执行完,该对象的引用还被持有着,证明该对象是比较重要的对象,越到后面要回收则越来越困难。这个情况不就刚好符合上述两种演化的情况,当对象刚出生时,我们可以将其使用演化一的方式进行回收,当使用演化一的方式回收不了的对象,则证明该对象为比较重要的对象,我们就可以采用演化二的方式进行回收。这样我们可以对我们的内存进行分区

第一次分区


 
当new对象时,内存均从上图右上方的区域申请,当右上方的内存区域满时,则进行一次复制算法进行垃圾回收。从上面的思考我们知道,绝大多数新对象都有朝生夕死的特点,所以在这次的垃圾回收中,存活的对象寥寥无几,然后存活的对象全部塞到右下方区域。在下一次垃圾回收到来时,根据上述分析,之前存活的对象绝大多数还会继续存活,我们将经历过一次垃圾回收的对象年龄+1,可见大多数的对象都熬不过两岁,一般在一岁时就被回收了。而当对象经历了多次垃圾回收仍然存活,此时它很难被回收了,我们可以将其移到左边的区域,另外右边上下俩区域都满了时,则通过垃圾回收将存活对象的那一边区域也移动到左边区域中。当左边区域满时,可通过标记-压缩算法进行垃圾回收。在这种分区方式中,左侧区域称之为老年代,而右侧区域则为新生代。新生代使用复制算法进行一次垃圾回收,称之为Minor GC,而复制完后如果老年代区域不够,也会触发老年代使用标记-压缩算法进行垃圾回收,称之为Major GC,一般Major GC会伴随着Minor GC,所以也称为Full GC。 
在上述分区中,新生代仍然只有一半的区域可以用,之前使用一半区域的原因是考虑到有可能所有对象都是存活对象,这样才足够完全复制,但现在有老年代的存在,再考虑到此区域每一次回收时仅有少数对象需要复制,分区方式是否还有优化的空间呢?

第二次分区

 

这个分区是在第一次分区的基础上,将新生代分为三部分,分别是伊甸园、幸存区S0,幸存区S1(8:1:1,S0与S1大小相同。对象的一生如下: 
①所有对象都在伊甸园出生,当伊甸园占满时,开始进行一次Minor GC,此次GC会将已存活的对象复制到S0区中 
②伊甸园区又被占满,此时又进行一次Minor GC,伊甸园存活的对象又复制到S0区。 
③在若干次GC后,幸存区S0也满了,此时Minor GC会对伊甸园和幸存区S0的 
做一次垃圾回收,将两个区存活的对象复制到幸存区S1中,再把伊甸园和S0清空,最后把S1的内存与S0交换,此时S1又腾空了,S0剩下一些老对象。 
④又经历若干次GC,幸存区S0已经放满了经历过N次GC都回收不了的老对象,此时会将老对象复制到老年代中,腾空幸存区。 
⑤并非当幸存区被老对象占满才复制到老年代中,当老对象年龄达到15岁,即经历过15次GC都还活着的,也会复制到老年代中,另外伊甸园中如果诞生了一个比幸存区还大的对象,那么该对象回收不了时,也会直接送入到老年代中。 
⑥又经历过若干次GC后,老年代也满了,那么此时它会进行一次Major GC。

二、垃圾回收涉及的算法
在垃圾回收中,涉及的算法主要有以下五个

引用计数算法
可达性分析
标记-回收算法
标记-压缩算法
复制算法 

前两个算法用于判断对象是否需要回收,其原理简单讲,

        引用计数算法就是计算对象被谁引用,一旦有其它对象引用此对象,引用次数加一,而GC时引用次数大于零的对象则判断为存活对象,但此算法无法解决循环引用问题,如A引用B,B引用A,此时A与B均无法回收,所以现在JVM不采用此算法;

        而可达性算法则从GCRoot出发,若A引用B,B引用C,则通过A可以到达C,此时ABC三个对象均不进行回收。后面的三个算法为回收策略,其思路在第一章有提及,在这里就不加赘述,下面总结一下三个算法优缺点:


三、JVM常用参数
Xss:每个线程的栈大小
Xms:堆空间的初始值
Xmx:堆空间最大值、默认为物理内存的1/4,一般Xms与Xmx最好一样
Xmn:年轻代的大小
XX:NewRatio :新生代和年老代的比例
XX:SurvivorRatio :伊甸园区和幸存区的占用比例
XX:PermSize:设定内存的永久保存区域(1.8已废除)
XX:MetaspaceSize:1.8使用此参数替代上述参数
XX:MaxPermSize:设定最大内存的永久保存区域(1.8已废除)
XX:MaxMetaspaceSize:1.8使用此参数替代上述参数
四、附录-演示代码
 

public class OutOfMemoryErrorTest {
    public static void main(String[] args) throws Throwable {
        Random r = new Random();
        List<TestObject[]> testObjectList = new ArrayList<TestObject[]>();
        while (true) {
            try {
                TestObject[] testObjects = new TestObject[2048];
                // 模拟30%左右的对象为有用对象
                if (r.nextInt(10) > 4) {
                    testObjectList.add(testObjects);
                }
                Thread.sleep(1);
            } catch (Throwable t) {
                throw t;
            }
        }
    }
}

class TestObject {

    private String name;

    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

五、面试题

  1. JVM垃圾回收机制,GC发生在哪部分,有哪几种GC,它们的算法是什么?

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值