Java核心篇之JVM探秘:对象创建与内存分配机制

系列文章目录

第一章 Java核心篇之JVM探秘:内存模型与管理初探

第二章 Java核心篇之JVM探秘:对象创建与内存分配机制

第三章 Java核心篇之JVM探秘:垃圾回收算法与垃圾收集器

第四章 Java核心篇之JVM调优实战:Arthas工具使用及GC日志分析


目录

前言

一、对象创建过程

(1)类加载检查

(2)分配内存

1.对象创建与内存分配

2.对象填充

3.执行构造函数

4.并发问题处理

(3)初始化零值

(4)设置对象头

Mark Word

Type Pointer

Array Length (如果对象是数组)

(5)执行方法

二、对象内存分配

(1)对象在栈上分配

示例:

(2)对象在Eden上分配

Eden区分配的特点:

示例:

(3)大对象直接进入老年代

(4)长期存活的对象将进入老年代

(5)对象动态年龄判断

(6)老年代空间分配担保机制

三、对象内存回收

(1)引用计数法

示例:

(2)可达性分析算法

(3)常见引用类型

(4)finalize()方法最终判定对象是否存活

(5)如何判断一个类是无用的类

总结


前言

        Java虚拟机(JVM)是Java语言的核心组件之一,负责执行Java字节码。在JVM中,对象的创建和内存管理是一个复杂而精细的过程,涉及多个阶段和多种策略。本文将深入探讨JVM中的对象创建流程、内存分配机制以及它们如何影响程序性能。


一、对象创建过程

下边两张图分别是类加载机制和对象创建过程的流程图

(1)类加载检查

当程序请求创建一个新对象时,JVM首先会检查这个类是否已经被加载、解析和初始化过。如果尚未完成这些步骤,那么JVM会先执行类加载过程。

(2)分配内存

一旦类被确认可以使用,JVM会在堆内存中为新对象分配空间。对象的内存大小由其成员变量决定,包括实例变量和继承链上的变量。

在JVM中,内存分配主要发生在堆内存中,堆内存是所有线程共享的内存区域,用来存储所有Java对象实例和数组。内存分配的过程可以分为几个关键步骤:

1.对象创建与内存分配

当Java程序请求创建一个新对象时,JVM首先检查这个类是否已经被加载、解析和初始化过。如果类已经准备好,JVM会在堆中为新对象分配内存。内存分配的方式有两种主要策略:指针碰撞(Bump-the-Pointer)和空闲列表(Free List)。

  • 指针碰撞:如果堆内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间维护一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
  • 空闲列表:如果堆内存是不规整的,已使用的内存和空闲的内存相互交错,那就需要维护一个列表,记录上面那些大小不一的空闲内存区间。

2.对象填充

分配好内存之后,JVM会对新对象进行填充,包括初始化对象的成员变量为默认值(例如,int类型为0,引用类型为null)。

3.执行构造函数

最后,JVM会执行对象的构造函数,完成对象的初始化。

4.并发问题处理

在多线程环境下,内存分配和对象创建可能会引发竞态条件和内存一致性问题。JVM采用了多种机制来解决这些问题:

  • 线程同步 JVM使用锁机制来保证在多线程环境下内存分配的原子性。当多个线程试图同时创建对象时,JVM会使用锁来确保一次只有一个线程能够进行内存分配,从而避免竞态条件。

  • 内存屏障 为了保证内存访问的有序性,防止指令重排序,JVM使用内存屏障(Memory Barrier)技术。内存屏障是一种特殊的指令,它可以阻止编译器和处理器对内存操作进行重排序,确保内存操作的顺序符合程序的预期。

  • 缓存一致性 在现代多核处理器中,每个CPU都有自己的缓存,为了保持缓存之间的一致性,处理器使用了一种称为MESI(Modified, Exclusive, Shared, Invalid)协议的缓存一致性协议。JVM利用硬件提供的缓存一致性协议来维持多线程环境下的内存一致性。

  • TLAB (Thread Local Allocation Buffer) 为了减少锁的使用,提高对象创建的效率,JVM提供了一个叫做TLAB(线程本地分配缓冲区)的概念。每个线程都有一个独立的TLAB,对象优先在自己的TLAB中分配,这样可以避免在多线程环境下频繁地获取锁,从而提高了对象创建的速度。

  • CAS (Compare and Swap) CAS是一种无锁编程技术,用于原子更新变量。JVM利用CAS操作来实现一些轻量级的同步机制,比如原子变量类(如java.util.concurrent.atomic包中的类)。

(3)初始化零值

