JVM(一)——GC,内存分配和垃圾回收

心得:Java中垃圾回收和内存可以实现高度的自动化,栈帧可以由JVM自动分配和回收,局部变量表和操作数栈也可以在编译时就确定好,堆中的内存分配和回收才是JVM关注的重点,JVM实现大多采用可达性分析来标记存活对象,什么时候标记?让用户线程主动跑到那些安全的地方(引用关系不变的时候,SafePoint和Safe Region),再由GC收集器来标记进行处理。

不同的垃圾收集器甚至可以决定堆的内存布局,比如G1的“化增为零”一方面借助Remember Set可以更细粒度的进行并发标记和回收。

分代是GC种重要的思想,对不同特点对象进行各自适合的回收策略,Minor GC一般是新生代采用Copying算法,“空间换时间”,也是因为新生代大部分对象“朝生夕死”;Full GC在老年代一般是Mark and Compact两个步骤,不用额外空间,但停顿长,可谓“用时间省空间”;

CMS和G1更是采用并行+并发的手段,但一个新的问题,就是并发期间的用户线程的内存开销(CMS和G1各有对应策略)和对象引用关系的变化,因此它们都有remark的过程。

总之,不同的场景用不同的技术,“知其然”的同时能够“知其所以然”才能在实际的场景下选择“对的”技术。

垃圾回收是一个复杂的系统问题,本人认识还是十分有限。。。

学习参考资料
(1)《深入Java虚拟机》(第二版);
(2)RednaxelaFX的回答—现代JVM中的Safe Region和Safe Point到底是如何定义和划分的?
(3)G1垃圾收集器入门
(4)我在知乎的提问(多谢RednaxelaFX大神的回答)
(5)CMS的原始论文:A Generational Mostly-concurrent Garbage Collector

一个小栗子(我在知乎的提问)

一个问题Java中的对象到底占多少内存?
JVM规范也不能回答这个问题,因为它是一个公有设计;

The Java Virtual Machine does not mandate any particular internal structure for objects.
看过书的同学应该都知道,对象由对象头+实例数据+padding组成;我利用Instrumentation做了一个小小的实验,基于64位JDK 8的Hotspot:

/*
        基本信息:对象内存布局,对象的大小
        注意:以HotSpot为例,Java中的对象内存布局包括:MarkWord,ClassPointer,实例数据,padding
        如果是数组还有数组长度;如果开启了字段压缩,会进行指针压缩,子类的窄变量会插入父类的宽变量之中
        */
        public static void main(String[] args) {
            //Object
            System.out.println(ClassUtils.sizeOf(new Object())); //16 8字节MarkWord,4字节klass指针,4字节padding
            //数组
            System.out.println(ClassUtils.sizeOf(new byte[0])); //16 8字节MarkWord,4字节klass指针,4字节数组长度
            System.out.println(ClassUtils.sizeOf(new byte[7])); //24 padding补齐
            System.out.println(ClassUtils.sizeOf(new byte[_1MB])); //1024 * 1024 + 16 = 1048592
            //窄对象
            System.out.println(ClassUtils.sizeOf(new Integer(1))); //16 int和klass补齐
            System.out.println(ClassUtils.sizeOf(new Byte("1"))); //16 byte和klass补齐
            System.out.println(ClassUtils.sizeOf(new Character('a'))); //16 char和klass补齐
        }

因为对象的大小一定是8的倍数,可以看到Hotspot很节省的将类型指针和数组长度或者int,byte,char合并保存了,而64位的Hotspot中reference的长度仍然是4个字节,一些博客上有说成8个字节的。

1. 确定回收对象

引用计数和可达性分析(counting和tracing)

前者通过对象的引用计数器来记录被引次数,显然的一个问题是循环引用;Java采用的是可达性分析;

从GC Roots出发,延引用链对对象进行搜索,没有任何引用链和GC Roots相连的对象就被成为不可达的,被判定为可回收的对象;

GC Roots(方法区和栈中):
(1)虚拟机栈(栈帧中的局部变量表);
(2)本地方法栈JNI引用的对象;
(3)方法区中类静态属性;
(4)方法区中常量引用;

引用类型

就像进程的状态不能由简单的运行和终止描述一样;引用也需要进行一步细分:

强引用:永远不会被回收掉的对象;
软引用:如果一次回收后,内存还是不足,才进行回收,如果再不够,OOM;
弱引用:发生GC时,无论内存是否足够都会被回收;
虚引用:程序不能引用到,但是被回收时可以收到一个通知;

一个很重要的应用就是缓存,在内存中缓存一定要注意防止内存泄漏,在Java Collection Framework中,容器在删除是都执行置空的操作;
另一个注意的是可以使用WeakHashMap作为缓存容器;如果不是WeakHashMap一定要控制数量和及时清除(Integer.valueOf等就控制了数量);

终结(finalize)

从对象的可触及性来说还有2个状态:
可复活和不可触及(两次标记):
在判定为不可达后:
(1)可复活:一次标记,对象分为没有必要执行finalize方法(包括没有覆盖和已经执行两种)需要执行finalize方法,前者直接就可以被回收;
(2)有必要执行finalize的方法被放在F-Queue队列中,有JVM中一个低优先级的Finalizer线程去触发它们的finalize方法(不会等待方法结束),如果在finalize方法中对象有引用链建立了连接就会被“复活”,否则就Over;

