Java内存结构、虚拟机垃圾回收和gc算法

http://blog.csdn.net/tjiyu/article/details/53915869

一、Java虚拟机

1. 什么是Java虚拟机

main函数被执行时,java虚拟机就启动了。
你运行几个application就有几个java.exe/javaw.exe。或者更加具体的说,你运行了几个main函数就启动了几个java应用,同时也启动了几个java的虚拟机。
总结:
什么是java虚拟机,什么是java的虚拟机实例?
java的虚拟机相当于我们的一个java类,而java虚拟机实例,相当我们new一个java类,不过java虚拟机不是通过new这个关键字而是通过java.exe或者javaw.exe来启动一个虚拟机实例。

2. 生命周期

java的虚拟机种有两种线程,一种叫叫守护线程,一种叫非守护线程,main函数就是个非守护线程,虚拟机的gc就是一个守护线程。java的虚拟机中,只要有任何非守护线程还没有结束,java虚拟机的实例都不会退出,所以即使main函数这个非守护线程退出,但是由于在main函数中启动的匿名线程也是非守护线程,它还没有结束,所以jvm没办法退出
总结:
java虚拟机的生命周期,当一个java应用main函数启动时虚拟机也同时被启动,而只有当在虚拟机实例中的所有非守护进程都结束时,java虚拟机实例才结束生命。

3.java虚拟机的体系结构

  • 虚拟机结构类似操作系统内存布局,

和操作系统内存的对比

  • 基于操作系统的角度,jvm就是个该死的java.exe/javaw.exe,也就是一个应用,而基于class文件来说,jvm就是个操作系统,而jvm的方法区,也就相当于操作系统的硬盘区,所以你知道我为什么喜欢叫他permanent区吗,因为这个单词是永久的意思,也就是永久区,我们的磁盘就是不断电的永久区
  • 而java栈和操作系统栈是一致的,无论是生长方向还是管理的方式,
  • 堆嘛,虽然概念上一致目标也一致,分配内存的方式也一直(new,或者malloc等等),但是由于他们的管理方式不同,jvm是gc回收,而操作系统是程序员手动释放,所以在算法上有很多的差异,

内存对比

  • 计算机上的pc寄存器是计算机上的硬件,本来就是属于计算机,计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位)
  • 虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址,它甚至可以是操作系统指令的本地地址,当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined,所以你现在应该很明确的知道,虚拟机的pc寄存器是用于存放下一条将要执行的指令的地址(字节码流)。
  • 当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader,
    指向

  • 【可以不背这块儿】方法区中的字节码内存块,除了记录一个class自己的class对象引用和一个加载自己的ClassLoader引用之外,还记录了什么信息呢??我们还是看图,然后我会讲给你听,听过一遍之后一辈子都不会忘记。
    这里写图片描述

虚拟机运行流程【重点】

首先,当一个程序启动之前,它的class会被类装载器装入方法区(不好听,其实这个区我喜欢叫做Permanent区),执行引擎读取方法区的字节码自适应解析,边解析就边运行(其中一种方式),然后pc寄存器指向了main函数所在位置,虚拟机开始为main函数在java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统APIi等等。

二、Java内存

1. JVM内存结构

