大家好,欢迎来到IT知识分享网。
自动内存管理机制
Java内存区域与内存溢出异常
运行时数据区域
程序计数器
线程私有空间,用于存放下一条指令所在单元地址的地方。每执行一条指令,程序计数器就会加1。
Java 虚拟机栈
线程私有空间,由许多栈帧组成,方法调用时会将当前方法对应栈栈帧压入虚拟机栈,每个栈帧包括局部变量表、操作数栈、动态链接、以及方法返回地址。
本地方法栈
线程私有空间,当线程调用本地方法时,会在本地方法栈中压入当前本地方法的栈帧。
Java 堆
所有线程共享的区域,用于存放大部分的对象实例及数组。
方法区
所有线程共享的区域,用于存放类信息、常量、静态变量、JIT 即时编译器编译后的机器代码等数据。JDK 1.8之前是永久代(持久代),JDK 1.8之后使用元空间替代永久代。
运行时常量池
运行时常量池是方法区的一部分,用于存放Class文件的常量池表。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,但是这部分内容也会被频繁使用,因为 Java 在1.4之后加入了 NIO,允许 Java 程序直接分配堆外内存,当使用的堆外内存突破物理机的总内存大小时,也会发生 OutOfMemoryError 异常。
HotSpot 虚拟机对象探秘
对象的创建
- 检查创建指令在常量池中是否有类的信息,并且检查类是否已经加载、解析、初始化,如果没有则执行类加载过程;
- 在 Java 堆中为对象分配内存空间;
- 分配方式:指针碰撞或空闲列表,跟垃圾收集器相关
- 线程安全:CAS 重试或本地线程分配缓冲(Thread Local Allocate Buffer,TLAB)
- 初始化内存空间,将其初始化为零值;
- 设置对象的对象头;
- 调用类的构造函数。
对象的内存布局
- 对象头,由以下三部分组成:
- 运行时数据:哈希码、GC 年龄分代、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等;
- 类型指针,即对象指向类型元数据的指针;
- 数组长度,当对象是 Java 数组时会存储。
- 实例数据,在 Java 程序代码中所定义的各种类型字段内容,包含父类中的内容;
- 对齐填充,填充为8的倍数。
对象的访问和定位
- 通过句柄访问,Java 虚拟机栈的局部变量表中会保存一个 reference,指向 JVM 堆中的句柄池,句柄池中的句柄保存了对象实例数据的指针和对象类型数据的指针。优点:当对象频繁移动时,只需要修改句柄中的实例数据指针,不需要修改本地变量表中的 reference。
- 通过直接指针访问,Java 虚拟机栈的局部变量表中会保存一个 reference,直接指向对象的实例数据,对象的实例数据中会保存对象类型数据的指针。优点:访问对象速度快,只需要一次指针定位,HotSpot 虚拟机就是使用直接指针。
实战:OutOfMemoryError异常
Java堆溢出
- “Java.lang.OutOfMemoryError:Java heap space”,堆中大量的对象未回收导致的。
虚拟机栈溢出和本地方法栈溢出
- “Java.lang.StackOverflowError”,单线程情况下,新的栈帧内存无法分配,会抛出这个异常:
- 栈帧太大,单个栈所需的内存超过-Xss设置的大小,Linux 228K;
- 虚拟机栈容量太小时,一般是递归导致超过栈的深度导致的。
- “Java.lang.OutOfMemoryError”: unable to create native thread, possibly out of memory or process/resource limits reached
- 多线程情况下,由于达到了操作系统内存限制,无法再创建新线程时会抛出这异常。
方法区和运行时常量池溢出
- 使用 String.intern()创建大量字符串常量触发,在 JDK 1.6中,持久代内存溢出:”Java.lang.OutOfMemoryError:PermGen space”,在 JDK 1.7及以后的版本中,”Java.lang.OutOfMemoryError:Java heap space”,堆内存溢出
- 使用 CGLib 字节码增强技术来运行时生成大量的动态类,在 JDK 1.7 中触发”Java.lang.OutOfMemoryError:PermGen space”异常
本机直接内存溢出
- 在 JDK 1.4版本中,加入了 NIO,允许 Java程序直接申请操作系统内存,当大量使用 NIO 的 DirectByteBuffer 申请内存时,会触发本机直接内存溢出,通过Unsafe.allcateMemory()来触发”Java.lang.OutOfMemoryError”。如果内存溢出后产生的Dump 文件很小,操作系统内存占用又很大,程序中又直接或间接的使用了NIO,可以考虑重点检查一下直接内存方面的原因。
垃圾收集器与内存分配策略
对象已死吗
引用计数算法
缺点:当对象间存在循环引用时,无法正确回收对象
可达性分析算法
通过一系列成为”GC ROOT”的根对象作为起始节点,根据引用关系向下搜索,当 GC ROOTS 到某个对象不可达时,证明这个对象是不可能被使用的。
可以作为 GC ROOT 的对象:
- 虚拟机栈中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI 引用的对象;
- Java 虚拟机内部的引用;
- 所有被同步锁(synchronized)持有的对象;
- 反映 Java 虚拟机内部情况的一些对象(JMXBean、JVMTI中注册的回调、本地代码缓存)。
再谈引用
- 强引用(Strongly Reference),典型强引用的是 Object obj = new Object();只要强引用关系存在,对象永远不会被回收。
- 软引用(Soft Reference),用来描述一些还有用,但是非必须的对象,在系统将要发生内存溢出先,将会把只被软引用的对象列入回收范围进行第二次回收,如果这次回收之后仍没有足够的内存,才会抛出内存溢出异常。
- 弱引用(Weak Reference),用来描述那些非必须的对象,只被弱引用的对象在下一次垃圾回收中一定会被回收。
- 虚引用(Phantom Reference),最弱的一种引用关系,不会影响对象的生存时间,设置虚引用的目的只是为了在对象被回收时收到通知。
使用非强引用在很多情况下可以避免出现 OutOfMemoryError,但是过量使用也会对 GC 造成严重的影响,反而会降低系统性能。
在实际开发过程中,需要及时处理非强引用对象。
详情可参考:https://www.jianshu.com/p/13cfd631ad5e
生存还是死亡
对象在标记为不可达之后,并不会立即回收,虚拟机会判断是否有必要执行对象的 finalize() 方法,当对象重写了 finalize() 方法并且该方法未被执行过时,虚拟机会将对象放入 F-Queue 队列中,由另一个低优先级的 Finalizer 线程去执行队列中对象的 finalize() 方法,稍后收集器将会对 F-Queue中的对象进行二次标记,如果对象到 GC ROOT仍然不可达,将会被回收。
但是虚拟机不一定会等到 finalize() 方法结束后再回收它,是为了避免在 finalize() 方法中卡死了影响队列中其他的对象回收。
因为 finalize()方法运行成本过高,且不易管控,在 Java 程序中,不推荐重写 finalize() 方法,需要使用的话,可以用 try-catch 代替。
回收方法区
主要是回收方法区中的废弃的常量和不再使用的类型,
-
废弃的常量,当常量池中的某个常量不再被任何 Java 对象引用,且虚拟机中也没有其他地方引用这个常量,当发生垃圾回收时,且收集器判断确有必要,将会把这个常量清理出常量池。
-
不再使用的类型,需要满足以下三个条件,
- 该类型的所有实例都已被回收;
- 该类型的类加载器已经被回收;
- 该类型对应的 java.lang.class 对象没有在任何地方被引用。
满足以上三个条件的类型,将被允许回收,但是并不是必然会被回收,可以通过 -Xnoclassgc 来控制是否对类型进行垃圾回收。在大量使用反射、动态代理、CGlib 等字节码框架,动态生成 JSP 等场景中,需要 Java 虚拟机具备类型卸载能力。
垃圾收集算法
分代收集理论
收集器应该将 Java 堆划分出不同的区域,然后根据回收对象的年龄,将其分配到不同的区域之中存储,垃圾收集器可以每次只回收某一个或者某些部分的区域。
- 弱分代假说:绝大多数对象是朝生夕灭的。
- 强分代假说:能熬过越多轮垃圾回收的对象,越难消亡。
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
- 针对跨代引用,会在新生代中建立一个全局的记忆集(Remembered Set),标识出老年代中的哪一块区域会存在跨代引用,在发生 Minor GC 时,只需要把老年代中这一小块内存中的对象加入 GC ROOT 进行扫描就行。
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出存活的对象,统一回收所有未被标记的对象。
缺点:1.执行效率不稳定,如果 Java 堆中包含大量对象,而其中大部分是需要回收的,这时必须进行大量的标记和清除动作,导致标记和清除的过程执行效率降低;2.会使内存空间碎片化,标记、清除会产生大量不连续的内存碎片。
标记-复制算法
先标记出一个半区中所有存活的对象,然后将存活的对象复制到另一个半区的内存空间上,再将已使用过的内存空间一次清理掉,这样就不需要考虑内存空间碎片的问题,
缺点:1.将可用内存缩小为原来的一半;2.在对象存活率较高时需要进行较多的复制操作,效率会降低。
改进: “Appel”式回收,将内存空间分为一块较大的 Eden 区和两块较小的 Survivor 区,每次分配内存只使用 Eden 和其中一块 Survivor 区,发生垃圾回收时,将 Eden 和一块 Survivor 区中存活的对象一次性复制到另一块 Survivor 区中,然后清理掉 Eden 区和一块 Survivor 区。
分配担保机制:当 Survivor1 区没有足够的空间存放一次 Minor GC 之后存活对象时,这些对象将通过分配担保机制来提前进入老年代。
标记-整理算法
先标记所有存活的对象,再将所有存活的对象都想内存空间的一端移动,然后直接清理掉边界以外的内存。
缺点:1.在有大量对象存活的区域,需要大量的移动对象,必须全程暂停用户程序才能进行(Stop The World)
但如果像标记-清除算法那样不考虑移动和整理存活对象的话,会产生内存碎片,在每次对象分配内存的时候,需要通过一个分区空闲分配链表来解决内存分配问题。
这是一个两难的选择,移动对象,垃圾回收的时间会增加,不移动对象,内存分配时会更复杂,但是从全局来看,内存的分配和访问比垃圾回收更频繁,所以即使移动对象,整体的吞吐量还是上升的。
CMS收集器:在大多数时候使用标记-清除算法,当内存碎片率达到一定比例时,进行一次标记-整理算法收集。
HotSpot 的算法实现
根节点枚举
安全点
安全区域
记忆集与卡表
写屏障
并发的可达性分析
经典垃圾收集器
- Serial 收集器
使用标记-整理算法 - ParNew 收集器
使用标记-整理算法 - CMS 收集器
使用标记-清除算法
内存分配与回收策略
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/29016.html