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的配置参数网上很多博客都有,这里列举一些比较常用的,但是不全。
三、对象
对象的创建
在类加载检查通过后,虚拟机将为对象分配内存(上一篇文章中介绍了类的加载)。
在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可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
- java基础知识(三)jvm 内存空间+对象+GC
- JVM 基础知识(GC)
- JVM 基础知识(GC)
- JVM 基础知识(GC)
- JVM 基础知识(GC)
- JVM 基础知识(GC)
- JVM 基础知识(GC)
- JVM 基础知识(GC)
- JAVA基础知识-JVM的GC算法
- JVM基础知识GC
- 也学习Java/JVM/GC (三)
- JVM那些事儿之内存空间管理(三)
- java基础知识(三)对象数组
- java面向对象基础知识(三)
- JVM虚拟机-GC(对象)
- 【Java】深入理解JVM学习笔记(三) —— GC收集器和内存分配
- 深入JVM虚拟机(三) Java GC垃圾收集
- jvm系列(三):java GC算法 垃圾收集器
- java.lang.NoClassDefFoundError: xxx.xxx.xxx
- 《七点三刻》,有价值的新闻早餐。2017年11月6日,星期一
- springboot+maven+tomcat问题
- spring data redis-1
- 排序算法总结
- java基础知识(三)jvm 内存空间+对象+GC
- PyTorch笔记9-Batch Normalization
- CSS选择器
- putty访问mysql
- C# 实现Dijkstra算法
- android 架包依赖过多问题:com.android.dex.DexIndexOverflowException
- elastic Job 引入jar 包冲突解决办法
- Acer 4750G-2432G50Mnkk(540M/1GB独显)
- 在CentOS下使用fdisk添加新硬盘