重点知识学习(4.2)--[JVM的运行时数据区,本地方法接口]

Posted 小智RE0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重点知识学习(4.2)--[JVM的运行时数据区,本地方法接口]相关的知识,希望对你有一定的参考价值。

一:运行时数据区

Java 8 虚拟机规范规定,Java 虚拟机所管理的内存包括5个运行时数据区域;

  • 程序计数器(Program Counter Register):用来运行程序,是线程私有的;
  • Java 虚拟机栈(Java Virtual Machine Stacks):用来运行程序,是线程私有的;
  • 本地方法栈(Native Method Stack):用来运行程序,是线程私有的;
  • Java 堆(Java Heap):存放数据 是线程共享的;
  • 方法区(Methed Area):存放数据 是线程共享的;

Java 虚拟机定义了序运行期间会使用到的运行数据区,
其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁.
另外一些则是与线程对应的.这些与线程对应的区域会随着线程开始和结束而创建销毁

1.1程序计数器(Program Counter Register)

命名源于
CPU 的寄存器,寄存器存储指令相关的现场信息.CPU 只有把数据装载到寄存器
才能运行;
注意: jvm中的程序计数器不是cpu中的寄存器, 可以理解为计数器.

  • 可看作是当前线程所执行的字节码的行号指示器;
  • 程序计数器用来存储下一条指令的地址,也即将要执行的指令代码.由执行引擎读取下一条指令;
  • 内存空间很小,运行速度最快;
  • 记录当前线程中的方法执行的位置. 以便于cpu在切换执行时,记录程序执行位置;
  • 在运行时数据区中唯一 不会出现内存溢出(OutOfMemoryError) 的区域.
  • 任何时间一个线程都只有一个方法在执行,即当前方法.程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;
  • 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致;
  • 作为程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成


1.2Java本地方法栈(Native Method Stack)

本地方法栈是为虚拟机调用 Native 方法服务的;线程私有的

  • 使用C语言编写实现的;在Native Method Stack中登记 native 方法,在Execution Engine执行时加载本地方法库;
  • 固定或者是可动态扩展的内存大小;内存溢出方面也是相同的. ;当前线程请求分配的栈容量超过本地方法栈允许的最大容量会抛出StackOverflowError栈溢出异常;

1.3Java 虚拟机栈(Java Virtual Machine Stacks)

早期也叫 Java 栈

Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程

由于跨平台性的设计,基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实
现同样功能需要更过的指令集

栈是运行时的单位,而堆是存储的单位
栈:加载方法运行
堆:存储对象

  • 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次方法的调用.
  • Java 虚拟机栈是线程私有的; 生命周期和对应的线程一致.

Java 虚拟机栈主要负责 Java 程序的运行,它保存方法的局部变量(8 种基本数据类型,对象的引用地
址),部分结果,并参与方法的调用和返回;

栈帧中包含( 局部变量(基本类型,引用地址) 方法地址,返回地址)

关于栈异常: StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
注意在递归调用方法次数过多时也会出现;

栈的存储

  • 每个线程都有自己的栈,栈中的数据都以栈帧为单位存储.

  • 在这个线程上正在执行的每个方法都各自对应一个栈帧.

  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息.

  • JVM 直接对 java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出的原则.

  • 在一条活动的线程中,一个时间点上,只会有一个活动栈.即只有当前在执行的方法的栈帧(栈顶)是有效地,此栈帧被称为当前栈(Current Frame),当前栈帧对应的方法称为当前方法(Current Method),定义这个方法的类称为当前类(Current Class).

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作.

  • 如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧.

  • 不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法).

  • 如果当前方法调用了其他方法,方法返回时,当前栈帧会传回此方法的执行结果(递归原理);给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧. Java 方法有两种返回的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常.不管哪种方式,都会导致栈帧被弹出.

栈帧的存储
栈帧中包含( 局部变量(基本类型,引用地址) 方法地址,返回地址)
局部变量表(Local Variables)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
操作数栈(Operand Stack)(或表达式栈)
栈可以用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。程序中的所有计算过程都是在借助于操作数栈来完成的。
动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)
在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址(Retuen Address)(或方法正常退出或者异常退出的定义)
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。


1.4Java 堆(Java Heap)

内存最大被所有线程共享,在虚拟机启动时候创建
Java 堆内存唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存.

  • 一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域.
  • Java 堆区在 JVM 启动时的时候即被创建,其空间大小也就确定了,是 JVM 管理的最大一块内存空间.
  • 堆内存的大小是可以调节. 例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小) 一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率.
  • 《Java 虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的.
  • 所有的线程共享 Java 堆,在这里还可以划分线程私有的缓冲区.
  • 《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例都应当在运行时分配在堆上.
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除.
  • GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域.

Java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生代 : Eden(伊甸园)区 ; Survivor0(幸存者)区; Survivor1(幸存者)区

在堆中为何要分区
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫
描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

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

  1. new 的新对象先放到伊甸园区,此区大小有限制.
  2. 当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.
  3. 然后将伊甸园区中的剩余对象移动到幸存者 0 区
  4. 如果再次出发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.
  5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区.
  6. 什么时候去养老区呢?默认是 15 次,也可以设置参数,最大值为 15
    -XX:MaxTenuringThreshold=<N>
    在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111,所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。
  7. 在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理.
  8. 若养老区执行了 Major GC 之后发现依然无法进行对象保存,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space

