天天看点

运行时数据区02--本地方法栈、本地方法接口、堆

运行时数据区02--本地方法栈、本地方法接口、堆

本地方法接口

简单地讲,一个 native method 就是一个 java 调用非 java 代码的接口。一个 native method 是这样一个 java 方法:该方法的实现由非 java 语言实现,比如 c。这个特征并非 java 所特有,很多其它的编程语言都有这一机制,比如在 c++中,你可以用 extern"c“告知 c++编译器去调用一个 c 的函数。

a native method is a java method whose implementation is provided by non-java code

在定义ー个 native method 时,并不提供实现体(有些像定义ー个 java interface),因为其实现体是由非 java语言在外面实现的。

本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合c/c++程序。

java 使用起来非常方便,然而有些层次的任务用 java 实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

与 java 环境外交互:

有时 java 应用需要与 java 外面的环境交互,这是本地方法存在的主要原因。你可以想想 java 需要与一些底层系统,如操作系统或某些硬件交换信息时的情。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 java 应用之外的繁琐的细节。

与操作系统交互:

jvm 支持着 java 语言本身和运行时库,它是 java 程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方我们得以用 java 实现了 jre 的与底层系统的交互,甚至 jvm 的一些部分就是用 c 写的。还有,如果我们要使用一些 java 语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

sun 's java

sun 的解释器是用 c 实现的,这使得它能像一些普通的 c 一样与外部交互。jre 大部分是用 java 实现的,它也通过一些本地方法与外界交互。例如:类 java.lang. thread 的 setpriority()方法是用 java 实现的,但是它实现调用的是该类里的本地方法 setpriority0()。这个本地方法是用 c 实现的,并被植入 jvm 内部,在 windows95 的平台上,这个本地方法最终将调用 win32 setpriority () api。这是一个本地方法的具体实现由 jvm 直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被 jvm 调用。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过 java 程序驱动打印机或者 java 系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 socket 通信,也可以使用 web service 等等,不多做介绍。

本地方法栈

java虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用

本地方法栈,也是线程私有的

允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)

如果线程请求分配的容量超过本地方法栈允许的最大容量,java 虚拟机将会抛出一个 stackoverflowerror 异常。

如果本地方法可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么 java 虚拟机将会抛出一个 outofmemoryerror 异常。

本地方法是使用c语言实现的

它的具体做法是 native method stack 中登记 native 方法,在 execution engine 执行时加载本地方法库。

当某个线程调用一个本地方它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥同样的权限。

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区

它甚至可以直接使用本地处理器中的寄存器

直接从本地内存的堆中分配任意数量的内存

并不是所有的 jvm 都支持本地方法。因为 java 虚拟机规范并没有明确要求本地方法的使用语言、具体实现方式、数据结构等。如果 jvm 产品不打算支持 native 方法,也可以无需实现地方法栈

在 hotspot jvm 中,直接将本地方法和虚拟机合二为一

一个 jvm 实例只存在一个堆内存,堆也是 java 内存管理的核心区域。

java 堆区在 jvm 启动的时候即被创建,其空间大小也就确定了。是 jvm 管理的最大一块内存空间。堆内存的大小是可以调节的。

《java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

所有的线程共享 java 堆,在这里还可以划分线程私有的缓冲区(thread local allocation buffer, tlab)。

《java虚拟机规范》中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。

“几乎”所有的对象实例都在这里分配内存。----从实际的角度看的

数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾回收时才会被移除。

堆,是gc(garbage collection)执行垃圾回收的重点区域。

运行时数据区02--本地方法栈、本地方法接口、堆

现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

运行时数据区02--本地方法栈、本地方法接口、堆

堆空间的内部结构(jdk7&jdk8)

运行时数据区02--本地方法栈、本地方法接口、堆
运行时数据区02--本地方法栈、本地方法接口、堆

java 堆区用于存储 java 对象实例,那么堆的大小在 jvm 启动时就已经设定好了,大家可以通过选项“-xmx“和“-xms“来进行设置。

“-xms“用于表示堆区的起始内存,等价于-xx: initialheapsize

