我看Java虚拟机(2)---Java虚拟机内存区域详解

虚拟机内存区域的组成

直接上图:
这里写图片描述

  • 程序计数器:对于Java方法,用来选取下一条要执行的字节码;对于本地方法,值为空。线程独有
  • 虚拟机栈:执行Java方法,每一层都是一个栈帧,栈帧包括局部变量表、操作数栈、动态链接和方法出口等信息。线程独有
  • 本地方法栈:执行Native方法,sun HotSpot将其与虚拟机栈合二为一。
  • :存放对象实例。堆分为新生代和老生代,新生代分为Eden区和两个Survivor区,默认Eden和Survivor(一个Survivor)之比为8:1。所有线程共享
  • 方法区:HotShot将其实现为永生代。虚拟机读入(javac编译器生成)class文件,将会存储信息到该部分,则该部分会存储虚拟机加载的类信息,常量,静态变量即时编译器编译后的代码。该部分还有一个重要的组成部分——常量池。所有线程共享
  • 直接内存:严格来说这部分并不属于Java虚拟机的内存区域,不过Java 在JDK1.4中新加入了NIO类,引入了基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后在Java堆中生成一个该内存地址的引用,使用该引用来操作堆外内存中的对象。

内存分配和垃圾收集

  • 程序计数器不存在内存分配的问题。
  • 虚拟机栈和本地方发栈是在运行时,给将要运行的方法分配内存。
  • 方法区在类加载的时候分配内存。
    以下都是主要研究堆内存的分配和垃圾回收

内存分配

堆中区域图:

这里写图片描述
主要有以下几点:
对象优先在Eden区分配:当Eden区内存不足时,将发起一次Minor GC(Garbage Collection),需要将Eden区和Survivor A(笔者自行编的号,另一个则编号Survivor B)区中,仍旧存活的数据全部复制到Survivor B区中;当下一次GC时,则Survivor A和Survivor B互换位置,即Survivor B+Eden —–复制到—–》Survivor A,如此循环,循环,循环。。。直到Survivor 再也不能容纳Eden+Survivor的时候,老生代就不能再闲着了,就会选择一些数据放置到老生代了。那么问题来了,要选择哪些数据去老生代呢?即选择的标准是什么,接着往下看。
老生代:能进入老生代的数据分两种:“先天条件好的”大对象和“后天足够努力的“顽强者。先天条件好的大对象享受特权,可直接分配内存到老生代,主要是考虑到当发生Minor GC(当作城管)时,若管理的区域全是有背景的大对象,清理起来特别不方便,影响装X;那么问题又来了:多大才是大?虚拟机提供了-XX:PretenureSizeThreshold参数(只对Serial和ParNew两个收集器有效,垃圾收集会讲到)来设置。
小对象就没了这种顾虑,让你去Survivor A绝不会去Survivor B,正所谓大浪淘沙,为了能挑选出顽强的对象,有两种方式来判定:

  1. 计数器:给每一个新生代的对象配一个计数器,当发生一次Minor GC,对象移动一次,计数器+1,直到达到设定的值(默认15,可通过–XX:MaxTenuringThreshhold设置),就可以进入老生代。
  2. 动态判定:当新生代相同年龄的对象的大小之和大于Survivor(一个)的一半,则将该年龄的对象和比他们老的对象全部进入老生代。

当新生代对象需要进入老生代时,老生代也不是无限大的,所以保不齐需要复制进老生代的对象,其大小会超出老生代的最大容量,这时候,就会进行分配担保。简单说,虚拟机会根据以往,每次晋升到老生代所需分配内存的平均值,比较老生代剩余空间,如果大于剩余(即剩余空间不足),则进行一次Full GC(清理老生代);如果小于剩余,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,则进行Minor GC,否则Full GC。

垃圾收集

三个知识点:

  1. 判断对象死亡
  2. 垃圾收集算法
  3. 垃圾收集器
    判断对象死亡
    两种算法:
    • 计数器:当对象有一条引用时,其引用计数器加一,当计数器为0时,可判断其死亡。缺陷:当堆中对象互相引用时,即使外部没有了指向该对象的引用,他们计数器也不为0,不能被回收,如图:
      这里写图片描述
    • 根搜索:每个节点都是一个对象,有一个根节点,当有节点到根节点不可达时,即可判断该对象死亡。Java和C#都使用该算法。如图4,5,6都可回收:
      这里写图片描述
      垃圾收集算法
      标记-清理算法:首先,标记需要清除的对象,然后清除被标记的对象。缺点是,碎片化太严重。
      复制算法:新生代使用的算法
      标记-整理:比标记-清理,多出整理这一步。老生代使用的算法
      分代收集算法:复制算法和标记-整理算法的简单相加。
      垃圾收集器
      盗来的图:
      这里写图片描述
      新生代(复制算法):Serial(单线程),ParNew(多线程),Parallel (注重吞吐量)
      老生代(标记-整理):Serial Old,CMS,Parallel Old

对象访问

事实上,堆中对象除了存储对象本身外,还要存储其类型信息,而类型信息存储于方法区,那么该对象就需要存储一个指向方法区的指针。
当使用一个引用reference访问对象时,主流的访问方式有两种:句柄访问方式直接指针访问方式
句柄访问方式:两个指针,一个指向真实的对象,一个指向类型信息(堆中两个指针,一个对象);
如图:这里写图片描述
直接指针访问方式:真实的对象和指向类型信息的指针(堆中一个指针,一个对象)。
如图:这里写图片描述

聪明的你一定会疑问,为什么要有句柄访问方式,多此一举,直接指针就好了,干嘛要多出一个指针来,我选择第二种。事实上,sun Hotspot虚拟机也选用的第二种。可存在必合理,第一种到底是基于什么考虑?
共识:垃圾收集时,堆中的对象移动是非常普遍的行为(前面讲到了)。如果采用句柄式的话,就无需改变reference的值,只需要改变一个指向对象本身的指针即可。直接访问方式的话,当对象移动时,那就需要改变reference的值了。
直接访问方式的优势也就是访问速度更快,节省一次指针定位的时间,由于对象访问在程序中非常频繁,HotSpot虚拟机也是基于这种考虑吧!
疑惑:当对象移动时,我们在使用HotSpot虚拟机下写程序时,并未手动改变过reference的值,reference又是怎么定位到已经移动过的对象的?这次笔者真是不知道了
又是万能的知乎:当发生一次GC时,对象移动之后会自动刷新一次引用reference。似乎想的通,希望大神们不吝赐教。


思考总结:
“对象存放于堆,基本类型存放于栈”,这句话准确吗?如果准确,怎么对应于上面的区域?否则,哪里不准确?
答案是不准确。
不知道大家有没有发现,上面的解释叙述了类变量存放于方法区;实例变量中,全局对象存放于堆中,局部变量基本类型和对象引用存放于虚拟机栈中,对象实例存放于堆。唯独没有说明全局基本类型存放的位置,网上大部分说的对象存放于堆,基本类型存放于栈这种说法,可观众朋友们,你们是学习过Java虚拟机的高级程序员,能这么肤浅吗?就算是栈,就那两栈,谁能收留基本类型?没有一个。
万能的知乎已经告诉了我们答案,是堆!话不多说,
进入副本看答案,我就不搬运了。
下一节,讲解类文件结构,想想以后可以自己可以将calss文件反编译为Java代码,是不是还有点小激动,骚年,我看好你。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章