Java对象之生

简介: 内存、性能是程序永恒的话题,实际开发中关于卡顿、OOM也经常是打不完的两只老虎,关于卡顿、OOM的定位方法和工具比较多,这篇文章也不打算赘述了,本章主要是来整理一下JVM的内存模型以及Java对象的生与死。 生存空间(内存区域) Java程序运行在JVM之上,如果Java对象是一个有血有肉的生灵

内存、性能是程序永恒的话题,实际开发中关于卡顿、OOM也经常是打不完的两只老虎,关于卡顿、OOM的定位方法和工具比较多,这篇文章也不打算赘述了,本章主要是来整理一下JVM的内存模型以及Java对象的生与死。

生存空间(内存区域)

Java程序运行在JVM之上,如果Java对象是一个有血有肉的生灵,那么它生存环境是怎样的呢?很多人把Java内存分为堆内存(Heap)和栈内存(Stack),实际上这种划分比较出粗糙和片面。比较细致的划分是这样的:

内存区域

分为程序计数器、虚拟机栈、本地方法栈、方法区和堆。

程序计数器

理解程序计数器之前,我们先来理解一下线程的并行:

线程并行

感官上两条线程是同时执行的,但是在一个CPU上实际上是切换轮流执行的,在同一时刻,CPU不会同时执行一个线程,线程的“并行”通过CPU的高速切换来实现的。

线程切换执行

现在的问题是:线程切换回来之后是如何确定当前线程之前执行的位置和状态?
答案是:使用程序计数器。每一条线程需要一个独立的程序计数器来记录线程执行的状态,各个线程之间的计数器互不影响,所以程序计数器是线程私有的内存区域。所以说线程越多开销越大。

Java 虚拟机栈

虚拟机栈也是线程私有的内存区域,用于存储方法执行过程中的局部变量、操作数栈、方法出入口等信息。线程执行每一个方法都会创建一个栈帧,栈帧中就包含了局部变量表、操作数栈、方法出入口等信息,局部变量表存放基本类型的临时变量,包含boolean、byte、char、short、int、float、long、doubble和对象应用类型(reference,对象地址)。例如下面一段代码:

_

假如某线程执行方法a(),那么该线程的栈内存大概是这样的:

_1

假如方法执行到15行,方法b()的栈帧创建并入栈:

_2

执行完15行到16行,方法b的栈帧出栈:

_1

Java虚拟机栈,就是我们常说的栈内存,如果线程请求的深度超过虚拟机运行的深度就会抛出 StackOverflowError 的异常。

本地方法栈

本地方法栈和虚拟机栈是类似的,虚拟机栈是为虚拟机执行 Java 代码服务,本地方法栈是为虚拟机使用Native 方法服务。在 HotSpot 虚拟机中本地方法栈也会抛出 StackOverflowError 的异常。

对象栖息之地-Java堆

堆内存,大部分人都比较熟悉了,Java 堆是虚拟机中站内存最大的一块区域,是所有对象实例的土壤。堆内存是线程共享的,所有线程产生的对象都要在这块区域中划分内存。

_

Java 堆是垃圾回收器主要管理的区域,又叫 “GC堆”,从垃圾回收器的角度来看 Java 堆又分为新生代和老生代(和垃圾回收器的分代算法相关,见:《《 Java 对象之死》);从线程共享的情况来看,Java 堆还可能划分为每个线程划分一个下的内存区域作为线程使用的缓存区域(Thread Local Allocation Buffer,简称TLAB,后面会进一步说明)。无论怎么划分,当堆中没有足够的空间来存放新的实例时就会抛出 OutOfMemoryError(OOM) 异常。

方法区

和 Java 堆一样,也是线程共享的,这部分内存用于存储类信息、常量、静态变量和即时编译的代码数据。

方法区

类型信息,指的是类型和其指针的对应关系,在创建和访问对象的时候得到,用于查找区分类型。例如有两个类,class A 和 class B ,假如这两个类都加载了,那么方法区大概是这样记录的:

_

常量中还有一部分叫做运行时常量池,这部分并不是在编译和加载期间产生的,而是运行期间产生的,例如:

String a = "abc";
String b = "def";
String c = a+b;

上述代码产生的 "abc" 和 "def" 会被存放到运行时常量池中。方法区的内存使用超过限制会抛出 StackOverflowError 的异常。

对象的“出生”

前面介绍了对象的生存环境:内存的区域和各个区域的作用。接下来说说对象的“出生”,一个 new 关键字到底包含了那些“不为人知”的过程?

对象创建过程

当程序执行遇到一个 new 关键字之后,首先会去方法区参赛定位到这个类符合的引用,查询到是什么类之后再去检查这个类有没有被加载,如果没有执行类加载过程,类加载过程也是一个比较复杂的过程,这里不展开论述。

在类加载完成(或者已经加载过)之后,接下来就开始为新的对象分配内存了,为对象分配内存一般有两种方式:“指针碰撞”和“空闲列表”。如果 Java 堆中正在使用的内存和空闲内存分别都是连续的规整的,中间临界点存放一个指针作为分界标识,为新对象分配内存的时候该指针移动和这个对象大小相等的一段距离就行了,所以叫“指针碰撞”。如果 Java 堆中正在使用的内存和空闲内存不是连续的,那么就没有办法是用指针碰撞这种方式分配内存了,虚拟机就必须维护一个列表来记录那些内存是空闲的,在分配内存的时候就冲空闲列表中找一份足够大的空间类分配给对象,这种方式称为“空闲列表”。是用“指正碰撞”还是“空闲列表”取决 Java 堆内存是否规整,而 Java 堆内存是否规整取决于垃圾回收器使用的回收算法(参考《 Java 对象之死》)是否带有压缩功能。