内存模型

  1. 类装载器:作用是在JVM启动时或某个Class要运行的时候把类装载到JVM中。
  2. 运行时数据区(内存区域):这是JVM在运行时操作的内存区域,


      • Java堆:线程共享,存放Java对象,所有的对象(包括数组,但Class对象除外)数据实际存放地方。堆是程序级别,每一个Java程序共享一个堆(所以存在多线程访问堆内存同步问题)。
      • 分为:Old区+ Young区,Young区又划分为:Eden + Survivor,Survivor又划分为:From + To (From 、To 大小相等,这些大小都能手动设置)

    1. 栈又分为两种,一是Java方法栈,一是本地方法栈(有的JVM这两者是合在一起的,不过这里还是讨论逻辑上)。另外,每个线程都有各自的程序计数器,也是栈格式的。
      • 本地方法栈:线程私有,与Java栈类似,不过用于执行C/C++的native方法。
      • Java栈:线程私有,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值、常量引用等(如果是基本类型,则存的是值)。
      • 程序计数器(PC):线程私有,用于保存当前线程执行的内存地址。每个线程都有各自的程序计数器,也是栈格式的。
    2. 非堆
      主要用来存储加载的类的信息、常量、静态变量等,因为主要是方法,所以也叫方法区;也因为gc基本不涉及这区,也叫永久代。
      是程序共享区域,从这一点可以看出,JVM规范把他描述为堆的逻辑一部分是有一定的道理,虽然它有个非堆(Non Heap)名称。

      • 方法区(永久代):线程共享,里面存储了类结构信息、常量池以及静态变量,即时编译后的代码等,一般不进行GC,因此也被称为永久代。方法区还包含一个运行时常量池。
      • 堆与非堆的区别是,堆是供给程序使用,而非堆是供给JVM使用的。
        运行时内存
  3. 执行引擎:负责执行class文件中包含的字节码指令

  4. 本地方法接口:主要是调用C/C++实现的本地方法
  5. 总结
    1. 栈线程私有,存放局部变量;堆程序共享,存放对象实例数据;非堆主要存放类信息(Class对象)。这是Java内存最主要的三块内存,而直接内存,如果没用到NIO是不会操作到的。
    2. 数据存放地方
      局部变量:栈中,包括基本类型(存放的是值)、对象引用、返回地址;
      类变量:方法区(非堆),类变量算是类信息一部分;
      字符串和基本类型常量:常量池(事实上,常量池已被放到堆中,不过我们姑且将常量池单独在逻辑上拿出来);
      Class对象:方法区(非堆);
      new对象:引用放到栈中,对象数据放在堆中;
      这些存放地方可能因为Java不断发展改变而不一样,但是逻辑上大概是这个意思,还有细节地方可能还有所不同(比如Eden可能还存放其他信息),如果想追究具体是什么情况的话,可以查看最新的jdk说明文档。
      本节主要讲的是JVM运行时数据区,JVM内存各个区域的划分以及作用。

2 堆内存中各代分布

堆内存
Java堆主要分为3代:
- 年轻代:这里是所有新对象产生的地方。年轻代被分为3个部分——Enden区(伊甸园区,新对象的出生地,这部分均为连续内存,分配快速)和两个Survivor区(From和to)。当Eden区被对象填满时,就会对Eden区和Survivor From区执行Minor GC(Young GC),并把所有存活下来的对象转移到Survivor To区,然后把from区变成下次GC的to区。这样在一段时间内,总会有一个空的Survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。
- 年老代:与年轻代相比,年老代里面的对象存活时间较长,大小也较大(较大的对象可能直接进入年老代)。当年老代被空间占满时,会触发Major GC(Full GC),不仅对年老代进行GC,对年轻代和永久代也进行GC,释放掉已经没有被引用的对象。
- 永久代:永久代即上文提及的方法区,由于方法区放的都是静态文件,故GC影响并不是很大。

  • 为什么要这样划分?
    因为性能,每次GC可以按照不同区域GC,加快垃圾回收速度。不过也因此,如果有更好的GC算法,可能划分就不一样了。
  • 新建的对象存放到Eden区,From/To存放经过一次及以上GC的对象,若经过n次(可设置,通常为0,这里的0意思是GC检查时是0移到From/To区,并且该值+1,不是0移到Old区),对象每次GC若能存活(每次GC,Eden区清空),移到From/To区,以后都是从From到To再到From跳来跳去,经过n次,变成“老对象”就会被移到Old区。
  • 堆是共享空间,每次分配空间要加锁。但是有的JVM会为每个线程分配一个TLAB空间,这样就不用加锁(不过这种仅适用于小对象,大的还是直接分配在堆上)。
  • 堆在物理上是可以分散的,只要在逻辑上连续的就可以,大小可以是固定的,也可以是可扩展的,主流的JVM采用的都是可扩展的,可以用-Xmx -Xms控制(详见JVM优化一节)。
  • Old区、Eden区的内存并不一定会被填满,相反,一般情况下,不可能被填满,而是达到一定的值就会启动GC(详见GC一节)。

三、JVM垃圾收集算法(gc算法)

  1. JVM垃圾收集算法有四种:标记-清除算法、复制算法、标记-整理算法、分代收集算法
  2. 触发gc的条件
    (1).Minor GC:当Eden区满了或达到指定值,触发Minor GC,清理Eden区和Survivor区;
    (2).Major GC:当Old区满了或达到指定值,触发Major GC,清理Old区(一般也会触发Minor GC,但不一定,各个GC回收器策略不一样,耗时一般是Minor GC的10倍,因为采用的回收方法不一样)
    (3).Full GC:当前两者触发,但仍内存不足,或者方法区满了,则会触发Full GC,清理内存所有对象空间(包括方法区)。
  3. 这三者并没有那么明确的区分,因为,一般Old区对象都有Young区的引用,所以一般Major GC会引发Minor GC,从而减轻Major GC负担,而Full GC也会触发Major GC,你要关心的不是这三者的区别,而是当什么条件下会触发GC,以及相应的GC算法,以及执行这些算法GC的时候对程序的影响。
    堆内存