分配完内存后,JVM会将对象的成员变量初始化为默认值,如整型为0,浮点型为0.0,引用类型为null等。这一步是为了确保对象在构造函数执行前处于一致状态。

(4)设置对象头

当JVM为新对象分配完内存后,在初始化零值之前,它会先设置对象头。对象头通常包含以下信息:

Mark Word

Mark Word是对象头中的一部分,主要用于存储对象的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁等。Mark Word的布局会根据对象的锁定状态动态改变,以便支持轻量级锁、偏向锁、重量级锁等不同的锁级别。

Type Pointer

这是指向对象所属类的元数据的指针,JVM通过这个指针确定对象所属的类。在某些JVM实现中,类型指针可能不在对象头中,而是通过其他方式(如类指针压缩)来存储。

Array Length (如果对象是数组)

如果创建的对象是一个数组,对象头还会额外包含一个长度字段,用于存储数组的长度。

(5)执行<init>方法

执行<init>方法,也就是执行对象的构造函数,这是程序员定义的用于初始化对象状态的方法。构造函数可以调用其他方法或访问静态变量,但不能直接访问或修改非初始化的实例变量。

二、对象内存分配

(1)对象在栈上分配

通常情况下,对象是在堆内存中分配的,这是因为对象的生命周期不可预测,可能需要长期存在。然而,在某些特殊情况下,对象可以分配在栈上,这种情况被称为栈上分配(Stack Allocation)或标量替换(Scalar Replacement)。栈上分配主要应用于局部变量,尤其是那些在方法体内创建并很快就会被销毁的小型对象,这样可以显著减少垃圾收集的压力。 

示例:

假设有一个简单的Java方法,其中创建了一个局部变量对象,这个对象不会被方法之外的代码引用,而且它的生命周期仅限于该方法的执行期间。在这种情况下,如果JVM启用了栈上分配并且经过逃逸分析确定该对象不会逃逸,那么该对象就有可能在栈上分配。

public class StackAllocationExample {public void method() {User user = new User();// 使用user...// user仅在method方法的栈帧中存在}
}class User {String name;int age;
}

在这个例子中,User对象只在method方法内部创建和使用,如果它满足栈上分配的条件,那么它将会在栈上分配,而不是在堆上。 

(2)对象在Eden上分配

JVM的堆内存被划分为新生代和老年代。新生代又进一步分为一个Eden区和两个Survivor区(S0和S1)。Eden区是对象首次创建的地方,大部分对象在Eden区分配。

Eden区分配的特点

  • 对象创建时,首先尝试在Eden区内分配。
  • 如果Eden区没有足够的空间,或者对象太大,可能直接进入老年代。
  • 经过若干次垃圾回收(Minor GC),存活下来的对象会从Eden区晋升到Survivor区,或者直接晋升到老年代。

示例:

当一个对象创建时,默认情况下它会在Eden区内分配。Eden区是新生代的一部分,专门用于存放新创建的对象。如果对象在一次或多次垃圾回收后仍然存活,它将被移动到Survivor区,或者直接晋升到老年代。

public class EdenAllocationExample {public static void main(String[] args) {User user = new User();// 使用user...}
}class User {String name;int age;
}

在这个例子中,User对象创建在Eden区,如果它在接下来的垃圾回收过程中存活,那么它可能被晋升到Survivor区或老年代。 

(3)大对象直接进入老年代

大对象指的是需要大量连续内存空间的对象,例如大的数组或长字符串。JVM为了减少新生代的碎片化,避免频繁的Minor GC,采取了以下策略:

  • 如果对象的大小超过了一定的阈值(通常是JVM的一个参数设置),这个对象会被直接分配到老年代中。
  • 直接进入老年代可以避免新生代的频繁垃圾回收,因为大对象通常生命周期较长,不易被回收。
  • 这种策略有助于提高大对象密集型应用的性能,减少垃圾收集的次数。

(4)长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数–XX:MaxTenuringThreshold来设置。

(5)对象动态年龄判断

对象的年龄是指对象从创建开始到被垃圾收集器回收之间所经历的Minor GC次数。JVM给每个对象分配一个年龄计数器(Age Counter)。对象在Eden区创建后,如果在第一次Minor GC后依然存活,它会被移动到一个Survivor区,并且年龄计数器会加1。此后,每次经历Minor GC并且没有被回收,年龄计数器都会递增,直到达到一定的阈值(默认为15)。当对象的年龄达到这个阈值时,它将被提升到老年代。

对象年龄的判断机制有助于将长期存活的对象及时移动到老年代,避免新生代的频繁垃圾回收,同时也减少了老年代的垃圾回收压力,因为老年代的垃圾回收(Full GC或Major GC)成本更高。

(6)老年代空间分配担保机制

在进行Minor GC之前,JVM会检查老年代是否有足够的空间来接收可能从新生代提升过来的对象。如果老年代的空间不足以担保这次Minor GC后所有存活对象的迁移,JVM将触发一次Full GC,以腾出足够的空间。这种机制被称为老年代空间分配担保(Space Allocation Guarantee),其目的是避免在新生代进行垃圾回收时出现内存不足的情况,确保Minor GC的顺利进行。

如果担保机制检测到老年代空间不足,JVM会进行如下操作:

