java基础知识(三)jvm 内存空间+对象+GC

来源:互联网 发布:郑州网络诈骗被一窝端 编辑:程序博客网 时间:2024/05/12 11:32

一、jvm基础结构

jvm中主要把内存分成下面几个部分:

  • 程序计数器(PC寄存器)
  • 方法区
  • 栈空间
  • 堆空间
  • 本地方法栈

下面这张图是从《深入java虚拟机》这本书里截取出来的:

这里写图片描述

程序计数器(PC寄存器):

所占据的内存空间很小,可以看作是当前线程所执行的字节码的行号指示器,简单来说就是执行完当前语句之后,根据程序计数器找到所要执行的下一条语句。这方面的知识在操作系统中应该都有涉及到。

从图中也可以看出,程序计数器线程独立的,每个线程都有一个计数器。

方法区:

线程共享的内存区域,主要存储的是已经被虚拟机加载的类的信息,常量,静态变量,字节码等信息。

在目前的java7,java8中,虚拟机采用的都是HotSpot,方法区和永久区(待会说)管理的是同一块内存区域。

顺便提一下,在java7以后,字符串常量已经从方法区移到堆空间中,即是String str = new String("hello world");hello world 这个常量在java6(包括)之前是放在方法区的,java7将这个常量移动到堆空间中。关于String的常量这方面的知识点也是不少的,这里不做展开。

栈空间

这里的栈空间指的主要是虚拟机栈,这个栈空间是线程私有的,一般来说,栈空间一般只有几十k到几百k,空间较小。String str = new String("hello world");上面这个语句,str这个变量就是放在栈空间的。

每个方法在执行的同时都会创建一个栈帧用于存储局部变量表(这个东西好像挺重要的,但是不是很看得懂),操作数栈,动态链接,方法出口等信息,每一个方法从调用知道执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出StackOverflowError异常(递归中最常见的一个错误)
如果栈空间申请不到自己所需要的内存,就是抛出OutofMemoryError异常。

本地方法栈:

这个和上面的栈空间基本是相同的,不同的是这里面存放的是native方法的栈空间,由于native方法基本上都不是java写的,所以很多程序员对这部分的内存不是很关心。

堆空间:

堆空间是java虚拟机中最大的一块内存空间,所有线程共享堆空间的内存。String str = new String("hello world");这个语句中new出来的对象就是存放在堆空间的。这部分空间也是GC算法主要工作的地方。在不同的GC算法下,对这块内存的管理也是不同的。关于GC算法待会再说。

二、JVM配置参数

jvm的配置参数网上很多博客都有,这里列举一些比较常用的,但是不全。

参数 含义 -verbose:gc 打印gc -XX:+PrintGCDetails 打印GC详细信息(最后打印) -XX:+PrintGCTimeStamps 打印GC发生的时间戳 -Xloggc:log/gc.log 指定GC log的输出位置 -XX:+PrintHeapAtGC 每次GC之后都打印堆信息 -XX:+TraceClassLoading 监听类的加载 -XX:+PrintClassHistogram 按下Ctrl+Break之后打印类的信息(序号、示例数量、总大小、类型) -Xmx20m 最大堆20M -Xms20m 最小堆20M -Xmn15m 新生代(eden+2*s)的大小 -XX:NewRatio 新生代(eden+2*s)和老年代的比 -XX:SurvivorRatio eden:s -XX:+HeapDumpOnOutOfMemoryError 内存不足是导出堆到文件 -XX:+HeapDumpPath 上一个导出的文件位置 -XX:OnOutOfMemoryError OOM时执行指定目录下的脚本 -XX:PermSize 永久区的初始空间 -XX:MaxPermSize 永久区的最大空间 -Xss 栈空间分配(几百K就够了)

三、对象

对象的创建

在类加载检查通过后,虚拟机将为对象分配内存(上一篇文章中介绍了类的加载)。
在HotSpot(java虚拟机中的一种,最常用的一种)中,内存的分配方式有两种

  • 指针碰撞
  • 空闲列表

指针碰撞:如果java堆中的内存绝对规整,即是已分配的在一边,为分配的在另一边,中间用一个指针将二者区分开,这样下次分配的时候只需将指针移动一下即可;
空闲列表:如果java堆中的内存空间不是规整的,即是已分配的和未分配的内存空间是相互交织的,则虚拟机维护一个列表,列表上分别记录着那一块区域被使用,那一块未被使用,下一次分配的时候,从未分配中找到合适的大小给对象即可。这样的话就涉及到另外的一些问题,比如内存紧缩和空间分配问题,感觉上和操作系统中的内存管理可以采取同一策略。

空间的分配在并发下往往不是线程安全的,解决的策略有两种,一是对指正或者对表进行同步处理,加锁或者其他的一些策略,再则就是可以每个线程在内存分配的手,提前分配本地线程缓存(TLAB),线程仅可在分配的空间内分配空间,待分配的空间分配完之后再去申请TLAB。

对象的内存布局

HotSpot中,对象分为三个部分

  • 对象头
  • 实例数据
  • 对齐填充

1)对象头
分为两个部分,第一部分用来存储对象自身的运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向线程时间戳等信息,另一部分是类型指针,及对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果是数组,则对象头中还有一块用于记录数组长度的数据。

2)实例数据
运行时的数据,没什么好说的

3)对齐填充
无意义,仅仅是起到占位符的作用,因为在HotSpot中java对象的大小都是8字节的倍数,对象头是8的倍数,但是实例数据不一定,对齐填充用来补全实例数据中空缺的位数。

