赞
踩
有疑问的地方标记※
C、C++程序开发需要内存管理,内存的分配及回收。
Java开发只需要内存分配,内存回收是jvm的工作
一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。
线程私有
记录执行到哪一行,为了线程切换后能恢复到正确的执行位置
如果是本地方法则计数器值为空
不会出现OOM
线程私有
调用一个方法时,jvm创建一个栈帧(Stack Frame)存储局部变量表、操作数栈、动态连接、方法出口等信息
方法被调用直至执行完毕,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
如果栈深度大于虚拟机所允许的深度StackOverflowError
如果栈容量可以动态扩展无法申请到足够的内存会抛出OutOfMemoryError
线程私有
与虚拟机栈作用类似,区别是执行本地方法
线程共享
虚拟机启动时创建,存放对象实例
垃圾收集器管理的内存区域,也被称作“GC堆”
新生代:Eden空间、From Survivor空间、To Survivor空间
老年代
通过参数-Xmx和-Xms设定
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常
线程共享,别名叫作“非堆”(Non-Heap)
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
是方法区的一部分。
存放Class文件的常量池表
一般都是编译期,除非动态编译
方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
之前叫永久代,有-XX:MaxPermSize的上限,且有默认大小,能够像堆内存一样进行分代回收,但永久代一般不需要这些特点(除非动态编译,需要GC)
所以JDK 7的HotSpot,把原本放在永久代的字符串常量池、静态变量等移到元空间
到了jdk8完全废弃了永久代的概念,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
不属于虚拟机运行时数据区的一部分
※NIO,基于通道(Channel)与缓冲区 (Buffer)的I/O方式,使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆中来回复制数据。
可能导致OutOfMemoryError异常出现
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:
对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
通过栈上的reference,定位堆上的具体对象。
访问方式主要有两种,句柄和直接指针:
《Java虚拟机规范》规定,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能
年轻代老年代全满了,并且fullGC清理不掉,jvm就挂了
java.lang.OutOfMemoryError: Java heap space
设置堆内存最大值与最小值为20M
-Xms20m -Xmx20m
在出现内存溢出异常的时候Dump出当前的内存堆转储快照会把整个堆内存dump下来,不必担心性能,因为这时jvm已经挂了
-XX:+HeapDumpOnOutOfMemoryError
package com.baomidou.mybatisplus.samples.assembly; import java.util.ArrayList; import java.util.List; /** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { public static void main(String[] args) { List list = new ArrayList(); while (true) { list.add(new Object()); } } }
结果
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid46776.hprof ...
Heap dump file created [28353060 bytes in 0.058 secs]
生成堆内存快走文件
分析快照文件结果
使用jdk自带工具jvisualvm.exe(工具在jdk\bin目录下)分析
点击线程
可以定位到代码具体位置
点击类,可以看到实例个数
堆内存溢出:分为内存泄漏(Memory Leak)、内存溢出(Memory Overflow)
HotSpot虚拟机中不区分虚拟机栈和本地方法栈
-Xoss参数设置本地方法栈大小(实际没有效果)
栈容量由-Xss参数来设定
《Java虚拟机规范》中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,HotSpot虚拟机的选择是不支持扩展,所以创建线程申请内存时,内存不足就会OutOfMemoryError。线程运行时不会出现OutOfMemoryError,只会因为无法容纳新的栈帧出现StackOverflowError
总结
不要一直创建线程,使用线程池,可以避免OutOfMemoryError
尽量避免使用递归,递归过深就会StackOverflowError,如果避免不了就设置-Xss,但设置后线程数就会减少,因为每个线程占用的内存多了
方法区的主要职责是用于存放类型的相关信息,如类 名、访问修饰符、常量池、字段描述、方法描述等
/** * JDK 6中运行,会得到两个false * 在JDK 6中 ,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储 * intern()方法返回是永久代里面这个字符串实例的引用 * StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。 * JDK8:true、false * JDK8常量池在元空间,但字符串常量池在堆 * 就不需要再拷贝字符串的实例从堆到字符串常量池,因为字符串常量池在堆里面 */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { String str1 = new StringBuilder("计算机").append("软件").toString(); System.out.println(str1.intern() == str1); //“java”字符是加载sun.misc.Version这个类的时候已经进入常量池 String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2); } }
-XX:MaxDirectMemorySize指定
默认与Java堆最大值(由-Xmx指定)一致
jdk8总结
垃圾收集(Garbage Collection,GC)
·哪些内存需要回收?
·什么时候回收?
·如何回收?
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是垃圾。
优点
原理简单,判定效率也很高
缺点
占用了一些额外的内存空间来进行计数
循环引用时无法回收
在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。
Object obj=new Object()
Object obj=new Object();
SoftReference reference = new SoftReference(obj);
//通过get()方法获取引用的指针,如果被回收返回的就是null
Object o = reference.get();
Object obj=new Object();
WeakReference reference = new WeakReference(obj);
//通过get()方法获取引用的指针,如果被回收返回的就是null
Object o = reference.get();
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class Test { public static void main(String[] args) throws InterruptedException { PhantomReference reference = getReference(); //执行gc System.gc(); //get()方法返回null System.out.println(reference.get()); //被回收true,如果不执行gc返回false System.out.println(reference.isEnqueued()); //被排队false,如果不执行gc返回true System.out.println(reference.enqueue()); } static PhantomReference getReference() { Object obj = new Object(); ReferenceQueue queue = new ReferenceQueue(); PhantomReference reference = new PhantomReference(obj, queue); return reference; } }
经过2次标记
/** * 仅仅为演示使用,实际不要这么做 * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 */ public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); //对象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } // 下面这段代码与上面的完全相同,但是这次自救却失败了 SAVE_HOOK = null; System.gc(); // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } } }
简单来讲,判断对象不可达进行第一次标记,如果覆盖了finalize()方法就放入F-Queue队列,执行finalize()时,如果被拯救变成可达,就移出。否则就标记为垃圾
个人认为没必要
性价比低
回收不可达的常量,不再被使用的类
不再使用的类要三个条件:
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载 器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
建立在两个分代假说之上:
“标记-清除”(Mark-Sweep)算法
首先标记出需要回收的对象,后回收掉被标记对象,也可以反过来,标记存活对象,统一回收未标记对象。
缺点有两个
第一个:执行效率不稳定。当堆中包含大量需要GC的对象,须大量标记和清除,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
第二个:内存空间的碎片化问题。标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记复制(半区复制Semispace Copying)算法
为解决标记-清除算法面对大量可回收对象时执行效率低的问题
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
当多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
实现简单,运行高效
缺点:
将可用内存缩小为了原来的一半,空间浪费。
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
※存活的对象都向内存空间一端移动,可回收对象占着位置,能移动吗?,有可能移动就是覆盖
缺点:
如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行“Stop The World”
3.4.2 安全点
3.4.3 安全区域
3.4.4 记忆集与卡表
3.4.5 写屏障
3.4.6 并发的可达性分析
图3-6展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用
中间横线上面是年轻代垃圾收集器,下面是老年代
横线上的比较特殊,但也遵循分代理论
标记-复制
基于标记-整理
基于标记-整理
整体来看基于标记-整理,局部(两个Region 之间)基于标记-复制
把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待,如图3-12所示
在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region
如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的 运作过程大致可划分为以下四个步骤:
·初始标记(Initial Marking):只标记GC Roots直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
·筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的
除了 CMS收集器,其他都不存在只针对老年代的收集。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。