经典小故事:
我是一个普通的 Java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。
有一天,Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的 “From” 区。
自从去了 Survivor 区,我就开始飘了,有时候在 Survivor 的 “From ” 区,有时候在Survivor的 “To” 区,居无定所。
直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。
于是我就去了老年代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。
在年老代里,我生活了20年(每次GC加一岁),然后被回收了。

模拟栈溢出;

/**
 * @author by 信计1801 李智青 学号:1809064012
 */
public class TestOOM 
    public static void main(String[] args) 
        List<Integer> list = new ArrayList();
        while(true)
            list.add(new Random().nextInt());
        
    

运行结果

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:267)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
	at java.util.ArrayList.add(ArrayList.java:464)
	at com.xiaozhi.advanced.day04_jvm.test.TestOOM.main(TestOOM.java:14)

可使用此监测工具查看状态

堆各区域的占比

若整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优

新生代 占整堆的三分之一

新生代中的 伊甸园区 幸存者0 幸存者1 占比是 8:1:1

对象经过15次垃圾回收后,依然存活的.将被移动到老年区

为什么最大是15次 , 因为在对象头中只有4个bit位的空间,只能表示最大值15.


分代收集思想 Minor GC、Major GC、Full GC
JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分回收的是新生区.

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为:部分收集,整堆收集.

部分收集:不是完整收集整个 java 堆的垃圾收集.

  • 新生区收集(Minor GC/Yong GC):仅对于新生区(Eden,S0,S1)的垃圾收集
  • 老年区收集(Major GC / Old GC):仅对于老年区的垃圾收集.

整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集.

整堆收集的条件

  • System.gc();时
  • 老年区空间不足
  • 方法区空间不足

注意:尽量避免整堆收集


TLAB(Thread Local Allocation Buffer) 机制

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据.
  • 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的.
  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度.

TLAB 线程本地分配缓存区

在多线程情况下,可以在堆空间中通过-XX:UseTLAB 设置. 在堆空间中为线程开辟一块空间,用来存储线程中产生的一些对象, 避免空间竞争,提高分配效率

堆空间的参数设置:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

字符串常量池为什么位置改变

JDK7 及以后的版本中将字符串常量池放到了堆空间中。

方法区的回收效率很低,在 Full GC 的时候才会执行永久代(方法区)的垃圾回收,而 Full GC 是老年代的空间不足、方法区不足时才会触发。
导致字符串常量池回收效率不高,实际使用时有大量的字符串被创建回收效率低,导致永久代内存不足。放到堆里,能及时回收内存

1.5方法区(Methed Area)

存储已被虚拟机加载的类信息常量静态变量即时编译后的代码等数据。
方法区是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应用程序的实时运行.

线程共享区域

方法区包含了一个特殊的区域“运行时常量池”;

尽管所有的方法区在逻辑上是属于堆的一部分,但对于 HotSpotJVM 而言,方法区还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开.

  • 方法区可以选择固定大小或者可扩展;
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误

模拟内存溢出

/**
 * @author by 信计1801 李智青 学号:1809064012
 */
public class Demo 
    public static void main(String[] args) 
        String temp = "world";
        for (int i = 0; i < Integer.MAX_VALUE; i++) 
            String str = temp + temp;
            temp = str;
            //将字符串存储到字符串常量池中
            str.intern();
        
    

  • 方法区的大小可以通过 -XX:MetaspaceSize 设置;可以设置的大一些,减少FULL GC的触发

  • 方法区在windows中默认大小是21MB;如果到达21MB会触发FULL GC;

  • -XX:MaxMetaspaceSize 的值是-1,没有限制

反编译字节码文件,并输出值文本文件中,便于查看。参数 -p 确保能查看private 权限类型的字段或方法

/**
 * @author by 信计1801 李智青 学号:1809064012
 */
public class Test 
    private static int a = 10;

    static 
        System.out.println("静态代码块");
    

    public void test(int w)
        System.out.println(w);
    

    public static void main(String[] args) 
        Test s = new Test();
    

尝试使用命令javap -v -p Test.class > test.txt

即可得到反编译的文件


方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。
要判定一个类型是否属于“不再被使用的类:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例;
  • 加载该类的类加载器已经被回收,如 OSGi、JSP 的重加载等;
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

二:本地方法接口

本地方法

一个 Native Method 就是一个 java 调用非 java 代码的接口,该方法的底层实现不是 Java 语言
例如 C语言。

  • 很多其他的编程语言都有这一机制在定义一个 native method 时,并不提供实现体(有些像定义一个 Java interface),因为其实现体是由非 java 语言在外面实现的。

  • 关键字 native 可以与其他所有的 java 标识符连用,但是abstract 除外

为何需要本地方法

  • 有时 java 应用需要与 java 外面的环境交互[例如hashCode方法 ; IO流的read()方法 ; 线程的start()方法 ],这是本地方法存在的主要原因。
  • 本地方法作为中介:它提供非常简洁的接口,使得开发者无需去了解 java 应用之外的繁琐细节。
  • Sun 的解释器是用 C 实现的,这使得它能像一些普通的 C 一样与外部交互.

以上是关于重点知识学习(4.2)--[JVM的运行时数据区,本地方法接口]的主要内容,如果未能解决你的问题,请参考以下文章

初识JVM(JVM运行流程,JVM运行时数据区,内存布局中的异常)

JVM学习笔记二:运行时数据区概述及JVM线程

JVM调优:基本概念

JVM学习-运行时数据区2

JVM学习笔记运行时数据区

(2021-05-28)JVM概览学习