首页 > 其他分享 >main 方法的执行过程

main 方法的执行过程

时间:2023-03-30 20:56:38浏览次数:49  
标签:初始化 main Java 对象 虚拟机 内存 执行 方法 加载

参考 https://www.cnblogs.com/max-home/p/12270183.html
CS-Notes
JavaGuide

1、执行流程

public class Test {
    public static void main(String[] args) {
        Student student = new Student("abcd");
        student.sayName();
    }
}

class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public void sayName() {
        System.out.println(name);
    }
}

main 方法的执行过程如下:

  1. 首先对 Test 类进行加载(类加载过程看下面),执行 Test 类的加载过程,会生成一个App.class文件,系统会启动一个 JVM 进程
  2. 执行 Student student = new Student("abcd"); ,发现没有加载 Student 类,也就是说这个时候方法区中是没有 Student 类的信息的,所以需要对 Student 进行加载,把 Student 类的信息放到方法区
  3. 执行 new Student("abcd"); ,包括分配内存区域(堆中分配),初始化等,这个 Student 实例(student)持有 指向方法区中的 Student 类的类型信息 的引用
  4. 执行 sayName 时,先根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表(对象的头部),获得 sayName() 的字节码地址。再执行 sayName 方法

主要考察了类的加载过程和 new 分配内存的过程

总结:只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找。

Object obj = new Object();,简单的一句话,包含了三个动作:

  • Object obj 在栈内存中创建了一个名为 obj 的 Object 类型引用变量,它可以指向 Object 类型的对象实体;
  • new Object() 在堆内存中创建了一个 Object 类型对象实体;
  • 最后用一个“=”将对象引用 obj 指向对象实体 new Object()。

2、类加载过程

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。

类的生命周期

img

包括以下 7 个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

类加载过程

包含加载、验证、准备、解析和初始化这 5 个阶段

1、加载

这里的加载指的是类加载的一个阶段,主要完成以下任务:

  • 通过类的完全限定名称获取定义该类的二进制字节流。
  • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
  • 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。

其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

全限定类名:就是类名全称,带包路径的用点隔开,例如: java.lang.String

2、验证

确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。在验证阶段,会进行如下四个检验:

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

3、准备

类变量是被 static 修饰的变量(在编译阶段将常量放入到方法区的常量池中),准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存

实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

在对 static 类变量初始化时,int 类型的变量被初始化为 0,如下面代码,value 的值被初始化为 0 而非 123

public static int value = 123;

但若类变量被 final 修饰为常量,那么它在编译时将会为 value 生成 ConstantValue 属性,在准备阶段会根据 ConstantValue 将 value 设置为 123。

public static final int value = 123;

4、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析过程主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行。

  • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,因为引用的目标不一定已经加载到内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

5、初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。注意,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。

3、对象创建过程

3.1 对象的创建

下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
Java创建对象的过程

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式“指针碰撞”“空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

  • 指针碰撞 :
    • 适用场合 :堆内存规整(即没有内存碎片)的情况下。
    • 原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
    • 使用该分配方式的 GC 收集器:Serial, ParNew
  • 空闲列表 :
    • 适用场合 : 堆内存不规整的情况下。
    • 原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
    • 使用该分配方式的 GC 收集器:CMS

内存分配并发问题(补充内容,需要掌握)

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

3.2 对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

