JVAV内存区域
一、JVM的主要组成部分
1、类加载器(ClassLoader)
2、运行时数据区(Runtime Data Area)
3、执行引擎(Executon Engine)
4、本地库接口(Native Interface)
运行流程:
1、在我们运行java文件的时候,先通过编译器将java文件编译成class文件。
(这个过程主要是词法、语法、语义分析以及解糖的过程)
备注:解语法糖的过程由desugar()方法触发,在com.sun.tools.javac.comp.TransTypes类 和com.sun.tools.javac.comp.Lower类中完成。
2、再由类加载器(classloader)class文件转换成字节码文件
3、进入与形式数据区(Runtime Data Area)把字节码加载到内存中
4、这种字节码文件他只是JVM的一套指令集规范,底层操作系统还不能直接执行,因此需要一些特定的命令解析器执行引擎,将这种字节码文件在翻译成底层系统能够执行的字节码,并交给CPU执行
jdk1.8的与运行数据区与之前版本的区别:
二、元空间(Metaspace)与永久代(permanent generation)
【重点】方法区是一个逻辑概念,其具体实现为jdk1.8的元空间与jdk1.8之前的永久代。
方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的⼀种实现方式。
在JDK1.7及之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;而在JDK1.8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中连续的,但物理上是不连续的,也叫非堆。
【重点】元空间和永久代的本质区别是:元空间并不在JVM的内存中,而是使用本地内存。
之所以要使用元空间代替永久代是因为:
永久代的对象被垃圾回收的概率相对较小,用元空间将永久代与堆彻底分开,可以减少很多扫描永久代空间对象带来的时间开销。
类及方法的信息等比较难确定其大小,本地内存相比于JVM内存更大,没那么容易OOM,放入系统内存更为合适。
常量没人用了就回收,但是类型信息则不然,其需要满足比较多的条件才能被回收
1.该类所有的实例均被回收
2.加载该类的类加载器被回收
3.该类对应的 CLASS对象没有任何地方被引用
【元空间的大小设置】-XX:MaxMetaspaceSize=8M
方法区(none-heap)的内存结构方法区保存的信息包括:
(1)类型信息:包括了JVM加载类型(类class、接口interface、枚举enum、注解annotation)的完整有效名称(包名+类名)、其直接父类的完整有效名称、类型的修饰符、其直接继承的接口列表。
(2)域(field)(成员变量)信息:类型的所有成员变量的相关信息以及成员变量的声明顺序。
1. getDeclaredFiled 仅能获取类本身的属性成员(包括私有、共有、保护)
2. getField 仅能获取类(及其父类可以自己测试) public属性成员
(3)方法信息:包括了类型的成员方法的名称、返回类型、参数列表、修饰符、字节码、操作数栈、局部变量表、异常表等。
(4)静态变量:non-final的静态类变量和全局常量。区别在于全局常量在编译器给指定值,静态类变量在加载时准备阶段赋初值,初始化阶段再给指定值。
(5)JIT代码缓存:即时编译产生的代码缓存,将热点代码编译成与本地平台相关的机器码,并保存到内存。(6)运行时常量池(Runtime Constant Pool):Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有⼀些信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进⼊方法区的运行时常量池中存放。(Integer -128到127实例)
public class RuntimeConstantPoolDemo01 {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1 == i2);
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i3 == i4);
}
}
(Java 虚拟机对 Class 文件每一部分(⾃然也包括常量池)的格式都有严格的规定,每⼀个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行。)
各版本的方法区所储存的内容:
- JDK1.8中:元空间,类信息,运行时常量池。
- JDK1.7中:永久代,类信息,运行时常量池。
- JDK1.6中:永久代,类信息,运行时常量池,字符串常量池,静态变量(静态区)。
字符串常量池(StringTable) :字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable**(一种高效用来进行查找的数据结构),不同JDK版本下字符串常量池的位置以及默认大小是不同的!
问题:为什么将字符串常量池从永久代移到堆空间中
因为字符串常量池在开发过程中是经常会被使用到的,像之前被放置在永久代中被回收的概率就比较低,导致永久代空间不够用,放在堆中,能够及时回收。
但是这个哈希表与Java集合中的哈希表不用,无法进行扩容操作,并且字符串种类复杂,很可能发生哈希碰撞现象,一旦字符串在哈希表中形成了链表等数据结构,就会使字符串常量池的性能下降,所以字符串常量池中需要加入垃圾回收机制。
1、直接使用字符串常量进行赋值
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true
}
内存结构图示:
2、通过new创建String类对象
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3=new String("world");
String s4=new String("world");
}
intern方法的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中。
代码示例
@Test
public void test02() {
//1 true
char[] ch = new char[]{'a','a'};
String s1 = new String(ch);
//2 false
/*
String s1 = new String("aa");
*/
//3 false
/*
String s1 = new String("a" + "a");
*/
//4 true
/*
String s1 = new String("a") + new String("a");
*/
s1.intern();
String s2 = "aa";
String s3 = new String("aa");
System.out.println(s1 == s2);
}
问题:单独执行语句四,堆中创建了几个对象 (答案:5个)
@Test
public void StringTestDemo02() {
String s1=new String("asas");//语句一
String s2="asas"; //语句二
String s3=s1.intern(); //语句三
String s4=new String("as")+new String("as"); //语句四
System.out.println(s1==s2); //false
System.out.println(s2==s3); //true
System.out.println(s1==s3); //false
}
三、堆(Heap)
java堆(Heap)是虚拟机所管理的内存最大一块区域(GC的主要区域),和方法区一样是被所有线程共享的。堆在虚拟机启动时创建,生命周期和虚拟机相同,它的唯一目的就是存放对象实例,但并不是所有对象实例都在堆中分配内存,JVM会通过逃逸分析技术,对于逃不出方法的对象,会让其在栈空间上进行分配。
逃逸分析:
如果一个变量的使用,在运行期检测 他的作用范围不会超过一个方法或者一个线程的作用域。那么这个变量就不会被多个线程所共享,也就是说 可以不将其分配在堆空间中,而是将其线程私有化。
public class EscapeAnalysisDemo01 {
//StringBuffer对象发生了逃逸
public static StringBuffer createStringBufferWithEscape(String s1, String s2) {
//759
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
//StringBuffer对象没有发生逃逸
public static String createStringBufferWithnotEscape(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
public static void main(String[] args) {
String string=null;
StringBuffer stringBuffer=null;
stringBuffer=createStringBufferWithEscape("a","b");
string=createStringBufferWithnotEscape("a","b");
}
}
使用逃逸分析,编译器可以对代码做如下优化:
栈上分配:
如果一个变量经过逃逸分析后,判定可以被线程私有的,那么jvm将进行 一个大胆的优化手段, 栈上分配。 java 仅允许在 堆空间创建对象,但jvm的发展已经打破了这一规定。 如果一个对象,注定是线程私有的 那么为什么要放在堆空间,GC的回收以及主存与工作内存的同步都需要消耗大量资源。 而放在栈空间则不在需要担忧这些,对象将跟随栈的创建而创建,销毁而销毁。
public static void main(String[] args) {
long a1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
alloc();
}
// 查看执行时间
long a2 = System.currentTimeMillis();
System.out.println("cost " + (a2 - a1) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(100000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();
}
static class User {
}
关闭逃逸分析
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
开启逃逸分析
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
查看java进程pid以及jmap命令查看当前堆内存中有多少个User对象
jps
jmap -histo pid
同步省略:
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除。
标量替换(分离对象):
**标量(Scalar)**是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
**聚合量(Aggregate)**是还可以分解的数据。Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
转换前:
public static void main(String[] args) {
alloc();
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
private int x;
private int y;
}
转换后:
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x="+x+"; point.y="+y);
}
Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了
逃逸分析JVM相关指令:
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+PrintEscapeAnalysis 查看逃逸分析
-XX:+EliminateAllocations 开启标量替换
+XX:+EliminateLocks 开启同步消除
-XX:+PrintEliminateAllocation 查看标量替换
java堆是垃圾收集器管理的主要区域,现在的垃圾收集器大多都采用分代收集算法,将对划分为
1、新生代(Eden,SurvivorFrom,SurvivorTo)
2、老年代
3、永久代(JDK 8 后被移除)
堆中储存了什么:
【成员变量】
- 基本数据类型是直接保存值
- 引用类型保存指向对象的引用(地址)
【静态区】
- 类变量(静态成员变量)
【字符串常量池】
- 字符串常量池中保存的是字符串的引用。字符串在堆中以字节数组的形式存储。
简单文件的运行过程:
Person类
public class Person {
private String name;
public Person(String name){
this.name=name;
}
public void sayHello(){
System.out.println("Hello! My Name is: " + name);
}
}
PersonTest类
public class PersonTest {
public static void main(String[] args) {
Person p=new Person("luoyiming");
p.sayHello();
}
}
四、虚拟机栈(Virtual machine stack)
虚拟机栈(Virtual Machine Stack)也是线程私有的,它的生命周期 与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信 息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
注意:只有位于栈顶的方法才是运行的,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作
栈帧是如何分配内存的。其实早在编译阶段一个栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,也就是说一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。
局部变量表
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义 的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位。
一个变量槽大小一般为32位,其实《Java虚拟机规范》中并没有明确指出 一个变量槽应占用的内存空间大小,如在64位虚拟机中使用了64位的物理内存空间去实现一个变量槽,但是虚拟机仍要使用对齐和补白的手段 让变量槽在外观上看起来与32位虚拟机中的一致,也就是说64位的变量槽实际上只用到了前32位来存储数据,其余32位是不能使用的。对于boolean、byte、char、short、int、 float、reference 和returnAddress这8种数据类型,可以使用32位或更小的物理内存来存储,而64位的数据类型(double、long),Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间(64位的虚拟机分配的也是两个变量槽)。
类型 | 占用储存空间 | 表示范围 |
byte | 1字节 | -128~127 |
boolean | 1字节 | true、false |
short | 2字节 | -32768~32767 |
char | 2字节 | 0 ~ 65535(0 ~ 2^16-1) |
int | 4字节 | -2147483648 ~ 2147483647(-2^31 ~ 2^31-1) |
float | 4字节 | 3.4028235E38(2^128 - 1)~ 1.4E - 4(2^-149) |
long | 8字节 | 2^63 - 1~ -2^63 |
double | 8字节 | 1.7976931348623157E308 ~ 4.9E - 324(2^1024-1 ~ 2^-1074) |
为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。因为即使是一个方法内,也是存在作用域的,当离开了某些变量的作用域之后,这些变量对应的 Slot 空间就可以交给其他变量使用。但是这种机制有时候会影响垃圾回收行为,原因很简单,当离开某个作用域时,如果没有新的变量值覆盖之前作用域内的变量(指reference)空间,那么当垃圾回收时,则该引用对应的java堆中的内存则不允许被回收,因为局部变量表中还存在该引用。所以问题在于虚拟机并没有主动清理局部变量表中离开作用域的变量值,而是采用新盖旧的方法被动清理。
查看JVM垃圾收集过程
-verbose:gc
实例一代码
public class LocalVariableDemo01 {
public static void main(String[] args) {
// 1 start
byte[] placeholder = new byte[64 * 1024 * 1024];
System.gc();
// 1 end
}
}
执行结果:
[GC (System.gc()) 70697K->66594K(247296K), 0.0013343 secs]
[Full GC (System.gc()) 66594K->66389K(247296K), 0.0045586 secs]
上面的代码向内存填充了64MB的数据,然后通知虚拟机进行垃圾收集。在System.gc()运行后结果发现虚拟机并没有回收掉这64MB的内存,这是因为在执行System.gc()时,placeholder变量没有离开它的作用域,变量槽中还存有placeholder的引用,这时候的placeholder是无法回收的。
实例二代码:
public class LocalVariableDemo01 {
public static void main(String[] args) {
// 2 start
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
// 2 end
}
}
执行结果:
[GC (System.gc()) 70697K->66578K(247296K), 0.0009343 secs]
[Full GC (System.gc()) 66578K->66389K(247296K), 0.0048882 secs]
执行后,我们发现运行结果还是有64MB的内存没有被回收掉
虽然执行到gc()时,代码虽然已经离开了placeholder的作用域,但在此之后,再没有发生过任何对局部变量表的读写操作,placeholder原本所占用的变量槽还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联.
这种关联没有被及时打断,绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存但实际上已经不会再使用的变量,会导致内存的长时间占用。
解决办法:将placeholder设置为null;或后面定义一个变量,复用这个变量槽。
实例三代码:
public class LocalVariableDemo01 {
public static void main(String[] args) {
// 3 start
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a=1;
System.gc();
System.out.println("asdasd");
// 3 end
}
}
执行结果:
[GC (System.gc()) 70697K->66562K(247296K), 0.0016634 secs]
[Full GC (System.gc()) 66562K->853K(247296K), 0.0044665 secs]
asdasd
这次gc终于回收了placeholder对象
操作数栈
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(LIFO)栈。同局部变量表一样. 操作数栈的最大深度也在编译的时候被写入到Code属性的max_ stacks数据项之中,操作数栈的深度都不会超过在max stacks数据项中设定的最大值。。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1。64位数据类型所占的栈容量为2。操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的**入栈(push)和出栈(pop)**操作来完成一次数据访问。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写人和提取内容,也就是出栈和入栈操作。如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
注:Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
代码讲解:
public class OperandStackDemo01 {
public static void main(String[] args) {
byte i = 15;
int j = 8;
int k = i + j;
}
}
依次执行以下指令,获取字节码文件
首先编译,得到OperandStackDemo01.class文件
javac .\OperandStackDemo01
通过javap指令,得到字节码文件
javap -c .\OperandStackDemo01
执行结果
PS D:\desktop\ipcam-quickstart-sample> javap -c .\OperandStackDemo01.class
Compiled from "OperandStackDemo01.java"
public class com.ming.example1.OperandStackDemo01 {
public com.ming.example1.OperandStackDemo01();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
}
byte、short、char、boolean 内部都是使用int型来进行保存的。
从字节码就能解释了,为什么 byte类型相加,返回值类型必须是int
从上面的代码可知,通过bipush对操作数 15 和 8进行入栈操作,同时使用的是 iadd方法进行相加操作,其中 i 代表的就是 int,也就是int类型的加法操作。
执行流程如下所示:
1、首先执行第一条语句,PC寄存器指向的是0,也就是指令地址为0,然后使用bipush让操作数15入栈;
2、执行完后,让PC +1,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表1的位置,可以看到局部变量表的已经增加了一个元素;
为什么局部变量表不是从0开始的呢?
- 其实局部变量表也是从0开始的,但是因为0号位置存储的是this指针,所以说就直接省略了。
3、然后PC+1,指向的是下一行。让操作数8也入栈,同时执行store操作,存入局部变量表中;
4、然后从局部变量表中,依次将数据放在操作数栈中;
5、然后将操作数栈中的两个元素执行相加操作,并存储在局部变量表3的位置;
6、最后PC寄存器的位置指向10,也就是return方法,则直接退出方法
栈顶缓存技术
- 基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instructiondispatch)次数和内存读/写次数;
- 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
动态连接:
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令。
在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。 另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。说白了就是这个动态连接保存的是一个方法的符号引用,每次运行期间都会进行一次转换,将它转换成内存中方法的物理地址
扩展:在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池(Class文件中的常量池)获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中(放入运行时常量池中)。
public class DynamicLinkingTest {
int num;
String info;
public void test1(){
info="JVM";
this.test2();
}
public void test2(){
num=2;
}
}
使用javap -v命令,反编译之后
public class com.qf.DynamicLinkingTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#23 // java/lang/Object."<init>":()V
#2 = String #24 // JVM
#3 = Fieldref #6.#25 // com/qf/DynamicLinkingTest.info:Ljava/lang/String;
#4 = Methodref #6.#26 // com/qf/DynamicLinkingTest.test2:()V
#5 = Fieldref #6.#27 // com/qf/DynamicLinkingTest.num:I
#6 = Class #28 // com/qf/DynamicLinkingTest
#7 = Class #29 // java/lang/Object
#8 = Utf8 num
#9 = Utf8 I
#10 = Utf8 info
#11 = Utf8 Ljava/lang/String;
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/qf/DynamicLinkingTest;
#19 = Utf8 test1
#20 = Utf8 test2
#21 = Utf8 SourceFile
#22 = Utf8 DynamicLinkingTest.java
#23 = NameAndType #12:#13 // "<init>":()V
#24 = Utf8 JVM
#25 = NameAndType #10:#11 // info:Ljava/lang/String;
#26 = NameAndType #20:#13 // test2:()V
#27 = NameAndType #8:#9 // num:I
#28 = Utf8 com/qf/DynamicLinkingTest
#29 = Utf8 java/lang/Object
{
int num;
descriptor: I
flags:
java.lang.String info;
descriptor: Ljava/lang/String;
flags:
public com.qf.DynamicLinkingTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/qf/DynamicLinkingTest;
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: ldc #2 // String JVM
3: putfield #3 // Field info:Ljava/lang/String;
6: aload_0
7: invokevirtual #4 // Method test2:()V
10: return
LineNumberTable:
line 13: 0
line 14: 6
line 15: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/qf/DynamicLinkingTest;
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: iconst_2
2: putfield #5 // Field num:I
5: return
LineNumberTable:
line 17: 0
line 18: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/qf/DynamicLinkingTest;
}
SourceFile: "DynamicLinkingTest.java"
当编译Java程序的时候,会得到程序中每一个类或者接口的独立的class文件。虽然独立看上去毫无关联,但是他们之间通过接口(harbor)符号互相联系,或者与Java API的class文件相联系。当运行程序的时候,Java虚拟机装载程序的类和接口,并且在动态连接的过程中把它们互相勾连起来。在程序运行中,Java虚拟机内部组织了一张互相连接的类和接口的网。
class把他们所有的引用符号放在一个地方——常量池。每一个class文件有一个常量池,每一个被Java虚拟机装载的类或者接口都有一份内部版本常量池,被称作运行时常量池。运行时常量池是特定与实现的数据结构,数据结构映射到class文件中的常量池。因此,当一个类型被首次装载的时候,所有来自于类型的符号引用都装载到了类型的运行时常量池。
在程序运行的过程中,如果某个特定的符号引用将要被使用,它首先要被解析。解析过程就是首先根据符号引用查找到实体,再把符号引用替换成直接引用的过程。因为所有的符号引用都是保存在常量池中,所以这种解析叫做常量池解析。
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
方法返回地址也就是该方法最初被调时的地址。在一个方法执行完毕之后,必须返回到最初方法被调用时的位置,程序才能继续执行。当一个方法开始执行后,只有两种方式退出这个方法:
- 执行引擎遇到任意一个方法返回的字节码指令**(return)**。这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用 者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种 退出方法的方式称为“正常调用完成”。
- 在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成”。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
对象的访问定位的两种方式
建立对象就是为了使用对象,我们的Java 程序通过栈上的 reference 数据来操作堆上的具体对 象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄 和 ②直接指针 两种:
1. 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽ reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址(相当于代理),在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直 接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
五、栈和堆
虚拟机栈和堆是完全不同的两块内存区域,栈是私有的,堆是线程共享的,二者之间最大的区别在于存储的内容不同,还有就是他们两的储存逻辑有区别,存储在栈中的存储对象像是一种短期运行的记录,而堆中的存储对象相较于栈的,生命周期稍长一点,更像是一种长期运行的存储,生命的终结就是垃圾回收。
堆中主要存放对象实例。
栈(局部变量表)中主要存放各种基本数据类型、对象的引用。
从作用来说,栈是运行时的单位,而堆是存储的单位。栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。在 Java 中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
堆栈分离的好处
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,⼀个对象只对应了⼀个 4btye 的引用
1、从软件设计角度分析,栈代表了处理逻辑,堆代表了数据,这样分开,使得处理逻辑更清晰。分而治之的思想,这种隔离、模块化的思想体现在软件中的很多地方。
2、堆和栈的分离,使得堆的内容可以被多个栈共享(即多个线程访问同一个对象)。这种共享的收益很多,这种共享提供了一种有效的数据交互方式(共享内存),另一方面,堆中共享的常量和缓存可以被所有栈访问,节省了内存。
3、栈因为运行是需要,比如保存系统运行的上下文,需要地址段的划分,由于栈只能向上增长(从栈底到栈顶),因此限制住栈存储内容的能力和效率,而堆是根据需要可以动态增长的,因此栈和堆的拆分,使得堆动态增长成为可能,相应栈只需要记住堆中的一个地址即可。
六、本地方法栈(Native method stack)
本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常,不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。
一个Native Method就是一个java调用非java代码的接口方法。该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。
代码实例:
{
native public void Native1( int x ) ;
native static public long Native2() ;
native synchronized private float Native3( Object o ) ;
native void Native4( int[] ary ) throws Exception ;
}
1、标识符native可以与所有其它的java标识符连用,但是abstract除外。
这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。
native与其它java标识符连用时,其意义同非Native Method并无差别,比如native static表明这个方法可以在不产生类的实例时直接调用,这非常方便,比如当你想用一个native method去调用一个C的类库时。上面的第三个方法用到了native synchronized,JVM在进入这个方法的实现体之前会执行同步锁机制(就像java的多线程。)
2、如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法,同样的如果一个本地方法被fianl标识,它被继承后不能被重写。
为什么要使用Native Method?
与java环境外交互:
有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。
与操作系统交互:
VM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
JVM怎样使Native Method跑起来?
我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会回载一次。在这个被加载的字节码的入口维持着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的。
七、程序计数器(Program Counter Register)
程序计数器又叫程序计数寄存器。Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
可以看作在物理上实现程序计数器是通过一个叫寄存器来实现的,我们的程序计数器是Java对物理硬件的屏蔽和抽象,他在物理上是通过寄存器来实现的。寄存器可以说是整个CPU组件里读取速度最快的一个单元,因为读取/写指令地址这个动作是非常频繁的。所以Java虚拟机在设计的时候就把CPU中的寄存器当做了程序计数器,用他来存储地址,将来去读取这个地址。
作用:
程序计数器用于存储下一条指令的地址。详细的说PC寄存器是用来存储指向下一条指令的地址,也就是即将将要执行的指令代码。由执行引擎读取下一条指令。
具体可以参考:
1、写出java文件
源代码:
package com.ming.example1;
/**
* @author Yiming.Luo
* @date 2022/10/29
*/
public class StringConstantPoolDemo02 {
public static void main(String[] args) {
String s1=new String("asas");//语句1
String s2="asas"; //语句2
String s3=s1.intern(); //语句3
String s4=new String("as")+new String("as");
System.out.println(s1==s2);
System.out.println(s2==s3);
System.out.println(s1==s3);
}
}
2、使用javac命令获取class文件,或者直接复制idea生成文件
3、使用javap命令,将反编译的信息写入StringConstantPoolDemo02.txt文件
javap -v -p .\StringConstantPoolDemo02.class >StringConstantPoolDemo02.txt
4、得到源代码
Classfile /D:/APractice_file/JVM/Java-JVM/src/main/java/com/ming/example1/StringConstantPoolDemo02.class
Last modified 2022-10-29; size 959 bytes
MD5 checksum d117a76bbff92698125bd6d578c487b4
Compiled from "StringConstantPoolDemo02.java"
public class com.ming.example1.StringConstantPoolDemo02
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#27 // java/lang/Object."<init>":()V
#2 = Class #28 // java/lang/String
#3 = String #29 // asas
#4 = Methodref #2.#30 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Methodref #2.#31 // java/lang/String.intern:()Ljava/lang/String;
#6 = Class #32 // java/lang/StringBuilder
#7 = Methodref #6.#27 // java/lang/StringBuilder."<init>":()V
#8 = String #33 // as
#9 = Methodref #6.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#10 = Methodref #6.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#11 = Fieldref #36.#37 // java/lang/System.out:Ljava/io/PrintStream;
#12 = Methodref #38.#39 // java/io/PrintStream.println:(Z)V
#13 = Class #40 // com/ming/example1/StringConstantPoolDemo02
#14 = Class #41 // java/lang/Object
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 StackMapTable
#22 = Class #42 // "[Ljava/lang/String;"
#23 = Class #28 // java/lang/String
#24 = Class #43 // java/io/PrintStream
#25 = Utf8 SourceFile
#26 = Utf8 StringConstantPoolDemo02.java
#27 = NameAndType #15:#16 // "<init>":()V
#28 = Utf8 java/lang/String
#29 = Utf8 asas
#30 = NameAndType #15:#44 // "<init>":(Ljava/lang/String;)V
#31 = NameAndType #45:#46 // intern:()Ljava/lang/String;
#32 = Utf8 java/lang/StringBuilder
#33 = Utf8 as
#34 = NameAndType #47:#48 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #49:#46 // toString:()Ljava/lang/String;
#36 = Class #50 // java/lang/System
#37 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#38 = Class #43 // java/io/PrintStream
#39 = NameAndType #53:#54 // println:(Z)V
#40 = Utf8 com/ming/example1/StringConstantPoolDemo02
#41 = Utf8 java/lang/Object
#42 = Utf8 [Ljava/lang/String;
#43 = Utf8 java/io/PrintStream
#44 = Utf8 (Ljava/lang/String;)V
#45 = Utf8 intern
#46 = Utf8 ()Ljava/lang/String;
#47 = Utf8 append
#48 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#49 = Utf8 toString
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 println
#54 = Utf8 (Z)V
{
public com.ming.example1.StringConstantPoolDemo02();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=5, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String asas
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: ldc #3 // String asas
12: astore_2
13: aload_1
14: invokevirtual #5 // Method java/lang/String.intern:()Ljava/lang/String;
17: astore_3
18: new #6 // class java/lang/StringBuilder
21: dup
22: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
25: new #2 // class java/lang/String
28: dup
29: ldc #8 // String as
31: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
34: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
37: new #2 // class java/lang/String
40: dup
41: ldc #8 // String as
43: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
46: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
49: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
52: astore 4
54: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
57: aload_1
58: aload_2
59: if_acmpne 66
62: iconst_1
63: goto 67
66: iconst_0
67: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
70: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
73: aload_2
74: aload_3
75: if_acmpne 82
78: iconst_1
79: goto 83
82: iconst_0
83: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
86: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
89: aload_1
90: aload_3
91: if_acmpne 98
94: iconst_1
95: goto 99
98: iconst_0
99: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
102: return
LineNumberTable:
line 9: 0
line 10: 10
line 11: 13
line 12: 18
line 13: 54
line 14: 70
line 15: 86
line 16: 102
StackMapTable: number_of_entries = 6
frame_type = 255 /* full_frame */
offset_delta = 66
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
frame_type = 78 /* same_locals_1_stack_item */
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
frame_type = 78 /* same_locals_1_stack_item */
stack = [ class java/io/PrintStream ]
frame_type = 255 /* full_frame */
offset_delta = 0
locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
stack = [ class java/io/PrintStream, int ]
}
SourceFile: "StringConstantPoolDemo02.java"
特点:
1、线程私有,现在电脑普遍是支持多线程运行的,每个线程在CPU分配的时间片中执行程序,如果没有类似程序计数器的实现,是无法支持多线程运行的。
2、程序计数器是在Java虚拟机规范中规定的唯一一个不会存在内存溢出(OOM,OutOfMemoryError)的区。
比如其他的一些区(堆、栈、方法区之类的)他们都会出现内存溢出。
3、执行java方法时,程序计数器是有值的,执行native本地方法时,程序计数器的值为空。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined)。
4、程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。也是运行速度最快的存储区域。
5、它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成