“-xmx“则用于表示堆区的最大内存,等价于-xx: maxheapsize

一旦堆区中的内存大小超过“-xmx“所指定的最大内存时,将会抛出 outofmemoryerror 异常。

通常会将-xms 和 -xmx 两个参数配置相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

默认情況下, 初始内存大小:物理电脑内存大小1/64

最大内存大小:物理电脑内存大小1/4

存储在jvm中的java对象可以被划分为两类:

一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速

另一类对象的生命周期却非常长,在某些极端的情况下还能够与jvm生命周期保持一致。

java 堆区进一步细分的话,可以划分为年轻代(younggen)和老年代(oldgen)其中年轻代又可以划分为 eden 空间、survivor0 空间和 survivor1 空间(有时也叫做 from 区、to 区)。

运行时数据区02--本地方法栈、本地方法接口、堆

新生代和老年代的比例一般为1:2,默认- xx:newratio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3。

在 hotspot 中,eden 空间和另外两个 survivor空间缺省所占的比例是 8:1:1 当然开发人员可以通过选项“-xx: survivorratlo”调整这个空间比例。比如-xx: survivorratio=8

几乎所有的 java 对象都是在 eden 区被 new 出来的。

绝大部分的 java 对象的销毁都在新生代进行了。

ibm 公司的专门研究表明,新生代中 80%的对象都是“朝生夕死”的

可以使用选项-xmn“设置新生代最大内存大小

这个参数一般使用默认值就可以了。

为新对象分配内存是一件非常严谨和复杂的任务,jvm 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 gc 执行完内存回收后是否会在内存空间中产生内存碎片。

new 的对象先放伊甸园区。此区有大小限制。

当伊甸园的空间填满时,程序又需要创建对象,jvm 的垃圾回收器将对伊甸园区进行垃圾回收(minor gc),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区

然后将伊甸园中的剩余对象移动到幸存者0区

如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区的,如果没有回收,就会放到幸存者 1 区。

如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区。

啥时候能去养老区呢?可以设置次数。默认是 15 次。

可以设置参数:<code>--xx:maxtenuringthreshold=&lt;n&gt;</code>进行设置

在养老区,相对悠闲。当养老区内存不足时,再次触发 gc: major gc,进行养老区的内存清理

若养老区执行了 maior gc 之后发现依然无法进行对象的保存,就会产生 oom 异常

java.lang.outofmemoryerror:java heap space

运行时数据区02--本地方法栈、本地方法接口、堆

针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to。

关于垃圾回收:频繁发生在新生区,很少在养老区收集,几乎不在永久区/元空间收集。

运行时数据区02--本地方法栈、本地方法接口、堆

jvm 在进行 gc 时,并非每次都对上面三个内存(新生代、老年代、方法区)区域一起回收的,大部分时候回收的都是指新生代。

针对 hotspot vm 的实现,它里面的 gc 按照回收区域又分为两大种类型:一种是部分收集(partial gc),一种是整堆收集(full gc)

部分收集:不是完整收集整个java 堆的垃圾收集。其中又分为

新生代收集(minor gc/ young gc):只是新生代的垃圾收集

老年代收集 (major gc/old gc):只是老年代的垃圾收集。

目前,只有 cms gc 会有单独收集老年代的行为。

注意,很多时候 major gc 会和 full gc 混淆使用,需要具体分辨是老年代回收还是整堆回收。

混合收集(mixed gc):收集整个新生代以及部分老年代的垃圾收集。

目前,只有 g1 gc 会有这种行为

整堆收集(full gc):收集整个 java 堆和方法区的垃圾收集。

年轻代gc(minor gc)触发机制:

当年轻代空间不足时,就会触发 minor gc,这里的年轻代满指的是 eden 代满,survivor 满不会引发 gc。(每次 minor gc 会清理年轻代的内存。)

因为 java 对象大多都具备朝生タ灭的特性,所以 minor gc 非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解

minor gc会引发 stw,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

运行时数据区02--本地方法栈、本地方法接口、堆

老年代gc(major gc/full gc)触发机制:

指发生在老年代的 gc,对象从老年代消失时,我们说“major gc”或“full gc”发生了。