Hotspot 虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据(方法表指针、哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

类元数据是描述一个Java类的数据,包括类名、字段名、方法名、访问修饰符、返回值类型、参数类型、注解等信息。这些元数据信息被存储在Java虚拟机的方法区中

方法表指针:指向对象的方法表,包含了对象所属类的所有方法的信息。

对象的标记信息:用于垃圾回收,标记对象是否可达等信息

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.3 对象的访问定位

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象(对象在内存中的布局)。对象的访问方式由虚拟机实现而定,目前主流的访问方式有①使用句柄②直接指针两种:

  1. 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

    对象的访问定位-使用句柄

  2. 直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

对象的访问定位-直接指针

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

标签:初始化,main,Java,对象,虚拟机,内存,执行,方法,加载
From: https://www.cnblogs.com/pxypxy/p/17274215.html

相关文章

  • 中文设置成粗体的方法
    在xml文件中使用android:textStyle="bold"可以将英文设置成粗体,但是不能将中文设置成粗体,将中文设置成粗体的方法是:TextViewtv=(TextView)findViewById(R.id.TextView01);TextPainttp=tv.getPaint();tp.setFakeBoldText(true);......
  • java.lang.String中的trim()方法的…
    String.Trim()方法到底为我们做了什么,仅仅是去除字符串两端的空格吗?一直以为Trim()方法就是把字符串两端的空格字符给删去,其实我错了,而且错的比较离谱。首先我直接反编译String类,找到Trim()方法:publicstringTrim(){returnthis.TrimHelper(WhitespaceChars,2);}Trim......
  • CPU内部的奥秘:代码是如何被执行的?
    我们是袋鼠云数栈UED团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。本文作者:景明我们以一段C代码为例,来看一下代码被编译成二进制可执行程序之后,是如何被CPU执行的。在这段代码中,只是做了非常简单的加法操作,......
  • Linux系统把时间类型值转换为数值型的方法是什么?
    在实际工作中,我们往往会遇到各式各样的需求,今天老男孩教育小编给大家介绍一下,如何把时间类型值转换为数值类型,以下是详细的内容:1.取子串函数格式:substr(c,n1.n2)功能:取字符串C第n1个字符起的n2个字符.返回值类型是字符型.例:取姓名字符串中的姓.store"......
  • Linux系统中创建文件常用的方法!
    众所周知,在Windows系统中可以直接右键新建文件,而在Linux系统中,想要创建文件并非易事,需要通过执行命令来完成,那么Linux系统中创建文件常用的方法有哪些?本文为大家介绍一下Linux系统下创建文件的8种方法,快来了解一下吧。1、重定向符号>通常重定向符号可以创建一个0kb的......
  • DBSAT脚本快速收集方法
    DBSAT是Oracle官方提供的脚本,用于数据库的安全评估检查,用户可以放心下载使用。下载链接具体参见MOS:OracleDatabaseSecurityAssessmentTool(DBSAT)(DocID2138254.1)下面介绍DBSAT脚本快速收集方法:1.上传dbsat脚本并解压到指定目录2.使用dbsat采集必要信息3.生成报......
  • 谷歌秒收录方法,谷歌不收录怎么办
    作为全球最大的搜索引擎之一,谷歌在网站运营者心中占有举足轻重的地位。快速被谷歌收录意味着更高的知名度和更多的访问量。因此,许多网站运营者都关注谷歌秒收录方法。本文将为您介绍谷歌秒收录的方法以及遇到谷歌不收录时该如何应对。一、谷歌秒收录方法要实现谷歌秒收录,可以尝试以......
  • DTCloud异常与解决方法一览
    Dtcloud的异常抛出往往令人摸不着头脑,本文记录了一些本人在使用过程中遇到的一些异常以及解决的方式,配上异常截图方便日后可以”按图索骥”。目前所记录的问题可能还比较片......
  • 网络安全-Burp Suite安装和使用方法
    一、安装准备1、安装jdkjdk是必须有的,装jdk的过程就不说了,上官网下载,配置环境变量,java-version,如下:我电脑装了jdk8和JDK17,可能会出现jdk版本过高需要安装jdk8的情况,可......
  • 制作Macbook和苹果手机模型放在PPT里展示得方法
    需要如下素材放进PPT里,用来展示我们得客户端和移动端得显示,最终效果如下: 首先对于移动端得苹果手机展示得最终效果如下  Surfacepro截全屏快捷键是Fn+Space 1......