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 虚拟机所管理的内存将会包括以下几个区域,如图所示:
Java 堆(Heap)
Java堆(Java Heap)是 JVM 中内存最大的一块,被所有线程共享的,在虚拟机启动时创建,主要用于存放对象实例,大部分对象实例也都是在这里分配。随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么绝对了。小对象未逃逸还可以在直接在栈上分配。如果在堆中没有内存完成实例分配,并且堆已不可以再进行扩展时,系统底层运行时将会抛出 OutOfMemoryError。Java 虚拟机规范规定,Java 堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是可扩展的,通过 -Xmx 和 -Xms 参数定义堆内存大小。
方法区(Method Area)
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。不同jdk,方法区的实现不同,HotSpot 虚拟机在 JDK 8 中使用 Native Memory 来实现方法区。当方法无法满足内存分配需求时会抛出 OutOfMemoryError 异常。
Java 虚拟机栈(VM Stack)
Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行时的内存模型,每个方法在被线程调用时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,【关注尚硅谷,轻松学IT】都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常。如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常。
JVM本地方法栈 (Native Method Stack)
本地方法栈(Native Method Stack)与虚拟机栈的作用类似,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。在 Java 虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在 Sun HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一了。
JVM程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。
如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的区域。
如何理解JVM中的GC系统?
追踪仍然使用的所有对象,并将其余对象标记为垃圾,然后进行回收,这个过程称之为GC(垃圾回收).所有的GC系统可从GC判断策略(例如引用计数,对象可达性分析),GC收集算法(标记-清除,标记-清除-整理,标记-复制-清除,分代),GC收集器(例如Serial,Parallel,CMS,G1)等方面进行学习
JVM引用链中可以作为 Root 的对象?
Java 虚拟机栈中的引用对象;
本地方法栈中 JNI(既一般说的 Native 方法)引用的对象;
方法区中类静态常量的引用对象;
方法区中常量的引用对象。
JVM中常见垃圾回收算法有哪些?
引用计数器算法
这个算法是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象的时候,计数器就加 1,与之相反,每当引用失效的时候就减 1。也就是以计数来判断对象是否为垃圾。例如:
引用计数法,有一个很大的缺陷就是循环引用,例如:
可达性分析算法
这个算法的核心思路就是通过一系列的“GC Roots”对象作为起始点,从这些对象开始往下搜索,搜索所经过的路径称之为“引用链”。当一个对象到 GC Roots 没有任何引用链相连的时候,证明此对象是可以被回收的。例如:
复制算法
这个算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。此算法的缺点是只能利用一半的内存空间。例如:
标记-清除算法
这个算法执行分两阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。例如:
标记-整理算法
这个算法结合了“标记-清除”和“复制”两个算法的优点。第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象“压缩”复制到堆的其中一块空间中,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题,例如:
JVM对象引用都有哪些类型?
不管是引用计数法还是可达性分析算法都与对象的“引用”有关[说说Java中的四大引用类型。],这说明对象的引用决定了对象的生死,对象的引用关系如下。
强引用
在代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用
是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象,JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
弱引用
非必需对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
虚引用
也称为幽灵引用或幻影引用,是最弱的一种引用关系,无法通过虚引用来获取一个对象实例,为对象设置虚引用的目的只有一个,就是当这个对象被收集器回收时收到一条系统通知。
JVM垃圾回收器的分类都有哪些?
新生代回收器
Serial、ParNew、Parallel Scavenge
老年代回收器
Serial Old、Parallel Old、CMS
整堆回收器
G1垃圾回收器
分代垃圾回收器的组成部分有哪些?
分代垃圾回收器是由新生代(Young Generation)和老生代(Tenured Generation)组成的,默认情况下新生代和老生代的内存比例是 1:2。
新生代的组成部分有哪些?:
新生代是由:Eden、Form Survivor、To Survivor 三个区域组成的,它们内存默认占比是 8:1:1,如图所示:
新生代垃圾回收是怎么执行的?
第一步将Eden和From Survivor 活着的对象复制到 To Survivor 区;第二步将清空 Eden 和 From Survivor 分区;第三步将From Survivor 和 To Survivor 分区交换(From 变 To,To 变 From)。当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的。
谈谈JVM中的CMS 垃圾回收器?
CMS(Concurrent Mark and Sweep)是并发标记和清除垃圾收集器。它会使用空闲列表(free-lists)管理内存空间的回收,不对老年代进行整理。其优点是在标记、清除阶段的大部分工作和应用线程一起并发执行。可以降低延迟,缩短停顿时间,www.atguigu.com提高服务的响应时间。当然也有缺陷,主要表现在,对 CPU 资源要求敏感,无法清除浮动垃圾(浮动垃圾指的是 CMS 清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除),还会产生大量空间碎片。
谈谈JVM中的是 G1 垃圾回收器?
G1(Garbage-First GC)是一款实时收集器,其设计目标是将STW停顿时间和分布变成可预期以及可配置的。可以说是一种兼顾吞吐量和停顿时间的 GC 实现。G1 可以直观的设定停顿时间的目标,相比于 CMS ,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。
使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合,例如:
这样的划分使得 GC不必每次都去收集整个堆空间, 而是以增量的方式来处理,每次只处理一部分小堆区,称为此次的回收集(collection set). 每次暂停都会收集所有年轻代的小堆区, 同时也可能只包含一部分老年代小堆区。
G1的另一项创新, 是在并发阶段估算每个小堆区存活对象的总数。用来构建回收集(collection set)的原则是: 垃圾最多的小堆区会被优先收集。这也是G1名称的由来:garbage-first。
G1 解决了 CMS 中的各种疑难问题, 包括暂停时间的可预测性, 并终结了堆内存的碎片化。对单业务延迟非常敏感的系统来说, 如果CPU资源不受限制,那么G1可以说是 HotSpot 中最好的选择, 特别是在最新版本的Java虚拟机中。当然,这种降低延迟的优化也不是没有代价的: 由于额外的写屏障(write barriers)和更积极的守护线程, G1的开销会更大。所以, 如果系统属于吞吐量优先型的,又或者CPU持续占用100%, 而又不在乎单次GC的暂停时间, 那么CMS是更好的选择。
JVM垃圾回收的调优参数有哪些?
-Xmx:512 设置最大堆内存为 512 M;
-Xms:256 初始堆内存(最小堆)为 256 M;
-XX:MaxNewSize 设置最大年轻代内存;
-XX:MaxTenuringThreshold=6 设置新生代对象经过6次GC晋升到老年代;
-XX:PretrnureSizeThreshold 设置大对象的值,超过这个值的大对象直接进入老生代;
-XX:NewRatio 设置分代垃圾回收器新生代和老生代内存占比;
-XX:SurvivorRatio 设置新生代 Eden、Form Survivor、To Survivor 占比。
JVM现代并发GC有什么调优原则
第一要空间换时间与效率,针对G1 & ZGC 加大堆内存(更多的空余空间)的配置往往更有利 于GC达到目标暂停时间。第二要知道低暂停不代表高吞吐量,并发GC是保证并发阶段GC的同时业务线程依然有几率获得CPU时间片,但同时也意味着GC会与业务线程抢占计算资源,且往往更多的并发阶段为了处理更多的同步问题,也会占用更多的计算资源。第三是GC调优永远要考虑机器资源,对应系统应用场景等等,至少目前没有银弹。
文章来源于jason
I. 编译器的工作原理
编译 是从源代码(通常为高级语言)到能直接被计算机或虚拟机执行的目标代码(通常为低级语言或机器语言)的翻译过程。然而,也存在从低级语言到高级语言的编译器,这类编译器中用来从由高级语言生成的低级语言代码重新生成高级语言代码的又被叫做反编译器。也有从一种高级语言生成另一种高级语言的编译器,或者生成一种需要进一步处理的的中间代码的编译器(又叫级联)。
典型的编译器输出是由包含入口点的名字和地址, 以及外部调用(到不在这个目标文件中的函数调用)的机器代码所组成的目标文件。一组目标文件,不必是同一编译器产生,但使用的编译器必需采用同样的输出格式,可以链接在一起并生成可以由用户直接执行的EXE,
所以我们电脑上的文件都是经过编译后的文件。
J. 编译器错误怎么解决
1、分析原因,这样的错误出现一般是由于服务器拒绝了某一项请求,常见的是写入,所以问题在有表单输入的网页中更容易出现。