Java虚拟机调用Java主类的main()方法

目录

鸠摩

在前一篇 第1篇关于Java虚拟机HotSpot,开篇说的简单些 中介绍了call_static()call_virtual()等函数的作用,这些函数会调用JavaCalls::call()函数。我们看Java类中main()方法的调用,

调用栈如下:

JavaCalls::call_helper() at javaCalls.cpp
os::os_exception_wrapper() at os_linux.cpp
JavaCalls::call() at javaCalls.cpp
jni_invoke_static() at jni.cpp
jni_CallStaticVoidMethod() at jni.cpp
JavaMain() at java.c
start_thread() at pthread_create.c
clone() at clone.S

这是Linux上的调用栈,通过JavaCalls::call_helper()函数来执行main()方法。栈的起始函数为clone() ,这个函数会为每个进程(Linux进程对应着Java线程)创建单独的栈空间,这个栈空间如下图所示。

在Linux操作系统上,栈的地址向低地址延伸,所以未使用的栈空间在已使用的栈空间之下。图中的每个蓝色小格表示对应方法的栈帧,而栈就是由一个一个的栈帧组成。native方法的栈帧、Java解释栈帧和Java编译栈帧都会在***域中分配,所以说他们寄生在宿主栈中,这些不同的栈帧都紧密的挨在一起,所以并不会产生什么空间碎片这类的问题,而且这样的布局非常有利于进行栈的遍历。上面给出的调用栈就是通过遍历一个一个栈帧得到的,遍历过程也是栈展开的过程。后续对于异常的处理、运行jstack打印线程堆栈、GC查找根引用等都会对栈进行展开操作,所以栈展开是后面必须要介绍的。

下面我们继续看JavaCalls::call_helper()函数,这个函数中有个非常重要的调用,如下:

// do call
{
    JavaCallWrapper link(method, receiver, result, CHECK);
    {
      HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner
      StubRoutines::call_stub()(
         (address)&link,
         result_val_address,
         result_type,
         method(),
         entry_point,
         args->parameters(),
         args->size_of_parameters(),
         CHECK
      );

      result = link.result();
      // Preserve oop return value across possible gc points
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
}

调用StubRoutines::call_stub()函数返回一个函数指针,然后通过函数指针来调用函数指针指向的函数。通过函数指针调用和通过函数名调用的方式一样,这里我们需要清楚的是,调用的目标函数仍然是C/C++函数,所以由C/C++函数调用另外一个C/C++函数时,要遵守调用约定。这个调用约定会规定怎么给被调用函数(Callee)传递参数,以及被调用函数的返回值将存储在什么地方。

下面我们就来简单说说Linux X86架构下的C/C++函数调用约定,在这个约定下,以下寄存器用于传递参数:

  • 第1个参数:rdi c_rarg0
  • 第2个参数:rsi c_rarg1
  • 第3个参数:rdx c_rarg2
  • 第4个参数:rcx c_rarg3
  • 第5个参数:r8 c_rarg4
  • 第6个参数:r9 c_rarg5

在函数调用时,6个及小于6个用如下寄存器来传递,在HotSpot中通过更易理解的别名c_rarg*来使用对应的寄存器。如果参数超过六个,那么程序将会用调用栈来传递那些额外的参数。

数一下我们通过函数指针调用时传递了几个参数?8个,那么后面的2个就需要通过调用函数(Caller)的栈来传递,这两个参数就是args->size_of_parameters()CHECK(这是个宏,扩展后就是传递线程对象)。

所以我们的调用栈在调用函数指针指向的函数时,变为了如下状态:

右边是具体的call_helper()栈帧中的内容,我们把threadparameter size压入了调用栈中,其实在调目标函数的过程还会开辟新的栈帧并在parameter size后压入返回地址和调用栈的栈底,下一篇我们再详细介绍。先来介绍下JavaCalls::call_helper()函数的实现,我们分3部分依次介绍。

1、检查目标方法是否"首次执行前就必须被编译”,是的话调用JIT编译器去编译目标方法;

代码实现如下:

void JavaCalls::call_helper(
 JavaValue* result,
 methodHandle* m,
 JavaCallArguments* args,
 TRAPS
) {
  methodHandle method = *m;
  JavaThread* thread = (JavaThread*)THREAD;
  ...

  assert(!thread->is_Compiler_thread(), "cannot compile from the compiler");
  if (CompilationPolicy::must_be_compiled(method)) {
    CompileBroker::compile_method(method, InvocationEntryBci,
                                  CompilationPolicy::policy()->initial_compile_level(),
                                  methodHandle(), 0, "must_be_compiled", CHECK);
  }
  ...
}

对于main()方法来说,如果配置了-Xint选项,则是以解释模式执行的,所以并不会走上面的compile_method()函数的逻辑。后续我们要研究编译执行时,可以强制要求进行编译执行,然后查看执行过程。

2、获取目标方法的解释模式入口from_interpreted_entry,也就是entry_point的值。获取的entry_point就是为Java方法调用准备栈桢,并把代码调用指针指向method的第一个字节码的内存地址。entry_point相当于是method的封装,不同的method类型有不同的entry_point

接着看call_helper()函数的代码实现,如下:

address entry_point = method->from_interpreted_entry();

调用methodfrom_interpreted_entry()函数获取Method实例中_from_interpreted_entry属性的值,这个值到底在哪里设置的呢?我们后面会详细介绍。

3、调用call_stub()函数,需要传递8个参数。这个代码在前面给出过,这里不再给出。下面我们详细介绍一下这几个参数,如下:

  • (1)link 此变量的类型为JavaCallWrapper,这个变量对于栈展开过程非常重要,后面会详细介绍;
  • (2)result_val_address 函数返回值地址;
  • (3)result_type 函数返回类型;
  • (4)method() 当前要执行的方法。通过此参数可以获取到Java方法所有的元数据信息,包括最重要的字节码信息,这样就可以根据字节码信息解释执行这个方法了;
  • (5)entry_point HotSpot每次在调用Java函数时,必然会调用CallStub函数指针,这个函数指针的值取自_call_stub_entry,HotSpot通过_call_stub_entry指向被调用函数地址。在调用函数之前,必须要先经过entry_point,HotSpot实际是通过entry_point从method()对象上拿到Java方法对应的第1个字节码命令,这也是整个函数的调用入口;
  • (6)args->parameters() 描述Java函数的入参信息;
  • (7)args->size_of_parameters() 参数需要占用的,以字为单位的内存大小
  • (8)CHECK 当前线程对象。

到此这篇关于Java虚拟机调用Java主类的main()方法的文章就介绍到这了,更多相关Java虚拟机调用main()方法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java虚拟机原理:Class字节码二进制文件分析

    目录 一.字节码文件 与 JVM 二.字节码文件示例 三.字节码文件二进制结构分析 1.魔数 2.次版本号 3.主版本号 4.常量池个数 总结 一.字节码文件 与 JVM Java 源码编译成 Class 字节码 ; Java 虚拟机 可以被认为是一个 解释器 , 解释编译后的 Class 字节码文件 , 最后在不同的操作系统中运行 ; Android 虚拟机 不是 Java 规范的 虚拟机 , 有一些根据嵌入式设备进行的定制的实现 ; Class 字节码 本质上就是 二进制数据 , 运行时 ,

  • 关于Java虚拟机HotSpot

    我们写的主类中的main()方法是如何被Java虚拟机调用到的?在Java类中的一些方法会被由C/C++编写的HotSpot虚拟机的C/C++函数调用,不过由于Java方法与C/C++函数的调用约定不同,所以并不能直接调用,需要JavaCalls::call()这个函数辅助调用.(我把由C/C++编写的叫函数,把Java编写的叫方法,后续也会延用这样的叫法)如下图所示. 从C/C++函数中调用的一些Java方法主要有: (1)Java主类中的main()方法: (2)Java主类装载时,调用Ja

  • 浅析java程序入口main()方法

    main()方法的方法签名 public static void main(String[] args) 方法签名讲解 public修饰符:java类由java虚拟机(JVM)调用,为了没有限制可以自由的调用,所以采用public修饰符. static修饰符:JVM调用这个主方法时肯定不是先创建这个主类的对象,再通过对象来调用方法,而是直接通过该类来调用这个方法,因此需要使用static修饰符修饰这个类. void返回值:主方法被JVM调用,将返回值返回给JVM没有任何意义,因此该方法没有返回值

  • 详细讲解Java中的main()方法

    前言 JAVA中的主函数是我们再熟悉不过的了,相信每个学习过JAVA语言的人都能够熟练地写出这个程序的入口函数,但对于主函数为什么这么写,其中的每个关键字分别是什么意思,可能就不是所有人都能轻松地答出来的了.我也是在学习中碰到了这个问题,通过在网上搜索资料,并加上自己的实践终于有了一点心得,不敢保留,写出来与大家分享. Java中的main()方法 java虚拟机通过main方法找到需要启动的运行程序,并且检查main函数所在类是否被java虚拟机装载.如果没有装载,那么就装载该类,并且装载所有

  • 深入理解Java虚拟机 JVM 内存结构

    目录 前言 JVM是什么 JVM内存结构概览 运行时数据区 程序计数器 Java虚拟机栈 本地方法栈 方法区 运行时常量池 Java堆 直接内存 前言 JVM是Java中比较难理解和掌握的一部分,也是面试中被问的比较多的,掌握好JVM底层原理有助于我们在开发中写出效率更高的代码,可以让我们面对OutOfMemoryError时不再一脸懵逼,可以用掌握的JVM知识去查找分析问题.去进行JVM的调优.去让我们的应用程序可以支持更高的并发量等......总之一句话,学好JVM很重要! JVM是什么 J

  • Java虚拟机运行时栈的栈帧

    目录 Java虚拟机栈概述 局部变量表 操作数栈 动态连接 方法的返回地址 结合javap命令理解栈帧 Java虚拟机栈概述 Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同.虚拟机栈描述的是Java方法执行的内存模型:栈帧(Stack Frame)是用于支持Java虚拟机进行方法调用和执行的数据结构,它是虚拟机栈中的栈元素.每个方法在执行的同到都会创建一个栈帧用于存储局部变量表.操作数栈.动态链接.方法出口等信息. 在编译程序代码的

  • java 打印一字符串,并在main()方法内调用它

    这个是写的Java的第一个程序. 这个就是java 语言的基本框架了的吧 package com.itheima; /** * 1. 编写一个方法(名字自定,但要符合Java编码规范),方法内打印一字符串,并在main()方法内调用它. * @author 281167413@qq.com */ public class Test1 { public static void main(String[] args) { System.out.println("My first station! i

  • java内存模型jvm虚拟机简要分析

    目录 主内存和工作内存 内存间的交互操作 原子性.可见性.有序性 原子性 可见性 有序性 主内存和工作内存 Java 内存模型规定了所有的变量都存储在主内存中, 每条线程有自己的工作内存 线程的工作内存中保存了被该线程使用的变量的主内存副本, 线程对变量的所有操作 (读取.赋值等) 都必须在工作内存中进行, 而不能直接读写主内存中的数据 不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成 内存间的交互操作 原子性.可见性.有序性 Java 内存模型是围绕

  • Java虚拟机调用Java主类的main()方法

    目录 鸠摩 在前一篇 第1篇关于Java虚拟机HotSpot,开篇说的简单些 中介绍了call_static() .call_virtual()等函数的作用,这些函数会调用JavaCalls::call()函数.我们看Java类中main()方法的调用, 调用栈如下: JavaCalls::call_helper() at javaCalls.cpp os::os_exception_wrapper() at os_linux.cpp JavaCalls::call() at javaCalls

  • Java 找不到或无法加载主类的修复方法

    有时,当我们运行Java程序时,我们可能会看到"找不到或无法加载主类".原因很容易猜测:JVM找不到主类并给出了这个错误.但是为什么不能呢? 在本文中,我们将讨论找不到主类的可能原因.另外,我们将看看如何修复它们. 示例程序 我们将从HelloWorld程序开始: public class HelloWorld { public static void main(String[] args) { System.out.println("Hello world..!!!&quo

  • Android Studio无法执行Java类的main方法问题及解决方法

    Android Studio升级到哦最新版3.6.1后,新建了个项目,发现无法执行Java类的main方法.试了网上的各种方法,比如切换gradle离线模式.gradle.properties中添加android.enableAapt2=false等,我还尝试了重新情况Gradle缓存.重新下载gradle等方式,都没用. 环境 android studio版本:3.6.1 gradle版本:5.6.4 gradle插件版本:3.6.1 错误提示 11:41:35 PM: Executing t

  • java报错:找不到或无法加载主类的解决方法简单粗暴

    当我们在windows系统下安装完jdk时,测试案例HelloWorld:运行java命令时报错:找不到或无法加载主类 解决方法: 1.首先检查是否编译通过,生成了.class字节码文件 如果没有生成.class字节码文件,则需要执行javac编译命令编译源文件. 执行命令javac H:\javatest\HelloWorld.java(javac表示jdk内置编译命令:H:\javatest\HelloWorld.java表示源文件所在路径,这里我的测试源文件是位于H盘下,自己决定) 2.如

  • Java实现调用jython执行python文件的方法

    本文实例讲述了Java实现调用jython执行python文件的方法.分享给大家供大家参考,具体如下: 在web开发时候,经常在web环境使用本地环境的第三方库什么的,本文讲解java如何执行python文件. 网上说方法有三种,其实也就两种,下面着中介绍第二种通过(jython). 方法一 java.lang.Runtime Runtime rt = Runtime.getRuntime(); try { Process proc = rt.exec("python /tmp/test.py&

  • Java数据库操作库DButils类的使用方法与实例详解

    DbUtils是Javar的一个为简化JDBC操作类库 commons-dbutils是Apache组织提供的一个开源JDBC工具类库,它是对JDBC的简单封装,学习成本极低,并且使用dbutils能极大简化jdbc编码的工作量,同时也不会影响程序的性能.因此dbutils成为很多不喜欢hibernate的公司的首选. 整个dbutils总共才3个包: org.apache.commons.dbutils (该包中的类主要帮助我们更便捷的操作JDBC) org.apache.commons.db

  • Java线程创建与Thread类的使用方法

    目录 1.线程与Thread类 1.1操作系统中的线程与Java线程 1.1.1线程与Thread类 1.1.2Thread类的构造方法 1.1.3启用java线程必会的方法 1.2第一个Java多线程程序 1.3使用Runnable对象创建线程 1.4使用内部类创建线程 1.5使用Lambda表达式创建线程 1.6多线程并发执行简单演示 1.7多线程并发执行的优势 2.Thread类的常用属性与方法 2.1Thread类中的重要属性 2.2Thread类中常用方法总结 2.2.1常用方法 2.

  • 浅谈java运用注解实现对类中的方法检测的工具

    创建自定义注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { } 建立测试类 public class UserTest { @Test public void testInsert() { User user = null; System.out.println(user.getUsername()); } @Test public void testQuery(

  • Intellij IDEA命令行执行java无法加载主类解决方案

    思路一:环境配置中,CLASSPATH配置的最前面加入".;","."表示当前目录中搜索 思路二 1.命令行进入到.java所在目录 2.通过 javac d . [java文件名(带.java后缀)] 编译java文件 3.通过 java [package后的路径名].[java文件名(不带.java后缀)] 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们.

随机推荐