1.复制算法:

  • 复制算法将可用的内存分成两样大小的两块,每次只使用其中一块内存。当这块内存用完之后,就把还存活的对象复制到另外一块上面,然后,把这块清空。
  • 即针对Young区,依次扫描这个区的所有可达对象(如何确定可达对象,请参考前一节),扫描只扫描GC维护的一张对象关系有向图(以下称为可达对象链),只要在这个图上的,就将这个对象复制到另一个区域(实现这种算法需要堆内存保留一个与Young区大小一样的区域),原先的Eden区对象,移到From区,From区移到To区,有必要的话,将对象移到Old区(区域划分,见Java内存结构),原先内存全部清空,作为下一次GC用。
  • 【优化】随着时间的积累,现在使用的复制算法的虚拟机,不再是把内存分为1:1的两块。因为98%的对象是寿命很短的,创建之后,很快就被回收了,存活下来的只有2%,所以,用来存储存活对象的内存区,可以小一些。现在的商业虚拟机是把可用内存分为一个较大的Eden空间和两个较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,把Eden和Survivor中的存活对象一次复制到另一块Survivor内存区上,然后把Eden和刚才用过的Survivor空间清空。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,这样,每次新产生的对象可以使用90%的内存空间。
  • 优缺点:只需遍历可达的对象,不用访问不可达对象,遍历少,但需要巨大的复制成本和较多的内存。

2.标记-清除算法:

遍历可达对象链,对这些对象进行标记,下次,遍历整个区域的对象,没有标记的清除。
优缺点:不需要额外空间,但是遍历空间花费大,而且会产生大量内存碎片

3.标记-整理算法

前二者的结合,遍历可达对象链,标记这些对象,再按顺序将这些对象合并到一块内存上,比如,有1、2、3、4、5、6、7、8块连续内存对象,其中2,5,8是可达链上对象,标记整理算法的做法是:先标记他们,再从1开始遍历,1不是,到2,2是,将2复制到1,2标记清除,再遍历3,4,不是,遍历5,是,将5复制到2,依次如此,最后得到1,2,3有用内存,后面内存就被清除了。
优缺点:相对标记清除来说,没有了内存碎片,但是遍历花费仍然很大

4.分代收集算法

分代收集算法是根据对象的存活周期的不同,将内存划分为几块。当前的商业虚拟机的垃圾收集都采用了该算法。一般把Java堆分成新生代(年轻代)和老年代(年老代)。这样就可以根据各年代中对象的存活周期来选择最合适的收集算法了。新生代,由于只有少量的对象能存活下来,所以选用“复制算法”,只需要付出少量存活对象的复制成本。老年代,由于对象的存活率高,没有额外的空间分担,就必须使用“标记-清除”或“标记-整理”算法。

5.使用范围

实际上,GC根据堆内存空间不同区域,采用不同的算法回收:
Young区:存活的对象较少,复制代价小,但次数多,采用复制收集算法;
Old区和方法区:对象存活较多,次数少,较慢,采用标记清除或标记整理算法。

http://blog.csdn.net/a327369238/article/details/52120272
http://blog.csdn.net/zhuangyalei/article/details/51585852
http://blog.csdn.net/a327369238/article/details/52132579

四、GC回收器种类

GC回收器的衡量指标:

1.Throughput(吞吐量):所有没有花在执行GC上的时间占总运行时间的比重。
2.Pauses(暂停):当GC在运行时程序的暂停次数。或者是在感兴趣的暂停次数中,暂停的平均时长和最大时长。
3.Footprint(足迹?):当前使用的堆内存大小(算法所有花费的额外空间)。
4.Promptness(及时性):不再使用的对象多久能被清除掉并释放其内存。

1. 串行垃圾回收器

Serial收集器/Serial Old收集器,是单线程的,使用“复制”算法。当它工作时,必须暂停其它所有工作线程。特点:简单而高效。对于运行在Client模式下的虚拟机来说是一个很好的选择。
1.Serial:只对新生代使用;2.Serial Old:只对老年代使用,采用的算法不一样(一般作为CMS的替补)

2. 并行垃圾回收器