出现了 major gc,经常会伴随至少ー次的 minor gc(但非绝对的,在 parallel scavenge 收集器的收集策略里就有直接进行 major gc 的策略选择过程)

也就是在老年代空间不足时,会先尝试触发 minor gc。如果之后空间还不足则触发 major gc

major gc 的速度一般会比 minor gc慢10倍以上,stw 的时间更长。

如果 major gc 后,内存还不足,就报 oom 了。

full gc触发机制:(后面详细介绍)

触发 fu11gc 执行的情况有如下五种:

调用 system.gc()时,系统建议执行 full gc,但是不必然执行

老年代空间不足

方法区空间不足

通过 minor gc 后进入老年代的平均大小大于老年代的可用内存

由 eden 区、survivor space0 (from space)区向 survivor space1 (to space)区复制时,对象大小大于 to space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

说明:full gc 是开发或调优中尽量要避免的。这样暂时时间会短一些。

为什么需要把java堆分代?不分代就不能正常工作了吗?

经研究,不同对象的生命周期不同。70%-99%的对象都是临时对象。

新生代:有eden、两块大小相同的survivor构成,to总为空

老年代:存放新生代中经历多次gc仍然存活的对象

其实不分代完全可以,分代的唯一理由就是优化 gc 性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。gc 的时候要找到哪些对象没用这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当 gc 的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

如果对象在 eden 出生并经过第一次 minor gc 后仍然存活,并且能被 survivor 容纳的话,将被移动到 survivor 空间中,并将对象年龄设为 1。对象在 survivor 区中每熬过一次 minorgc,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁,其实每个 jvm、每个 gc 都有所不同)时,就会被晋升到老年代中

对象晋升老年代的年龄阈值,可以通过选项-xx: maxtenuringthreshold 来设置

针对不同年龄段的对象分配原则如下所示:

优先分配到 eden

大对象直接分配到老年代

尽量避免程序中出现过多的大对象

长期存活的对象分配到老年代

动态对象年龄判断

如果 survivor 区中相同年龄的所有对象大小的总和大于 survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 maxtenuringthreshold 中要求的年龄。

空间分配担保

-xx: handlepromotionfailure

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据

由于对象实例的创建在 jvm 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的

为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

从内存模型而不是垃圾收集的角度,对 eden 区域继续进行划分,jvm 为每个线程分配了一个私有缓存区域,它包含在 eden 空间内。

多线程同时分配内存时,使用 tlab 可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

据我所知所有 open jdk 衍生出来的 jvm 都提供了 tlab 的设计。

运行时数据区02--本地方法栈、本地方法接口、堆

尽管不是所有的对象实例都能够在 tlab 中成功分配内存,但 jvm 确实是将tlab 作为内存分配的首选

在程序中,开发人员可以通过选项“-xx: usetlab”设置是否开启tlab 空间

默认情況下,tlab 空间的内存非常小,仅占有整个 eden 空间的 1%,当然我们可以通过选项“-xx: tlabwastetargetpercent”设置 tlab 空间所占用 eden 空间的百分比大小。

一旦对象在 tlab 空间分配内存失败时,jwm 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 eden 空间中分配内存。

运行时数据区02--本地方法栈、本地方法接口、堆
运行时数据区02--本地方法栈、本地方法接口、堆
运行时数据区02--本地方法栈、本地方法接口、堆

在发生 minor gc 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。

如果大于,则此次 minor gc 是安全的

如果小于,则虚拟机会查看-xx: handlepromotionfailure 设置值是否允许担保失败。

如果 handlepromotionfailure=true,那么会继续检査老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。

如果大于,则尝试进行一次 minor gc,但这次 minor gc 依然是有风险的

如果小于,则改为进行一次 full gc。

如果 handlepromotionfailure= false,则改为进行一次 full gc

在 jdk6 update24 之后,handlepromotionfailure 参数不会再影响到虚拟机的空间分配担保策略,观察 openjdk 中的源码变化,虽然源码中还定义了handlepromotionfailure 参数,但是在代码中已经不会再使用它。jdk6 update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 minor gc,否则将进行 full g。

