郑州做定制网站的公司,如何建立网站快捷,简单的品牌创意设计公司,wordpress网站云备份系列文章目录
第一章 Java核心篇之JVM探秘#xff1a;内存模型与管理初探
第二章 Java核心篇之JVM探秘#xff1a;对象创建与内存分配机制
第三章 Java核心篇之JVM探秘#xff1a;垃圾回收算法与垃圾收集器
第四章 Java核心篇之JVM调优实战#xff1a;Arthas工具使用及…系列文章目录
第一章 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常见引用类型
4finalize()方法最终判定对象是否存活
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都有自己的缓存为了保持缓存之间的一致性处理器使用了一种称为MESIModified, 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会进行如下操作
尝试压缩老年代回收部分空间。如果压缩后仍然不足将触发Full GC清理整个堆内存包括老年代和永久代在JDK 8中是方法区。如果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互相引用如果没有垃圾收集器将导致它们都无法被回收}
}
在引用计数法中nodeA和nodeB互相引用对方即使它们不再被任何外部引用持有但由于它们相互之间的引用它们的引用计数都不会降到0因此按照引用计数法这两个对象将永远不会被回收导致内存泄漏。
2可达性分析算法
这是JVM中常用的垃圾回收算法。它基于一个基本思想通过一系列称为“GC Roots”的根对象作为起始点从这些根对象向下搜索搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时即可判定此对象是不可达的即不可能再被使用。
GC Roots通常包括
正在执行的方法中声明的局部变量对象。方法调用栈中引用的对象。本地方法栈中JNINative方法引用的对象。Java虚拟机内部引用的对象如基本类型的Class对象或者常量池中的引用。
3常见引用类型
Java中定义了四种强度不同的引用类型它们分别是强引用Strong Reference、软引用Soft Reference、弱引用Weak Reference和虚引用Phantom Reference。
强引用是最常用的引用类型只要强引用存在垃圾回收器就不会回收掉对象。软引用用于描述还有用但非必需的对象。当系统将要发生内存溢出异常前会把这些对象列进回收范围之中进行第二次回收如果这次回收后还没有足够的内存才会抛出内存溢出异常。弱引用比软引用的强度更弱一些被弱引用关联的对象只能生存到下一次垃圾回收发生之前。虚引用也称为幽灵引用或幻影引用一个对象是否有虚引用的存在完全不会对其生存时间构成影响也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
4finalize()方法最终判定对象是否存活
在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的优势构建高效稳定的应用系统。