对象的访问定位

目前主流的访问方式有使用句柄和直接指针两种。
(1)如果使用句柄访问的话,那么java堆中将会划分出一块内存来作为句柄池,referece中存储的就是对象的句柄地址,而句柄中包含了独享实例数据与类型数据各自的具体地址信息。

好处:reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中实例对象数据指针,而reference本身不需要修改。

(2)如果是使用直接指正访问,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference终存储的直接就是对象的地址。

好处:速度快,它节省了依次指针地位的时间开销。

四、对象的存亡

判断对象存亡的算法

1)引用基计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能在被使用的,这个时候就可以进行回收。

采用引用计数算法的语言:Python,Squirrel,FlashPlayer

缺陷:无法解决循环引用,加入A引用B,B引用A,则两者的计数器的值永远不可能为0,此时这两个对象永远都不可能被回收。

2)可达性分析算法
通过一系列的GC Roots的对象作为起始点,从这些节点开始往下搜索,搜索所走过的路成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象不可用。

这里写图片描述

可作为GC Roots的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法中JNI(Native)引用的对象

采用可达性分析算法的语言:java C#

对象的自救

要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

测试一下

package jvm;/** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 */public class 对象的自救 {    private static 对象的自救 SAVE_HOOK = null;    @Override    protected void finalize() throws Throwable {        super.finalize();        System.out.println("finalize mehtod executed!");        对象的自救.SAVE_HOOK = this;    }    public static void main(String[] args) throws Throwable {        SAVE_HOOK = new 对象的自救();        //对象第一次成功拯救自己        SAVE_HOOK = null;        System.gc();        //因为finalize方法优先级很低,所以暂停0.5秒以等待它        Thread.sleep(500);        if (SAVE_HOOK != null) {            System.out.println("yes,i am still alive:)");        } else {            System.out.println("no,i am dead:(");        }        //下面这段代码与上面的完全相同,但是这次自救却失败了        SAVE_HOOK = null;        System.gc();        //因为finalize方法优先级很低,所以暂停0.5秒以等待它        Thread.sleep(500);        if (SAVE_HOOK != null) {            System.out.println("yes,i am still alive:)");        } else {            System.out.println("no,i am dead:(");        }    }    /**     * 输出:     * finalize mehtod executed!     * yes,i am still alive:)     * no,i am dead:(     */}

五、GC算法思想

标记-清除算法

标记-清除算法是最基础的算法,很多算法都是由此改进的。算法分成两个部分,标记和清除过程,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记的方法就是根据上文中判断对象是否存活。

这里写图片描述

不足:
效率:标记和清除过程效率都不高
空间:标记清除之后的空间不连续,不利于下次的空间分配。

标记-整理算法

过程与标记-清除算法是一样的,但是在清除之前,让所有存活的对象都向一端移动,然后清除端边界以外的空间。这个算法主要用于老年代。

复制算法

将可用内存空间按容量划分为两个大小一样的两块空间,每次只使用其中一块,当这一块内存使用完之后,将这一块内存中的存活对象移动到另一块内存中,然后清除已使用过的内存空间。

这里写图片描述

优点:实现较简单,运行效率高
缺点:浪费了一半的内存

分代收集算法

将对象进行分代管理,不同代使用不同的回收算法。

在HotSpot中将内存划分为新生代和老年代(分配担保Handle Promotion如果另一块survivor空间没有足够的空间存档上一次新生代手收集下来的存活对象时,这些对象就进入老年代),新生代又分为Eden区,from survivor区和to survivor区,一般两个survivor区的空间都不会大,在垃圾回收的时候,新生代采用的是复制算法,每次只使用from和to中的一个,加上这两个空间不大,所以空间的浪费不会很严重。在进行垃圾回收的时候,将Eden区和from区中的老年对象(每次垃圾回收都会对对象代数+1,多次垃圾回收之后仍存在的对象就是老年对象)和较大的对象(另一个survivor区无法存放的对象)移动到老年代中,然后将Eden和from中存活的对象移动到to区,清除Eden区和from区的空间。在老年代中采用的是标记-清除算法或者标志-整理算法。

这里写图片描述

在GC算法中有一个叫做Stop The World 的概念,就是在进行GC的时候,必须停止除GC以外的全部进程避免其产生新的垃圾,但是Stop The World的时候可能会停止很长时间,为了避免这个问题,jvm采用了准确式GC,在HotSpot中使用称之为OopMap的数据结构来加快速度(与之相关的还有安全点和安全区域的概念,不是很懂。。。)

六、 垃圾收集器

垃圾收集器就是对内存回收的具体实现,主要有下面七种收集器:

这里写图片描述

关于这七种收集器的比较这里就不做记录了,感觉属于了解知识。java7和java8中用到的都是G1收集器

垃圾收集器参数

这里写图片描述

七、Minor GC和Full GC(Major GC)

http://blog.csdn.net/u010796790/article/details/52213708

  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

  • Minor GC触发机制:
    当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC

  • Full GC触发机制:
    (1)调用System.gc时,系统建议执行Full GC,但是不必然执行
    (2)老年代空间不足,同时回收年轻代、年老代
    (3)方法去空间不足,会导致Class、Method元信息的卸载
    (4)通过Minor GC后进入老年代的平均大小(概率计算得出)大于老年代的可用内存
    (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
原创粉丝点击