导航:首页 > 源码编译 > 编译器逃逸分析

编译器逃逸分析

发布时间:2022-10-09 07:42:12

A. 编译器的工作分为哪几个阶段

编译器就是一个普通程序,没什么大不了的
什么是编译器?

编译器是一个将高级语言翻译为低级语言的程序。

首先我们一定要意识到编译器就是一个普通程序,没什么大不了的。

在没有弄明白编译器如何工作之前你可以简单的把编译器当做一个黑盒子,其作用就是输入一个文本文件输出一个二进制文件。

基本上编译器经过了以下几个阶段,等等,这句话教科书上也有,但是我相信很多同学其实并没有真正理解这几个步骤到底在说些什么,为了让你彻底理解这几个步骤,我们用一个简单的例子来讲解。

假定我们有一段程序:

while (y < z) {
int x = a + b;
y += x;
}
那么编译器是怎样把这一段程序人类认识的程序转换为CPU认识的二进制机器指令呢?

提取出每一个单词:词法分析
首先编译器要把源代码中的每个“单词”提取出来,在编译技术中“单词”被称为token。其实不只是每个单词被称为一个token,除去单词之外的比如左括号、右括号、赋值操作符等都被称为token。

从源代码中提取出token的过程就被称为词法分析,Lexical Analysis。

经过一遍词法分析,编译器得到了以下token:

T_While while
T_LeftParen (
T_Identifier y
T_Less <
T_Identifier z
T_RightParen )
T_OpenBrace {
T_Int int
T_Identifier x
T_Assign =
T_Identifier a
T_Plus +
T_Identifier b
T_Semicolon ;
T_Identifier y
T_PlusAssign +=
T_Identifier x
T_Semicolon ;
T_CloseBrace }
就这样一个磁盘中保存的字符串源代码文件就转换为了一个个的token。

这些token想表达什么意思:语法分析
有了这些token之后编译器就可以根据语言定义的语法恢复其原本的结构,怎么恢复呢?

原来,编译器在扫描出各个token后根据规则将其用树的形式表示出来,这颗树就被称为语法树。

语法树是不是合理的:语义分析
有了语法树后我们还要检查这棵树是不是合法的,比如我们不能把一个整数和一个字符串相加、比较符左右两边的数据类型要相同,等等。

这一步通过后就证明了程序合法,不会有编译错误。

B. java 异常at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:20)

先了解SOF的生成原因,以下为转载,可以自己网络。

Hotspot虚拟机并不区分VM栈和本地方法栈,因此-Xoss参数实际上是无效的,栈容量只由-Xss参数设定。关于VM栈和本地方法栈在VM Spec描述了两种异常:StackOverflowError与OutOfMemoryError,当栈空间无法继续分配分配时,到底是内存太小还是栈太大其实某种意义上是对同一件事情的两种描述而已,在笔者的实验中,对于单线程应用尝试下面3种方法均无法让虚拟机产生OOM,全部尝试结果都是获得SOF异常。

1.使用-Xss参数削减栈内存容量。结果:抛出SOF异常时的堆栈深度相应缩小。

2.定义大量的本地变量,增大此方法对应帧的长度。结果:抛出SOF异常时的堆栈深度相应缩小。

3.创建几个定义很多本地变量的复杂对象,打开逃逸分析和标量替换选项,使得JIT编译器允许对象拆分后在栈中分配。结果:实际效果同第二点。

VM栈和本地方法栈OOM测试(仅作为第1点测试程序)

/**
*VMArgs:-Xss128k
*@authorzzm
*/
publicclassJavaVMStackSOF{

privateintstackLength=1;

publicvoidstackLeak(){
stackLength++;
stackLeak();
}

publicstaticvoidmain(String[]args)throwsThrowable{
JavaVMStackSOFoom=newJavaVMStackSOF();
try{
oom.stackLeak();
}catch(Throwablee){
System.out.println("stacklength:"+oom.stackLength);
throwe;
}
}
}

运行结果:

stacklength:2402Exceptioninthread"main"java.lang.StackOverflowErroratorg.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)atorg.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)atorg.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
这个应该和你遇到的问题一样,看看是不是存在以上3点的问题。

