重点知识学习(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 执行完内存回收后是否会在内存空间中产生内存碎片.
- new 的新对象先放到伊甸园区,此区大小有限制.
- 当伊甸园的空间填满时,程序又需要创建对象时,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁.再加载新的对象放到伊甸园区.
- 然后将伊甸园区中的剩余对象移动到幸存者 0 区
- 如果再次出发垃圾回收,此时上次幸存下来存放到幸存者 0 区的对象,如果没有回收, 就会被放到幸存者 1 区,每次会保证有一个幸存者区是空的.
- 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区.
- 什么时候去养老区呢?默认是 15 次,也可以设置参数,最大值为 15
-XX:MaxTenuringThreshold=<N>
在对象头中,它是由 4 位数据来对 GC 年龄进行保存的,所以最大值为 1111,所以在对象的 GC 年龄达到 15 时,就会从新生代转到老年代。 - 在老年区,相对悠闲,当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理.
- 若养老区执行了 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的运行时数据区,本地方法接口]的主要内容,如果未能解决你的问题,请参考以下文章