  1. 尝试压缩老年代,回收部分空间。
  2. 如果压缩后仍然不足,将触发Full GC,清理整个堆内存,包括老年代和永久代(在JDK 8中是方法区)。
  3. 如果Full GC后仍然不足,将抛出OutOfMemoryError异常。

三、对象内存回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象是没有被任何地方使用的。

(1)引用计数法

引用计数法是一种简单的内存管理策略,它通过跟踪指向一个对象的引用数量来确定对象是否可被回收。每当有一个地方引用一个对象时,它的引用计数器就会加1;当引用失效时,计数器减1。当一个对象的引用计数变为0时,表明没有任何引用指向它,此时它就可以被回收了。

优点:实现简单,运行时不需要进行额外的计算或全局性的搜索,即时性好。

缺点:无法处理循环引用的问题。如果有两个对象相互引用对方,即使它们不再被外部引用,引用计数法也无法正确识别它们为垃圾,从而导致内存泄露。

示例:

尽管Java的垃圾收集器不采用引用计数法,但我们可以通过一个类似场景的示例来说明,如果在引用计数法下,两个对象互相引用时可能导致的内存泄漏情况。下面是一个简化版的伪代码示例,用于说明这个问题:

// 注意:以下代码仅用于演示,实际上Java的垃圾收集器不使用引用计数法
class Node {private Node reference;public Node(Node ref) {this.reference = ref;}public void setReference(Node ref) {this.reference = ref;}public Node getReference() {return reference;}
}public class ReferenceCountingDemo {public static void main(String[] args) {Node nodeA = new Node(null);Node nodeB = new Node(nodeA);nodeA.setReference(nodeB);// 现在nodeA和nodeB互相引用,如果没有垃圾收集器,将导致它们都无法被回收}
}

在引用计数法中,nodeAnodeB互相引用对方,即使它们不再被任何外部引用持有,但由于它们相互之间的引用,它们的引用计数都不会降到0,因此按照引用计数法,这两个对象将永远不会被回收,导致内存泄漏。 

(2)可达性分析算法

这是JVM中常用的垃圾回收算法。它基于一个基本思想:通过一系列称为“GC Roots”的根对象作为起始点,从这些根对象向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,即可判定此对象是不可达的,即不可能再被使用。

GC Roots通常包括:

  • 正在执行的方法中声明的局部变量对象。
  • 方法调用栈中引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。
  • Java虚拟机内部引用的对象,如基本类型的Class对象,或者常量池中的引用。

(3)常见引用类型

Java中定义了四种强度不同的引用类型,它们分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

  • 强引用是最常用的引用类型,只要强引用存在,垃圾回收器就不会回收掉对象。
  • 软引用用于描述还有用但非必需的对象。当系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用比软引用的强度更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。
  • 虚引用也称为幽灵引用或幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

(4)finalize()方法最终判定对象是否存活

在Java中,finalize()方法是Object类的一个保护方法,允许在对象被垃圾回收前做一些必要的清理工作。当垃圾回收器准备回收一个对象时,如果发现这个对象中定义了finalize()方法,就会自动调用这个方法。但是,finalize()方法的调用并不是强制的,也不保证一定会被调用,且其执行时机不确定,不应依赖它进行资源释放,应使用try-finally或try-with-resources语句进行资源的显式释放。

示例:

在Java中 finalize()方法可以用来执行一些清理工作,如关闭文件、网络连接等资源。然而,finalize()方法的调用不是强制的,且其实现细节和调用时机由JVM决定,因此不应该依赖它来确保资源的释放。下面是一个使用finalize()方法的示例:

class ResourceManagedObject {private boolean isClosed = false;protected void finalize() throws Throwable {if (!isClosed) {// 执行清理工作,例如关闭文件或网络连接System.out.println("Resource cleaned up by finalize()");isClosed = true;}super.finalize();}public void close() {// 显式关闭资源System.out.println("Resource closed explicitly");isClosed = true;}
}public class FinalizeDemo {public static void main(String[] args) throws Exception {ResourceManagedObject obj = new ResourceManagedObject();// 使用obj...// 显式关闭资源obj.close();// 删除引用,使obj成为垃圾收集的目标obj = null;System.gc(); // 请求垃圾收集// 等待垃圾收集器运行Thread.sleep(1000); // 假设垃圾收集器会在1秒内运行}
}

在这个示例中,ResourceManagedObject类实现了finalize()方法,用于在对象被垃圾收集前执行资源清理工作。然而,最佳实践是不要依赖finalize()方法,而应该在不再需要对象时显式地调用close()方法来释放资源,这是因为finalize()方法的执行是不确定的,且其执行可能带来性能上的开销。此外,从Java 9开始,finalize()方法的使用已被弃用,建议使用其他资源管理技术,如try-with-resources语句或显式的资源关闭逻辑。 

(5)如何判断一个类是无用的类

类的卸载(Class Unloading)在Java中并不常见,但在某些特定情况下,如Web容器中,可能需要卸载不再使用的类。判断一个类是否无用,通常考虑以下几点:

  • 类的所有实例都已经回收。
  • 没有任何引用指向该类的Class对象。
  • 类的加载器已经回收或可被回收。

总结

        JVM中对象的创建和内存分配是一个多步骤、多策略的过程,涉及类加载、内存布局、垃圾回收等多个层面。理解和优化这一机制对于提升Java应用程序的性能至关重要。通过合理的设计和编码实践,我们可以最大限度地发挥JVM的优势,构建高效稳定的应用系统。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/46065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《Windows API每日一练》9.25 系统菜单

/*------------------------------------------------------------------------ 060 WIN32 API 每日一练 第60个例子POORMENU.C&#xff1a;使用系统菜单 GetSystemMenu函数 AppendMenu函数 (c) www.bcdaren.com 编程达人 -------------------------------------------…

亿康源用科技引领发展,开启大健康产业新篇章

&#xff08;本台记者报&#xff09;近日&#xff0c;杭州有一家公司凭借深厚的科技研发实力与卓越的创新能力在大健康领域屡受好评&#xff0c;其研发的新品一经推出便成为行业热议。为了探寻该公司的经营秘诀&#xff0c;我们找到了这家公司——亿康源&#xff0c;并有幸与亿…

C/C++ Windows 与 Unix 平台上面使用 access 访问文件函数

在 Windows 与 Unix 平台上面得 C/C 之中&#xff0c;都标准提供了 access 函数得实现&#xff0c;只不过参数会有一些不同。 为了确保跨平台编译、兼容得通用、及一致性&#xff0c;所以人们需要显示定义&#xff1a; #if defined(_WIN32) #include <io.h> #else #incl…

防火墙组网与安全策略实验

实验要求&#xff1a; 实现&#xff1a; 防火墙接口配置&#xff1a; 所有接口均配置为三层接口 由于G1/0/3口下为vlan环境&#xff0c;所以防火墙需要配置子接口 &#xff1a; 交换机划分vlan分开生产区和办公区、配置trunk干道 &#xff1a; 安全策略&#xff1a; 生产区访…

深度学习概览

引言 深度学习的定义与背景 深度学习是机器学习的一个子领域&#xff0c;涉及使用多层神经网络分析和学习复杂的数据模式。深度学习的基础可以追溯到20世纪80年代&#xff0c;但真正的发展和广泛应用是在21世纪初。计算能力的提升和大数据的可用性使得深度学习在许多领域取得…

[C++] 由浅入深理解面向对象思想的组成模块

文章目录 (一) 类的默认成员函数(二) 构造函数构造函数的特征构造函数示例无参构造带参构造 冲突:全缺省参数的构造函数与无参构造函数 &#xff08;三&#xff09;析构函数特性析构函数的析构过程解析 &#xff08;四&#xff09;拷贝构造函数什么是拷贝构造&#xff1f;特性为…

Angular页面项目以HTTPS方式启动调试

在 Angular 项目中&#xff0c;可以使用 HTTPS 启动开发服务器进行调试。以下是具体步骤&#xff1a; 1、生成 SSL 证书 首先&#xff0c;需要生成 SSL 证书。可以使用 OpenSSL 来生成自签名证书。 在 Windows 上&#xff0c;可以通过 Git Bash 或其他终端执行以下命令&#x…

初始c语言(2)运算符与表达式

一 c语言提供的运算符类型 以上会后续介绍 二 现阶段我们掌握如下的基本操作符 注意&#xff01;计算机的除法只会保留整数部分&#xff08;若被除数未负则不同的软件取整的结果不唯一&#xff09; 三 自加&#xff08;&#xff09;自减&#xff08;--&#xff09;符号 若为…

Vue3 defineProps的使用

1.什么是defineProps defineProps是Vue3中的一种新的组件数据传递方式&#xff0c;可以用于在子组件中定义接收哪些父组件的props。当父组件的props发生变化时&#xff0c;子组件也会随之响应。 2.如何使用defineProps&#xff1f; 在子组件中可以使用defineProps声明该组件…

流媒体服务器(21)—— mediasoup 之媒体流score评分计算(二)

目录 前言 正文 1、期望收包数 2、实际收包数 3、丢包数 4、修复包数 5、重传包数 6、综合计算 结尾 《流媒体服务器》专栏总览丨蓄力计划_开源流媒体服务器对比-CSDN博客 前言 上一篇文章介绍了 mediasoup 有一套评估媒体传输通道优劣的机制,主要是通过 score 评分…

Jmeter-单用户单表查询千条以上数据,前端页面分页怎么做

这里写自定义目录标题 单用户单表查询千条以上数据 单用户单表查询千条以上数据 对于单用户查询千条以上数据&#xff0c;但是前端页面做了分页的情况下 可以直接把查询接口下的分页限制去掉&#xff0c;便可查询出当前页面查询条件下的全部数据 例如去掉如下内容&#xff1…

GESP CCF C++ 四级认证真题 2024年6月

第 1 题 下列代码中&#xff0c;输出结果是&#xff08; &#xff09; A. 12 24 24 12 B. 24 12 12 24 C. 12 12 24 24 D. 24 24 12 12 第 2 题 下面函数不能正常执行的是&#xff08;&#xff09; A. B. C. D. 第 3 题 下面程序…

AI Native时代:重塑人机交互与创作流程

随着2024年上海世界人工智能大会的圆满落幕&#xff0c;业界领袖们纷纷就AI应用的新机遇展开深入讨论。结合a16z播客中的观点&#xff0c;本文将探讨AI原生&#xff08;AI Native&#xff09;应用的几个关键特征&#xff0c;这些特征正在重新定义我们的工作方式和创作过程。 一…

0708,LINUX目录相关操作 + LINUX全导图

主要是冷气太足感冒了&#xff0c;加上少吃药抗药性差&#xff0c;全天昏迷&#xff0c;学傻了学傻了 01&#xff1a;简介 02&#xff1a; VIM编辑器 04&#xff1a;目录 05&#xff1a;文件 03&#xff1a;常用命令 06&#xff1a;进程 07&#xff1a;进程间的通信 cat t_c…

LeetCode 每日一题 2024/7/8-2024/7/14

记录了初步解题思路 以及本地实现代码&#xff1b;并不一定为最优 也希望大家能一起探讨 一起进步 目录 7/8 724. 寻找数组的中心下标7/9 3102. 最小化曼哈顿距离7/10 2970. 统计移除递增子数组的数目 I7/11 2972. 统计移除递增子数组的数目 II7/12 2974. 最小数字游戏7/13 301…

微信小程序毕业设计-青少年科普教学系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

ftp pool 功能分析及 golang 实现

本文探究一种轻量级的 pool 实现 ftp 连接。 一、背景 简要介绍&#xff1a;业务中一般使用较多的是各种开源组件&#xff0c;设计有点重&#xff0c;因此本文探究一种轻量级的 pool 池的思想实现。 期望&#xff1a;设置连接池最大连接数为 N 时&#xff0c;批量执行 M 个 F…

vs2017/2019串口Qt Serial Port/modbus使用报错

vs2017/2019 Qt Serial Port/modbus配置 /* * 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK2019 无法解析的外部符号 "__declspec(dllimport) public: __cdecl QModbusTcpClient::QModbusTcpClient(class QObject *)" (__imp_??…

基于javaScript的冒泡排序

目录 一.前言 二.设计思路和原理 三.源代码展示 四. 案例运行结果 一.前言 冒泡排序简而言之&#xff0c;就是一种算法&#xff0c;能够把一系列的数据按照一定的顺序进行排列显示&#xff08;从小到大或从大到小&#xff09;。例如能够将数组[5,4,3,2,1]中的元素按照从小到…

读书笔记 《软技能:代码之外的生存指南(第2版)》

关于时间 每天记录并追踪自己的时间&#xff0c;以便我能了解自己的时间都去哪儿了 每天高效工作的时间有多少 这点皮皮哥深有同感,我发现我其实真正效率高的是早上,我一般周末周日的早晨回起的很早,泡一杯挂耳☕️,就在我的mac本上开始了本周的创作,我喜欢这种感觉 关于营…