前面提到 Java 堆内存是线程共享的,多线程同时在堆内存中分配内存,就要保证内存划分的原子行。如何保证内存分配的线程安全?一般有两种方案,第一种方案就是实用同步控制处理,第二种实用 Thread Local Allocation Buffer(TLAB)方式。第一种用多说了很好理解,不用过多解释。TLAB,即线程本地缓冲,就是预先为每个线程分配一个 TLAB ,当线程需要使用内存的时候就在自己的 TLAB 上分配就好了。

内存分配玩之后需要对对象进行必要的设置,例如对象类型信息、元数据
对象哈希码、GC年龄等。

对象长啥样子

通过上面的介绍我们知道对象的“出生”过程了,对象“长啥样”呢?对象在内存中可以分为3个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括哈希码、GC代年龄、锁状态、线程持有锁、偏向线程ID、时间戳等,另外还包含类型指针。类型指针的作用是JVM使用这个类型指针来查找这个对象是属于那个类的。实例数据部分才是真正存储对象信息那部分。对齐补充部分不是必然存在的,仅仅用于站位,因为 HotSpot VM 要求对象的大小必须是8字节的整数倍,对象头的长度已经是8字节的整数倍,实例数据大小不固定,所以使用Padding部分来填充。

对象布局

和它握手

对象已经创建了,并且已经知道它的大概模样,如何访问它呢。在 HotSpot 中使用直接指针访问的。前面介绍过虚拟机栈,方法执行过程中创建的栈帧中局部变量表中存储了方法的局部变量,包含基本类型和引用类型,其中引用类型其实就是一个指向对象内存地址的指针。

对象访问

Java 对象之死

《 Java 对象之死》

目录
相关文章
|
11天前
|
存储 Java
java的对象详解
在Java中,对象是根据类模板实例化的内存实体,具有唯一标识符、属性及行为。通过`new`关键字实例化对象并用构造方法初始化。变量存储的是对象引用而非对象本身,属性描述对象状态,方法定义其行为。Java利用垃圾回收机制自动处理不再使用的对象内存回收,极大地简化了对象生命周期管理,同时对象具备封装、继承和多态性,促进了代码的重用与模块化设计。这使得Java程序更易于理解、维护和扩展。
|
7天前
|
Java 编译器
Java——类与对象(继承和多态)
本文介绍了面向对象编程中的继承概念,包括如何避免重复代码、构造方法的调用规则、成员变量的访问以及权限修饰符的使用。文中详细解释了继承与组合的区别,并探讨了多态的概念,包括向上转型、向下转型和方法的重写。此外,还讨论了静态绑定和动态绑定的区别,以及多态带来的优势和弊端。
20 9
Java——类与对象(继承和多态)
|
7天前
|
SQL Java 编译器
Java——类与对象(封装)
封装是面向对象编程中的概念,指将数据(属性)和相关操作(方法)组合成独立单元(类),使外部无法直接访问对象的内部状态,只能通过提供的方法进行交互,从而保护数据安全。例如,手机将各种组件封装起来,只暴露必要的接口供外部使用。实现封装时,使用`private`关键字修饰成员变量,并提供`get`和`set`方法进行访问和修改。此外,介绍了包的概念、导入包的方式及其注意事项,以及`static`关键字的使用,包括静态变量和方法的初始化与代码块的加载顺序。
18 10
Java——类与对象(封装)
|
7天前
|
Java C语言
Java——类与对象
这段内容介绍了Java中的类和对象、`this`关键字及构造方法的基本概念。类是对现实世界事物的抽象描述,包含属性和方法;对象是类的实例,通过`new`关键字创建。`this`关键字用于区分成员变量和局部变量,构造方法用于初始化对象。此外,还介绍了标准JavaBean的要求和生成方法。
18 9
Java——类与对象
|
8天前
|
存储 Java
Java的对象和类的相同之处和不同之处
在 Java 中,对象和类是面向对象编程的核心。
|
2天前
|
Java
Java实现:将带时区的时间字符串转换为LocalDateTime对象
通过上述方法,你可以将带时区的时间字符串准确地转换为 `LocalDateTime`对象,这对于处理不需要时区信息的日期和时间场景非常有用。
39 4
|
2天前
|
SQL Java 关系型数据库
在Java中,创建数据源对象
在Java中,创建数据源对象
9 1
|
8天前
|
存储 Java
Java编程中的对象序列化与反序列化
【9月更文挑战第12天】在Java的世界里,对象序列化与反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何通过实现Serializable接口来标记一个类的对象可以被序列化,并探索ObjectOutputStream和ObjectInputStream类的使用,以实现对象的写入和读取。我们还将讨论序列化过程中可能遇到的问题及其解决方案,确保你能够高效、安全地处理对象序列化。
|
13天前
|
Java
Java 对象和类
在Java中,**类**(Class)和**对象**(Object)是面向对象编程的基础。类是创建对象的模板,定义了属性和方法;对象是类的实例,通过`new`关键字创建,具有类定义的属性和行为。例如,`Animal`类定义了`name`和`age`属性及`eat()`、`sleep()`方法;通过`new Animal()`创建的`myAnimal`对象即可调用这些方法。面向对象编程通过类和对象模拟现实世界的实体及其关系,实现问题的结构化解决。
|
16天前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。