C. Java中有类似于NGen的工具吗

Java世界里有很多AOT编译的解决方案,虽然Oracle/Sun JDK到JDK8为止都还没有提供这样的功能。

先来几个传送门:

HotSpot VM JIT的编译产出,理论上能否被复用? - RednaxelaFX 的回答

逃逸分析为何不能在编译期进行? - RednaxelaFX 的回答

为什么Java不能由JVM产生针对特定操作系统的机器码从而提高效率? - RednaxelaFX 的回答

请教:对Java类库jar文件,有什么好的防止反编译办法,最好是加密/解密方案,而不是代码混淆方案。 - RednaxelaFX 的回答

如何将Java打包成exe文件在没有JRE环境的电脑上执行? - RednaxelaFX 的回答

各个操作系统下的 JVM 是谁开发出来的? - RednaxelaFX 的回答

ios设备如何java编译? - RednaxelaFX 的回答
java现在能直接编译成机器码? - Java

在深入到具体实现前,要先强调一点:

AOT编译(Ahead-of-Time Compilation)不但涉及一个编译器,还要涉及配套的运行时支持系统(runtime system)。两者通常是紧密耦合的。

句话说,一个AOT编译器只能跟自己的runtime搭配使用,这个runtime可以是一个完整的VM(如NGen与CLR的搭配),也可以是一个比较
小的runtime(如.NET Native里的MRT(Minimal Runtime),只提供基础的GC、多线程支持等功能)。

所以不要想象说下面列举的工具能够对Java做AOT编译,然后运行时还搭配Oracle JDK / OpenJDK来使用。

来看看一些具体实现。

IBM JDK6+ - Enhance performance with class sharing

IBM JDK是主流JDK之一,并且提供了AOT编译的功能。
这个AOT编译的主要目的是提高启动速度,以及在多个进程之间共享AOT编译出来的机器码。
被AOT编译的代码在运行时还可以再次被JIT编译,这样既能提高启动速度,又不会影响最高速度(peak performance)。

这个AOT编译用的编译器就是IBM J9 VM里的JIT编译器,只是让它以AOT模式来工作。这点跟NGen有点类似(NGen也是直接用CLR里的JIT编译器来生成native image)。跟它搭配使用的runtime自然就是完整的J9 VM。

过跟NGen不同的是,NGen是真的在程序执行前就做了编译,而IBM
J9提供的AOT编译其实是在用户第一次运行程序时把特殊模式的JIT编译生成的代码缓存到磁盘上,后续执行的时候就可以直接使用该缓存里的机器码。所以
IBM把这个功能叫做“dynamic AOT”。

Excelsior JET

一个比较成熟的商业的Java AOT编译解决方案。仅支持x86上的若干操作系统。
这个AOT编译器是自己写的一个私有的编译器,其搭配使用的runtime也是私有的。它支持几种不同的编译模式,搭配使用的runtime可以完全不带解释器/JIT编译器,只带有GC、线程支持等功能,也可以带有更完整的JVM功能。

现在可能很多人都知道Android的ART,而对Java世界里的老前辈们没啥认知。其实ART的AOT编译与解释器/JIT混合的方式,跟Excelsior JET(以及下面提到的GCJ)是相当相似的。

GCJ

一个开源的Java运行时系统,支持AOT编译、解释执行与JIT编译。GCJ是GCC的一部分。在OpenJDK流行起来之前,通常各种Linux发行版带的Java实现会是GCJ。

RoboVM

一个让Java程序可以运行在iOS上的开源解决方案。

iOS不允许第三方程序做运行时代码生成(也就是不允许JIT编译),所以在iOS上运行程序要么得AOT编译,要么只能解释执行。Oracle ADF选择使用一个只能解释执行的JVM来支持Java程序,而RoboVM选择使用AOT编译。

RoboVM的AOT编译器借助了不少现成的框架来实现。其中最重要的两个是Soot与LLVM,前者解决编译器前端、后者解决编译器后端,RoboVM自己只要解决一些跟runtime搭配的地方就好了。
RoboVM配套的runtime是自己写的一个比较小的runtime。

VMKit

