对象分配原则
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代
- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
类加载器
- 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
- 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器
- 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
Java对象结构
Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
如何判断对象可以被回收?
判断对象是否存活一般有两种方式:
- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
- 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
JVM的永久代中会发生垃圾回收么?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)
引用分类
- 强引用 : GC时不会被回收
- 软引用 : 描述有用但不是必须的对象,在发生内存溢出异常之前被回收
- 弱引用 : 描述有用但不是必须的对象,在下一次GC时被回收
- 虚引用(幽灵引用/幻影引用) : 无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用用来在GC时返回一个通知
OutOfMemoryError异常
- (堆) Java对用于存储对象实例,只要不断的创建对象,并保证避免垃圾回收机制清除这些对象,那么对象数量达到最大堆的容积之后就会内存溢出
- (虚拟机栈、本地方法栈) 虚拟机在扩展栈时无法申请到足够的内存空间
- (运行时常量池) list.add(String.valueOf(i++).intern()),提示信息是"PermGen space"
- (方法区) 运行时产生大量的类去填满方法区,直到溢出
- (本机直接内存) Unsafe.allocateMemory()
垃圾收集算法
算法 | 描述 | 优点 | 缺点 |
---|---|---|---|
标记-清除 | 首先标记出所有需要回收的对象,在标记 完成后统一回收掉所有被标记的对象 | 最基础的收集算法 | 1.效率不高2.标记清除之后会 产生大量不连续的内存碎片 |
复制 | 将可用的内存容量划分为两个大小相等的两块, 每次只使用其中一块。当这一块的内存使用完, 就将还存活的对象复制到另外一块,然后将 已使用过的一次清理掉,适用于新生代 | 1.每次都是对半区进行内存回收, 内存分配时也不需要考虑内存碎片等复杂情况 2.只需要移动堆顶指针,按顺序分配内存即可 3.实现简单,运行高效 | 1.将内存缩小为原来的一半,代价高 2.在对象存活率较高时就要进行较多 的复制操作,效率将会变低 |
标记-整理 | 先标记出所有需要回收的对象,然后让所有存 活的对象都想一端移动,然后直接清理掉端边 界以外的内存 | 适用于对象存活率高老年代 | |
分代收集算法 | 把Java堆分成新生代和老年代,根据各个年代 的特点采用最适当的收集算法。新生代采用复制, 老年代采用标记-清除或标记-整理 |
垃圾收集器
- Serial收集器,最基本最古老的收集器,是一个单线程的收集器,可能产生较长时间的停顿。Client模式下的虚拟机默认新生代的收集器
- ParNew收集器,ParNew是Serial收集器的多线程版本。ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果。它默认开启的收集线程数与CPU数量相同,在CPU非常多的情况下,可以用-XX:ParallelGCThreads参数限制垃圾收集的线程
- Parallel Scavenge收集器,是一个新生代收集器、采用复制算法、并行的多线程收集器。Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- Serial Old收集器,是Serial收集器的老年代版本,同样是一个单线程收集器,使用"标记-整理"算法
- Parallel Old收集器,是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法
- CMS(Concurrent Mark Sweep)收集器,是一种以获取最短回收停顿时间为目标的收集器。基于"标记-清除"算法,整个过程分为4步:
- 初始标记 (CMS initial mark)
- 并发标记 (CMS concurrent mark)
- 重新标记 (CMS remark)
- 并发清除 (CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要"Stop The World"。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记就是进行GC Roots Tracing定位,而重新标记是为了修正并发标记期间用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
整个过程中耗时最长的并发标记和并发清除都是跟用户线程一起工作,所以,从总体上来说,CMS收集器内存回收是与用户线程一起并发执行的
缺点
- CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4
- CMS收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生
- G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
- 初始标记 (Initial Marking)
- 并发标记
- 最终标记
- 筛选回收
网友评论