一个对象的finalize方法只能被执行一次,也就是说一个对象甭想自救两次!

方法区中的回收

对于常量来说,没有任何东西引用,那么也是可以被回收的;

类的卸载:条件非常苛刻(JVM规范没有要求在方法区中实现垃圾回收,Hotspot中有但是类还是很难被卸载)
(1)该类所有的实例被回收;
(2)对应的ClassLoader被回收;
(3)对应的Class对象没有引用;

栈的回收是虚拟机静态分配和回收,栈帧的大小可以在编译时确定,JVM通过栈帧的分配和回收很容易(开销很低)就完成;
方法区的回收中类的卸载很棘手,对于大量用反射,动态代理,CGLIB等技术的程序,JVM要能够卸载类;
主要的一个问题就是堆中内存的分配和卸载;

2. 垃圾回收算法

标记-清除算法(Mark-Sweep)

问题:
(1)空间碎片;
(2)效率:标记和清除两个过程相对来说不高;

复制算法(Copying)

原理:两块内存来回复制,因为有一块空白的内存可以直接复制,因此不用再分Mark,Sweep或者Compact多个阶段了,空间换时间;
当然我们知道最后的设计是:一个Eden+两个较小的Survivor,这是由于Java中对象98%的新对象都可以被回收的统计数据得来的经验;

标记-整理算法

区别与“标记-清除”,整理指的是不再原来位置直接进行回收,而是存活的对象向一端移动,最后界限之外的部分直接清理掉;

根据不同对象的特点,采用分代的方式垃圾回收;

3. HotSpot的垃圾回收算法实现

什么时候回收垃圾,怎样尽量降低对用户线程的影响不同的业务需求对垃圾回收有什么不同的要求?

枚举根节点

一致性:进行引用链分析显然要基于一个一致性的快照,不能因为分析过程中引用关系变化而导致错误;

Stop the world:一个简单直接的办法,但是显然会产生停顿;

OopMap:为了避免进行全盘扫描,借助与OopMap这样的数据结构保存对象的被引用范围,告诉JVM哪些地方存折对象的引用;

安全点(节省开销,安全性)

定义:the thread’s representation of it’s Java mac

### JVM 内存模型 Java 虚拟机(JVM)内存被划分为多个不同的区域,这些区域各自承担特定的任务并具有独特的特性。 #### 方法区 方法区存储已被虚拟机加载的信息、常量、静态变量、即时编译后的代码等数据。此部分可以设置参数`-XX:+HeapDumpOnOutOfMemoryError`使得在发生 `OutOfMemoryError` 错误时生成堆转储文件以便分析[^2]。 #### 堆 堆是所有线程共享的块最大的内存区域,在几乎所有的应用中都是垃圾收集器管理的主要对象。它又细分为新生代老年代两个子域: - **新生代 (Young Generation)**:大多数情况下新创建的对象都会优先分配在此处。由于这部分经常有大量短生命周期的对象产生,因此频繁执行年轻代回收即Minor GC/Young GC。 - **老年代 (Old Generation)**:经过多次存活下来的对象会被晋升到老年代。这里发生的Full GC涉及整个堆空间,包括永久代或元空间,通常频率较低但耗时较长。 #### Java栈 每个线程都有独立的Java栈,它是用来保存局部变量表、操作数栈、动态链接、返回地址等信息的地方。每当启动条新的线程就会为其创建相应的Java栈实例。 #### 本地方法栈 与Java栈作用相似,不过这里是专门为Native Method服务的空间。 #### PC寄存器 程序计数器是块较小的内存空间,它的唯目的是记录当前线程所执行字节码指令的位置。 #### 直接内存 虽然不属于传统意义上的JVM运行时数据区组成部分之,但在某些场景下也会涉及到直接内存的操作。这内存在使用前需借助于JNI接口向操作系统请求获取,并且不受限于常规意义上由GC负责清理范围之内;如果超出可用限度则可能触发`OutOfMemoryError`异常[^3]。 ### 垃圾回收算法 为了提高效率,针对不同型的内存分区采用了多种策略相结合的方式来进行垃圾回收工作: #### 新生代回收 对于新生代而言,主要采取的是复制算法。考虑到大部分对象很快便会成为垃圾的特点,只需少量移动存活下来的数据即可完成清理过程。然而这种方式也意味着每次都需要预留足够的备用空间供转移之用。 #### 老年代回收 相比之下,老年代中的对象往往拥有更长的生命期,故而更适合运用标记清除或是标记整理的方法处理。前者能够快速定位待释放的部分,后者除了标注外还会将剩余的有效元素紧凑排列在起从而减少碎片化现象的发生。 #### 分代回收机制 实际上所谓的分代回收正是基于上述两种思路之上发展起来的种综合性解决方案——即将整个堆分成若干个小单元分别对待,依据各自的特征选取最合适的清扫手段组合而成。例如,当Eden区或者Survivor区不足以容纳新增加的内容时,则会触发次轻度的小规模搜集活动(Minor GC),反之若是老年区内出现了容量不足的情况,则不得不进行全面的大扫除(Full GC)[^5]。 ```java // 示例代码展示如何强制建议进行垃圾回收(尽管实际效果取决于JVM实现) public class GCDemo { public static void main(String[] args) { System.gc(); // 提交垃圾回收请求给JVM, 不保证立即执行 } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值