VMKit是一个基于许多现成的库组合起来实现的VM,主要可以用作JVM,也可配置为一个CLI。
VMKit支持AOT编译。它的JIT与AOT编译器都是基于LLVM实现的。不过实现得比较粗糙嗯。

Avian

Avian不是一个完整的JVM,只支持Java的一个比较有用的子集。很多时候也够用了。
它可以支持AOT编译。

ART (Android Runtime)

ART和Dalvik VM虽然不直接实现Java字节码,但从整个系统的角度看它们俩都是不折不扣的Java系统。
ART支持AOT编译与解释执行。Java程序的启动速度在ART上是比在Dalvik VM上快多了。

只想强调一点:ART的AOT编译并不依赖LLVM。详情请参考另外几个回答:

Android 中的 LLVM 主要做什么? - RednaxelaFX 的回答

如何看待微软新出的LLILC,一个新的基于LLVM的CoreCLR JIT/AOT编译器? - RednaxelaFX 的回答

Jikes RVM、Maxine VM、Joeq

这三个是元循环Java虚拟机的代表。关于元循环虚拟机(metacircular VM),请参考另一个回答:用 JavaScript 写成的 JavaScript 解释器,意义是什么? - RednaxelaFX 的回答
它们都是用纯Java实现的Java虚拟机,而且都能独立运行,也就是说可以自举(bootstrap)。要实现bootstrap,它们就必然需要能支持AOT编译的编译器。

所以说在这类实现里,AOT编译不是为了提高启动速度,而是为了实现bootstrap的根本需求。有趣的是,它们可以(在一定范围内)支持定制boot image的内容,也就是说可以让Java应用程序与JVM一起AOT编译构成boot image。

JNode - Java New Operating System Design Effort

JNode是一个用纯Java实现的操作系统。这比上面三个元循环Java虚拟机还要更进一步,可以在裸硬件上bootstrap。自然,它也需要一个支持AOT编译的编译器,同样是出于实现的根本需求。

Oracle Labs: Substrate VM

Substrate VM是Oracle Labs的一个研究项目,跟Graal编译器与Truffle框架搭配使用。它实现了一个很小型的、可定制的runtime,可以让基于Truffle实现的编程语言可以脱离标准JVM独立运行。
这里,Substrate VM提供的是runtime的功能,真正的AOT编译器是Graal。

Oracle/Sun JDK


实Sun以前在JDK6时期研究过实现AOT编译,但是当时选择的实现方式比较取巧。后来发现效果并不理想,而且在有了多层编译系统(tiered
compilation system)之后这个AOT编译的原型实现在启动速度上根本没有优势,就把这个项目搁置了。具体细节抱歉我无法多说。

但是Oracle JDK在计划提供新的AOT编译支持。或许会在未来版本的Oracle JDK里出现。请拭目以待。
目前Oracle在公开场合介绍这个AOT编译器的主要资料是JVMLS 2015上的一个演讲,Java Goes AOT - JVMLS 2015 (打不开请自备工具…),有兴趣的同学可以参考下。它是一个基于Graal编译器的实现。

IKVM.NET

前面说的AOT编译解决方案都是把Java(包含Java字节码的Class文件)编译到native code。http://IKVM.NET是一种比较特殊的方案,把Java Class文件编译到.NET Assembly,然后可以用任意CLI(Common Language Infrastructure)实现上运行,例如微软的CLR和Xamarin的Mono。

借助CLR的NGen和Mono的AOT编译,http://IKVM.NET生成的.NET Assembly还可以进一步被编译为native code。这样也算间接达到了AOT编译的目的。

这不是拿来搞笑的。http://IKVM.NET的作者做过一个演示,在Windows上基于IKVM+NGen来运行Eclipse,启动速度比当时的Oracle JDK6快得多…

跟http://IKVM.NET类似的项目以前还有几个,例如Ja.NET(官网挂了,介绍可以看InfoQ的新闻稿)。但活到现在的恐怕就IKVM.NET一家了。

D. Go 语言内存管理(三):逃逸分析