在《深入理解 java 虚拟机》中关于 java 堆内存有这样一段描述:

随着 java 编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

在 java 虚拟机中,对象是在 java 堆中分配内存的,这是一个普遍的常识。但是,有种特殊情况,那就是如果经过逃逸分析(escape analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

此外,前面提到的基于 openjdk 深度定制的 taobao vm,其中创新的 gcih (gc invisible heap)技术实现。off-heap,将生命周期较长的 java 对象从 heap 中移至 heap 外,并且 gc 不能管理 gcih 内部的 java 对象,以此达到降低 gc 的回收频率和提升 gc 的回收效率的目的。

如何将堆上的对象分配到栈,需要使用逃逸分析手段。

这是一种可以有效减少 java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法

通过逃逸分析,java hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上

逃逸分析的基本行为就是分析对象动态作用域:

当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。

当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

运行时数据区02--本地方法栈、本地方法接口、堆

没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。

运行时数据区02--本地方法栈、本地方法接口、堆

上述代码如果想要stringbuffer sb不逃出方法,可以这样写:

运行时数据区02--本地方法栈、本地方法接口、堆
运行时数据区02--本地方法栈、本地方法接口、堆

参数设置

在 jdk6u23 版本之后,hotspot 中默认就已经开启了逃逸分析。如果使用的是较早的版本,开发人员则可以通过

选项“-xx: + doescapeanalysis“显式开启逃逸分析

通过选项“-xx:+ printescapeanalysis“看逃逸分析的筛选结果。

结论:

开发中能使用局部变量的,就不要使用在方法外定义。

使用逃逸分析,编译器可以对代码做如下优化:

栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配

同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步

分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 cpu 寄存器中

jit编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成上分配。分配完成后,继续在调用内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

常见的上分配的场景

在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。

线程同步的代价是相当高的,同步的后果是降低并发性和性能

在动态编译同步块的时候,jit 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么 jit 编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除

如下代码:

运行时数据区02--本地方法栈、本地方法接口、堆

代码中对 hollis 这个对象进行加锁,但是 hollis 对象的生命周期只在 f()方法中,并不会被其他线程所访问到,所以在 jit 编译阶段就会被优化掉。优化成:

运行时数据区02--本地方法栈、本地方法接口、堆

标量(scalar)是指一个无法再分解成更小的数据的数据。java 中的原始数据类型就是标量。

相对的,那些还可以分解的数据叫做聚合量(aggregate), java 中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在 jit 阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过 jvm 优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换

运行时数据区02--本地方法栈、本地方法接口、堆

以上代码经过标量替换后,就会变成:

运行时数据区02--本地方法栈、本地方法接口、堆

可以看到,point 这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为旦不需要创建对象了,那么就不再需要分配堆内存了

标量替换为栈上分配提供了很好的基础。

标量替换参数设置

参数 xx: +elimilnateallocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。

上述代码在主函数中进行了 1 亿次 alloc。调用进行对象创建,由于 user 对象实例需要占据约 16 字节的空间,因此累计分配空间达到将近 1.5gb。如果堆空间小于这个值,就必然会发生 gc。使用如下参数运行上述代码:

关于逃逸分析的论文在 1999 年就已经发表了,但直到 jdk1.6 才有实现,而且这项技术到如今也并不是十分成熟的。

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。

注意到有一些观点,认为通过逃逸分析,jvm 会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取決于 jvm 设计者的选择。据我所知,oracle hotspot jvm 中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。

目前很多书籍还是基于 jdk7 以前的版本,jdk 已经发生了很大变化,intern 字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是, intern 字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上

年轻代是对象的延生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。

老年代放置长生命周期的对象,通常都是从 survivor 区域筛选拷贝过来的 java 对象。当然,也有特殊情况,我们知道普通的对象会被分配在 tlab 上;如果对象较大,jvm 会试图直接分配在 eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,jvm 就会直接分配到老年代。

当 gc 只发生在年轻代中,回收年轻代对象的行为被称为 minor gc。当 gc 发生在老年代时则被称为 major gc 或者 full gc。一般的,minor gc 的发生频率要比 major gc 高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。