本地方法接口 – Native Interface

本地方法接口(Java Native Interface,JNI)是Java为了融合C/C++程序,Java诞生是C/C++横行时代,想要立足必须能够调用C/C++程序;JVM中开辟了一块区域用于 标记native代码。

例如 Java Object超类中大量存在 native接口方法;native方法本身不是 Java代码,只有方法签名,类似接口;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package java.lang;

import jdk.internal.HotSpotIntrinsicCandidate;

public class Object {

private static native void registerNatives();

@HotSpotIntrinsicCandidate
public final native Class<?> getClass();

@HotSpotIntrinsicCandidate
public native int hashCode();

@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;

@HotSpotIntrinsicCandidate
public final native void notify();

@HotSpotIntrinsicCandidate
public final native void notifyAll();

public final native void wait(long timeoutMillis) throws InterruptedException;
}

本地方法栈 – Native Method Stack

如上所述 , 虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用

堆 – Heap

堆(Heap)历经多代版本的发展,最具典型的Java 1.7 Java8 Java11;本文就已此三个版本深入浅出分析一下 堆的分布和发展

Java7 堆结构

Java8 堆结构

  Java7中jvm堆内存分为 新生代 老年代 永久代,其中新生代又分为,eden space 和 survivor,survivor再分为 From和To;From和To是相对的概念,随着垃圾回收不断变换,谁空谁是To;Java8中将永久代移除用元数据空间取代;元数据空间本质和永久代类似。元数据空间和永久代的本质区别,永久代在堆中,元数据空间不是使用虚拟机堆内存而是直接使用物理内存。

堆之所以会分为不同区,是因为在Java程序中对象的生命周期不同,大多数对象都是临时对象,用完及时释放,这类对象在新生代中都是 “朝生夕死”。假如不分区垃圾回收都是直接扫描全部的内存空间,分区后垃圾回收只需要在小范围内收集垃圾。分区是为了优化jvm垃圾回收器的性能。

Java11 堆结构

    Java11中引入zgc后,堆内存不再有分区概念,Java11中将内存分为page页;ZGC支持三种页大小 ;小中大;其中小页是指 2MB大小内存,中页是32MB大小内存,大页受操作系统控制 是2n MB大小;JDK14之前,2GC仅Linux才支持。尽管许多使用zGc的用户都使用类Linux的环境,但在Windows和macos上,人们也需要zGC进行开发部署和测试。许多桌面应用也可以从ZGC中受益。因此,2GC特性被移植到了Windows和macos上。

现在mac或Windows上也能使用zGC了,示例如下:

1
-XX:+UnlockExperimentalVMOptions-XX:+UseZGC

堆空间分配内存的过程

  • 首先所有的新生对象都在Eden Space出现,刚开始Survivor和Old Generation都是空的

  • 随着对象的不断创建,Eden Space被填充满,此时触发 Minor GC 删除未被引用的对象,并且将存活的对象放入 Survivor From区,然后清空 Eden Space

  • 随着对象的不断创建 Eden Space 再次被填充满,此时触发第二次 Minor GC ,删除未被引用的对象,此时与上一次Minor GC有所不同,首先From区中的对象和Eden Space存活的对象都将移动到 To区,From区被清空,From和To属性交换,同时原有的From区中的对象年龄加1。 随着Minor GC不断触发,From和To区不断在交换,当幸存者年龄达到指定阈值(JVM中参数 MaxTenuringThreshold决定),对象被移动到老年代。

  • 随着Minor GC不断进行,导致老年代内存占满,此时触发 Major GC(或者Full GC)进行老年代内存清理,若Major GC处理完后依然无法进行对象内存分配,就会产生 OOM异常。

通过上述分析:

    会有几种特殊情况导致内存直接分配到老年代

1、对象创建后,无法直接放入Eden Space(不如Eden Space大小设置成 10m,但是对象是 70m),此时触发 YGC(Minor GC),Minor GC清空Eden Space,还是放不下对象,就会将对象直接放入 老年代。当然老年代放不下 会触发 FGC,FGC后还放不下就直接抛出OOM。

2、触发YGC后,Survivor中无法放入,就直接晋升到老年代

3、如果Survivor中年龄相同的所有对象的大小大于Survivor空间的一半,年龄大于或者等于这些对象年龄的对象将直接晋升到老年代,无需等待年龄达到阈值。

常见堆参数

参数 描述
-Xms 设置堆初始内存,默认情况是当前物理内存大小的1/64
-Xmx 设置堆的最大内存,默认情况是当前物理内存大小的1/4
-XX:Newratio 设置新生代和老年代的比例,默认值是2,及老年代是新生代的2倍,占堆内存的2/3
-XX:Survivorratio 设置Eden Space和Survivor的比例,默认情况是8,及Eden Space是Survivor的8倍(两个Survivor是相同的内存大小,8:1:1),如果设置成4 及变成 4:1:1
-XX:MaxTenuringThreshold 设置Survivor存活次数,默认是15,也可成为年龄

方法区 – Method Area

    方法区并不是名称所描述的存放方法的区域,而是提供线程共享的内存区域,用于存储JVM加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等信息;由此可以理解方法区是一种规范。

  •  方法区大小

    方法区的大小决定程序内沟沟加载多少个类,例如程序定义过多的类,导致加载的内占用过多的内存,方法区内存溢出,JVM抛出 OOM。以Java8为例,

1
-XX:MetaspaceSize=xxx

    设置元数据空间初始大小,也可通过

1
-XX:MaxMetaspsaceSize=xxx

    设置元数据空间的最大值,默认情况下 MaxMetasapceSize=-1,没有限制

  • 方法区、堆和栈的关系
  • 方法区的变化
版本 描述
jdk1.7之前 有永久代,静态变量都发在永久代上
jdk1.7 逐步去永久代,字符串常量池、静态变量移除,放入堆中
jdk1.8 无永久代,元数据空间出现;常量保存在本地元数据空间,字符串常量池、静态变量任然保存在堆中

执行引擎 – Excecution Engine

    类加载器加载的字节码并不是操作系统能够直接运行的本地机器指令,执行引擎的作用就是将字节码文件解释成本地机器指令,供操作系统直接运行。换言之执行引擎就是本地机器语言的翻译官,将字节码翻译成本地机器指令

  • 解释器(Interpreter):JVM在程序运行时通过解释器逐行将字节码转为本地机器指令执行;

  • JIT编译器(Just In Time Compiler,即时编译器):解释器的优点是程序一启动就可以马上发挥作用,逐行翻译字节码执行程序。而对于一些高频的代码(如循环体内代码和高频调用方法等),如果每次执行都用解释器逐行将字节码翻译为机器指令的话,势必会造成浪费,所以我们可以通过即时编译器将这部分高频代码直接编译为机器指令然后缓存在方法区中(上面介绍方法区内部组成时提到过JIT代码缓存),以此提高执行效率。和解释器相比,即时编译器的缺点就是编译需要耗费一定时间。