Go 语言较之 C 语言一个很大的优势就是自带 GC 功能,可 GC 并不是没有代价的。写 C 语言的时候,在一个函数内声明的变量,在函数退出后会自动释放掉,因为这些变量分配在栈上。如果你期望变量的数据可以在函数退出后仍然能被访问,就需要调用 malloc 方法在堆上申请内存,如果程序不再需要这块内存了,再调用 free 方法释放掉。Go 语言不需要你主动调用 malloc 来分配堆空间,编译器会自动分析,找出需要 malloc 的变量,使用堆内存。编译器的这个分析过程就叫做逃逸分析。

所以你在一个函数中通过 dict := make(map[string]int) 创建一个 map 变量,其背后的数据是放在栈空间上还是堆空间上,是不一定的。这要看编译器分析的结果。

可逃逸分析并不是百分百准确的,它有缺陷。有的时候你会发现有些变量其实在栈空间上分配完全没问题的,但编译后程序还是把这些数据放在了堆上。如果你了解 Go 语言编译器逃逸分析的机制,在写代码的时候就可以有意识地绕开这些缺陷,使你的程序更高效。

Go 语言虽然在内存管理方面降低了编程门槛,即使你不了解堆栈也能正常开发,但如果你要在性能上较真的话,还是要掌握这些基础知识。

这里不对堆内存和栈内存的区别做太多阐述。简单来说就是, 栈分配廉价,堆分配昂贵。 栈空间会随着一个函数的结束自动释放,堆空间需要时间 GC 模块不断地跟踪扫描回收。如果对这两个概念有些迷糊,建议阅读下面 2 个文章:

这里举一个小例子,来对比下堆栈的差别:

stack 函数中的变量 i 在函数退出会自动释放;而 heap 函数返回的是对变量 i 的引用,也就是说 heap() 退出后,表示变量 i 还要能被访问,它会自动被分配到堆空间上。

他们编译出来的代码如下:

逻辑的复杂度不言而喻,从上面的汇编中可看到, heap() 函数调用了 runtime.newobject() 方法,它会调用 mallocgc 方法从 mcache 上申请内存,申请的内部逻辑前面文章已经讲述过。堆内存分配不仅分配上逻辑比栈空间分配复杂,它最致命的是会带来很大的管理成本,Go 语言要消耗很多的计算资源对其进行标记回收(也就是 GC 成本)。

Go 编辑器会自动帮我们找出需要进行动态分配的变量,它是在编译时追踪一个变量的生命周期,如果能确认一个数据只在函数空间内访问,不会被外部使用,则使用栈空间,否则就要使用堆空间。

我们在 go build 编译代码时,可使用 -gcflags '-m' 参数来查看逃逸分析日志。

以上面的两个函数为例,编译的日志输出是:

日志中的 &i escapes to heap 表示该变量数据逃逸到了堆上。

需要使用堆空间,所以逃逸,这没什么可争议的。但编译器有时会将 不需要 使用堆空间的变量,也逃逸掉。这里是容易出现性能问题的大坑。网上有很多相关文章,列举了一些导致逃逸情况,其实总结起来就一句话:

多级间接赋值容易导致逃逸

这里的多级间接指的是,对某个引用类对象中的引用类成员进行赋值。Go 语言中的引用类数据类型有 func , interface , slice , map , chan , *Type(指针) 。

记住公式 Data.Field = Value ,如果 Data , Field 都是引用类的数据类型,则会导致 Value 逃逸。这里的等号 = 不单单只赋值,也表示参数传递。

根据公式,我们假设一个变量 data 是以下几种类型,相应的可以得出结论:

下面给出一些实际的例子:

如果变量值是一个函数,函数的参数又是引用类型,则传递给它的参数都会逃逸。

上例中 te 的类型是 func(*int) ,属于引用类型,参数 *int 也是引用类型,则调用 te(&j) 形成了为 te 的参数(成员) *int 赋值的现象,即 te.i = &j 会导致逃逸。代码中其他几种调用都没有形成 多级间接赋值 情况。
同理,如果函数的参数类型是 slice , map 或 interface{} 都会导致参数逃逸。

匿名函数的调用也是一样的,它本质上也是一个函数变量。有兴趣的可以自己测试一下。