ParNew收集器
ParNew收集器,是Serial收集器的多线程版。是运行在Server模式下的虚拟机中首选的新生代收集器。除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器/Parallel Old收集器
Parallel Scavenge收集器,也是使用“复制”算法的、并行的多线程收集器。这些都和ParNew收集器一样。但它关注的是吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值),而其它收集器(Serial/Serial Old、ParNew、CMS)关注的是垃圾收集时用户线程的停顿时间。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。

3. 并发标记扫描垃圾回收器(CMS)

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法。
  CMS收集器分4个步骤进行垃圾收集工作:
  1、初始标记   2、并发标记   3、重新标记   4、并发清除
  其中“初始标记”、“重新标记”是需要暂停其它所有工作线程的。

多线程,标记清理(Full GC的时候用)通过JVM命令 -XX:+UseConcMarkSweepGC使用, 主要用于老生代,策略为:
年老代只有两次短暂停,其他时间应用程序与收集线程并发的清除。采用两次短暂停来替代标记整理算法的长暂停,它的收集周期:
初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 重新标记(CMS-remark)-> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)。
它的主要适合场景是对响应时间的重要性需求大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。但CMS收集算法在最为耗时的内存区域遍历时采用多线程并发操作,对于服务器CPU资源不够的情况下,其实对性能是没有提升的,反而会导致系统吞吐量的下降;

4. G1垃圾回收器

G1(Garbage First)收集器,基于“标记-整理”算法,可以非常精确地控制停顿。
适用于堆内存很大的情况,它将对内存分割成不同的区域,并且并发的对其进行回收,回收后对剩余内存压缩,标记整理,服务器端适用。

五、方法区补充【超纲】

这里写图片描述
1. 类信息:修饰符(public final)
是类还是接口(class,interface)
类的全限定名(Test/ClassStruct.class)
直接父类的全限定名(java/lang/Object.class)
直接父接口的权限定名数组(java/io/Serializable)
也就是 public final class ClassStruct extends Object implements Serializable这段描述的信息提取
2. 字段信息:修饰符(pirvate)
字段类型(java/lang/String.class)
字段名(name)
也就是类似private String name;这段描述信息的提取
3. 方法信息:修饰符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
参数需要用到的局部变量的大小还有操作数栈大小(操作数栈我们后面会讲)
方法体的字节码(就是花括号里的内容)
异常表(throws Exception)
也就是对方法public static final String getStatic_str ()throws Exception的字节码的提取
4. 常量池:
- 4.1.直接常量:
1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;
1.2CONSTANT_String_info字符串直接常量池 public final String CONST_STR=”CONST_STR”;
1.3CONSTANT_DOUBLE_INFO浮点型直接常量池
等等各种基本数据类型基础常量池(待会我们会反编译一个类,来查看它的常量池等。)
- 4.2.方法名、方法描述符、类名、字段名,字段描述符的符号引用
也就是所以编译器能够被确定,能够被快速查找的内容都存放在这里,它像数组一样通过索引访问,就是专门用来做查找的。
编译时就能确定数值的常量类型都会复制它的所有常量到自己的常量池中,或者嵌入到它的字节码流中。作为常量池或者字节码流的一部分,编译时常量保存在方法区中,就和一般的类变量一样。但是当一般的类变量作为他们的类型的一部分数据而保存的时候,编译时常量作为使用它们的类型的一部分而保存
5. 类变量:
就是静态字段( public static String static_str=”static_str”;)
虚拟机在使用某个类之前,必须在方法区为这些类变量分配空间。
6. 一个到classLoader的引用,通过this.getClass().getClassLoader()来取得为什么要先经过class呢?思考一下,然后看第七点的解释,再回来思考
7. 一个到class对象的引用,这个对象存储了所有这个字节码内存块的相关信息。所以你能够看到的区域,比如:类信息,你可以通过this.getClass().getName()取得
所有的方法信息,可以通过this.getClass().getDeclaredMethods(),字段信息可以通过this.getClass().getDeclaredFields(),等等,所以在字节码中你想得到的,调用的,通过class这个引用基本都能够帮你完成。因为他就是字节码在内存块在堆中的一个对象
8. 方法表,如果学习c++的人应该都知道c++的对象内存模型有一个叫虚表的东西,java本来的名字就叫c++- -,它的方法表其实说白了就是c++的虚表,它的内容就是这个类的所有实例可能被调用的所有实例方法的直接引用。也是为了动态绑定的快速定位而做的一个类似缓存的查找表,它以数组的形式存在于内存中。不过这个表不是必须存在的,取决于虚拟机的设计者,以及运行虚拟机的机器是否有足够的内存

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值