只要使用了 Interface 类型(不是 interafce{} ),那么赋值给它的变量一定会逃逸。因为 interfaceVariable.Method() 先是间接的定位到它的实际值,再调用实际值的同名方法,执行时实际值作为参数传递给方法。相当于 interfaceVariable.Method.this = realValue

向 channel 中发送数据,本质上就是为 channel 内部的成员赋值,就像给一个 slice 中的某一项赋值一样。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 类型都会导致发送到 channel 中的数据逃逸。

这本来也是情理之中的,发送给 channel 的数据是要与其他函数分享的,为了保证发送过去的指针依然可用,只能使用堆分配。

可变参数如 func(arg ...string) 实际与 func(arg []string) 是一样的,会增加一层访问路径。这也是 fmt.Sprintf 总是会使参数逃逸的原因。

例子非常多,这里不能一一列举,我们只需要记住分析方法就好,即,2 级或更多级的访问赋值会 容易 导致数据逃逸。这里加上 容易 二字是因为随着语言的发展,相信这些问题会被慢慢解决,但现阶段,这个可以作为我们分析逃逸现象的依据。

下面代码中包含 2 种很常规的写法,但他们却有着很大的性能差距,建议自己想下为什么。

Benchmark 和 pprof 给出的结果:

熟悉堆栈概念可以让我们更容易看透 Go 程序的性能问题,并进行优化。

多级间接赋值会导致 Go 编译器出现不必要的逃逸,在一些情况下可能我们只需要修改一下数据结构就会使性能有大幅提升。这也是很多人不推荐在 Go 中使用指针的原因,因为它会增加一级访问路径,而 map , slice , interface{} 等类型是不可避免要用到的,为了减少不必要的逃逸,只能拿指针开刀了。

大多数情况下,性能优化都会为程序带来一定的复杂度。建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化。

E. Go切片数组深度解析

Go 中的分片数组,实际上有点类似于Java中的ArrayList,是一个可以扩展的数组,但是Go中的切片由比较灵活,它和数组很像,也是基于数组,所以在了解Go切片前我们先了解下数组。

数组简单描述就由相同类型元素组成的数据结构, 在创建初期就确定了长度,是不可变的。

但是Go的数组类型又和C与Java的数组类型不一样, NewArray 用于创建一个数组,从源码中可以看出最后返回的是 &Array{}的指针,并不是第一个元素的指针,在Go中数组属于值类型,在进行传递时,采取的是值传递,通过拷贝整个数组。Go语言的数组是一种有序的struct。

Go 语言的数组有两种不同的创建方式,一种是显示的初始化,一种是隐式的初始化。

注意一定是使用 [...]T 进行创建,使用三个点的隐式创建,编译器会对数组的大小进行推导,只是Go提供的一种语法糖。

其次,Go中数组的类型,是由数值类型和长度两个一起确定的。[2]int 和 [3]int 不是同一个类型,不能进行传参和比较,把数组理解为类型和长度两个属性的结构体,其实就一目了然了。

Go中的数组属于值类型,通常应该存储于栈中,局部变量依然会根据逃逸分析确定存储栈还是堆中。

编译器对数组函数中做两种不同的优化:

在静态区完成赋值后复制到栈中。

总结起来,在不考虑逃逸分析的情况下,如果数组中元素的个数小于或者等于 4 个,那么所有的变量会直接在栈上初始化,如果数组元素大于 4 个,变量就会在静态存储区初始化然后拷贝到栈上。

由于数组是值类型,那么赋值和函数传参操作都会复制整个数组数据。

不管是赋值或函数传参,地址都不一致,发生了拷贝。如果数组的数据较大,则会消耗掉大量内存。那么为了减少拷贝我们可以主动的传递指针呀。

地址是一样的,不过传指针会有一个弊端,从打印结果可以看到,指针地址都是同一个,万一原数组的指针指向更改了,那么函数里面的指针指向都会跟着更改。

同样的我们将数组转换为切片,通过传递切片,地址是不一样的,数组值相同。

切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。

所以,切片属于引用类型。

通过这种方式可以将数组转换为切片。

中间不加三个点就是切片,使用这种方式创建切片,实际上是先创建数组,然后再通过第一种方式创建。

使用make创建切片,就不光编译期了,make创建切片会涉及到运行期。1. 切片的大小和容量是否足够小;
切片是否发生了逃逸,最终在堆上初始化。如果切片小的话会先在栈或静态区进行创建。

切片有一个数组的指针,len是指切片的长度, cap指的是切片的容量。

cap是在初始化切片是生成的容量。

发现切片的结构体是数组的地址指针array unsafe.Pointer,而Go中数组的地址代表数组结构体的地址。

slice 中得到一块内存地址,&array[0]或者unsafe.Pointer(&array[0])。

也可以通过地址构造切片

nil切片:指的unsafe.Pointer 为nil

空切片:

创建的指针不为空,len和cap为空

当一个切片的容量满了,就需要扩容了。怎么扩,策略是什么?

如果原来数组切片的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况对现数组的地址和原数组地址不相同。

从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝,即浅拷贝。所以每次打印 Value 的地址都不变。

由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。

F. 反射的性能开销都在哪

1.反射调用过程中会产生大量的临时对象,这些对象会占用内存,可能会导致频繁 gc,从而影响性能。
2.反射调用方法时会从方法数组中遍历查找,并且会检查可见性等操作会耗时。
3.反射在达到一定次数时,会动态编写字节码并加载到内存中,这个字节码没有经过编译器优化,也不能享受JIT优化。
4.Method#invoke 方法会对参数做封装和解封操作,会涉及自动装箱/拆箱和类型转换,都会带来一定的资源开销。
5.反射方法难以内联
6.需要检查方法可见性和校验参数

invoke 方法的参数是一个可变长参数,也就是构建一个 Object 数组存参数,这也同时带来了基本数据类型的装箱操作,在 invoke 内部会进行运行时权限检查,这也是一个损耗点。普通方法调用可能有一系列优化手段,比如方法内联、逃逸分析,而这又是反射调用所不能做的,性能差距再一次被放大。
优化反射调用,可以尽量避免反射调用虚方法、关闭运行时权限检查、可能需要增大基本数据类型对应的包装类缓存、如果调用次数可知可以关闭 Inflation 机制,以及增加内联缓存记录的类型数目。

G. 菜鸟:刚学java,堆区,栈区,静态区,代码区,晕了!!!!!

你问题太多了。简单为你解答一下吧,JAVA语言的内存管理分为栈内存,堆内存和方法区,栈内存用来存储基本数据类型和对象的引用(对象的实体和引用这两个概念你要搞明白),堆内存用来存储对象的实体。。你记住,JAVA是一门面向对象的语言,在JAVA理万事万物都是对象,除了两个东西:1,8个基本数据类型(对应的,还有8个相关的包装类,但是为了JAVA运行速度的考虑,SUN公司保留了这8个基本数据类型);2,就是你所谓的入口方法,即main方法;这两点是JAVA不是纯粹的面向对象语言的表现,也就是他比较特殊的地方,你记住就行了;接下来,我们来看栈内存和堆内存,JAVA里面所有东西都是对象,那么对象保存在哪呢?其实,对象里的所有东西保存在堆内存里,里面包括了这个对象的成员变量和方法等东西,而栈内存里,保存的是这个对象所属的这块堆内存的首地址?也就是一个16进制的数字,明白了?因为你要告诉JAVA虚拟机从哪里去开始读取这块堆内存啊。所以,你明白栈内存用来存储基本数据类型和对象的引用,堆内存用来存储对象的实体了。。而内存管理里面还有一块叫方法区,这是JAVA虚拟机用来在执行一个JAVA程序之前保存这个程序的结构等级的地方,虚拟机按照这个结构等级来调用程序里德对象方法等,而静态变量和静态方法正是保存在方法区里,所以静态方法可以在不创建对象的时候就调用,因为创建对象就是为对象分配堆内存,只有创建了对象之后才能调用对象的非静态方法和非静态变量。。你的第一个问题就能解答了,这个情况就是zhangsan的堆内存里保存的car对象的引用,而这个引用又指向car对象的堆内存;对象的成员变量是保存在自己的堆内存里的;而入口类是一个特殊的东西,你特殊对待就行了。

H. 英语Max Non Heap Memory怎么翻译

为什么要学习JVM?

深入理解JVM可以帮助我们从平台角度提高解决问题的能力,例如,有效防止内存泄漏(Memory leak),优化线程锁的使用 (Thread Lock),更加高效的进行垃圾回收 (Garbage collection),提高系统吞吐量 (throughput),降低延迟(Delay),提高其性能(performance)等。

你是如何理解JVM的?

JVM 是 Java Virtual Machine的缩写,顾名思义,它是一个虚拟计算机,是硬件计算机的抽象(虚构)实现,是JAVA平台的一部分,如图所示(见图中的最底端):

JVM是 Java 程序能够实现跨平台的基础(Java的跨平台本质上是通过不同平台的JVM实现的),它的作用是加载 Java 程序,把字节码(bytecode)翻译成机器码再交由 CPU 执行。如图所示:

程序在执行之前先要把 Java 代码(.java)转换成字节码(.class),JVM 通过类加载器(ClassLoader)把字节码加载到内存中,【关注尚硅谷,轻松学IT】但字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine) 将字节码翻译成底层机器码,再交由 CPU 去执行。

市场上有哪些主流的JVM呢?

JVM是一种规范,基于这种规范,不同公司做了具体实现,BEA公司研发JRockit VM,后在2008年由Oracle公司收购;IBM公司研发了J9 VM,只应用于IBM 内部。Sun公司研发了HotSpot VM,后在2010年由Oracle公司收购。目前是甲骨文公司最主流的一款JVM虚拟机,也是我们现在最常用的一种。

JVM的体系结构是怎样的?

JVM 的体系结构,如图所示:

类加载系统 (ClassLoader System)负责加载类到内存;运行时数据区 (Runtime Data Area)负责存储对象数据信息;执行引擎(Execution Engine)负责调用对象执行业务;本地库接口(Native Interface)负责融合不同的编程语言为 Java 所用。

JVM有哪些运行模式吗?

JVM有两种运行模式Server与Client。两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。

现在64位的jdk中默认都是server模式(可通过 java -version进行查看)。当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而server模式启动的虚拟机采用相对重量级,代号为C2的编译器.c1、c2都是JIT编译器, C2比C1编译器编译的相对彻底,服务起来之后,性能更高。

JVM 运行时内存结构是怎样的?

不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚

拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个区域,如图所示:


I. 编译器的工作原理

编译 是从源代码(通常为高级语言)到能直接被计算机或虚拟机执行的目标代码(通常为低级语言或机器语言)的翻译过程。然而,也存在从低级语言到高级语言的编译器,这类编译器中用来从由高级语言生成的低级语言代码重新生成高级语言代码的又被叫做反编译器。也有从一种高级语言生成另一种高级语言的编译器,或者生成一种需要进一步处理的的中间代码的编译器(又叫级联)。
典型的编译器输出是由包含入口点的名字和地址, 以及外部调用(到不在这个目标文件中的函数调用)的机器代码所组成的目标文件。一组目标文件,不必是同一编译器产生,但使用的编译器必需采用同样的输出格式,可以链接在一起并生成可以由用户直接执行的EXE,
所以我们电脑上的文件都是经过编译后的文件。

J. 编译器错误怎么解决

阅读全文

与编译器逃逸分析相关的资料

热点内容
加密封条风噪小 浏览:971
安阳少儿编程市场 浏览:498
云服务器建设原理 浏览:258
javajunit4for 浏览:845
华为服务器如何进阵列卡配置 浏览:435
apache服务器ip地址访问 浏览:718
如何买到安卓手机预装软件 浏览:537
冤罪百度云不要压缩 浏览:87
苏州云存储服务器 浏览:177
解压收纳原声 浏览:386
java注册验证 浏览:376
火花app怎么上推荐 浏览:981
什么app能游戏投屏到电视上 浏览:455
服务器托管到云端是什么意思 浏览:836
app保存草稿怎么用 浏览:808
安卓如何进入proumb 浏览:144
主机虚拟云服务器 浏览:619
删除分区加密的空间会不会恢复 浏览:706
京东app客户上门怎么看搜索量 浏览:741
怎么在农行app购买黄金 浏览:46