Java8的JVM(一)

作者:梁开权,叩丁狼高级讲师。原创文章,转载请注明原文章地址,谢谢!

初识Java8的JVM

一.JVM是何方神圣,为何如此神通广大?

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够一次编译,到处运行的原因。

二.支持的数据类型

基本数据类型

byte://1字节有符号整数的补码

short://2字节有符号整数的补码

int://4字节有符号整数的补码

long://8字节有符号整数的补码

float://4字节IEEE754格式单精度浮点数

double://8字节IEEE754格式双精度浮点数

char://2字节无符号Unicode字符


Java8的JVM(一)_第1张图片
JVM

几乎所有的Java类型检查都是在编译时完成的。上面列出的原始数据类型的数据在Java执行时不需要用硬件标记。操作这些原始数据类型数据的字节码(指令)本身就已经指出了操作数的数据类型,例如iadd、ladd、fadd和dadd指令都是把两个数相加,其操作数类型分别是int、long、float和double。虚拟机没有给boolean(布尔)类型设置单独的指令。boolean型的数据是由integer指令,包括integer返回来处理的。boolean型的数组则是用byte数组来处理的。虚拟机使用IEEE754格式的浮点数。不支持IEEE格式的较旧的计算机,在运行Java数值计算程序时,可能会非常慢。

引用数据类型

object//对一个Javaobject(对象)的4字节引用
return Address//4字节,用于jsr/ret/jsr-w/ret-w指令
注:Java数组被当做object处理。虚拟机的规范对于object内部的结构没有任何特殊的要求。在Sun公司的实现中,对object的引用是一个句柄,其中包含一对指针:一个指针指向该object的方法表,另一个指向该object的数据。用Java虚拟机的字节码表示的程序应该遵守类型规定。Java虚拟机的实现应拒绝执行违反了类型规定的字节码程序。Java虚拟机由于字节码定义的限制似乎只能运行于32位地址空间的机器上。但是可以创建一个Java虚拟机,它自动地把字节码转换成64位的形式。从Java虚拟机支持的数据类型可以看出,Java对数据类型的内部格式进行了严格规定,这样使得各种Java虚拟机的实现对数据的解释是相同的,从而保证了Java的与平台无关性和可移植性。

三.原理

JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。


Java8的JVM(一)_第2张图片
JVM运行原理

java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

JVM的内存模型

Java8的JVM(一)_第3张图片
JVM内存模型
主要作用
栈区

线程私有,生命周期与线程相同。每个方法执行的时候都会创建一个栈帧(stack frame)用于存放局部变量表、操作栈、动态链接、方法出口。

存放对象实例,所有的对象的内存都在这里分配。垃圾回收主要就是作用于这里的。

堆中的字符串常量区是专门用于存储字符串常量(直接使用双引号声明的),内容相同的字符串常量都是直接引用相同的引用地址

堆得内存由-Xms指定,默认是物理内存的1/64;最大的内存由-Xmx指定,默认是物理内存的1/4。
默认空余的堆内存小于40%时,就会增大,直到-Xmx设置的内存。具体的比例可以由-XX:MinHeapFreeRatio指定
空余的内存大于70%时,就会减少内存,直到-Xms设置的大小。具体由-XX:MaxHeapFreeRatio指定。
因此一般都建议把这两个参数设置成一样大,可以避免JVM在不断调整大小。

程序计数器

这里记录了线程执行的字节码的行号,在分支、循环、跳转、异常、线程恢复等都依赖这个计数器。

元空间

元空间是Java8开始才出现的内存区域,取代了之前的永久区
元空间中存储的是类的元数据信息(metadata),只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中

类的元数据信息转移到Metaspace的原因是永久区很难调整。永久区中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且应该为永久区分配多大的空间很难确定,因为永久区的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。

此外,在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在永久去中的类的元数据信息。从永久区分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理,而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停。

永久区的移除对最终用户意味着什么?

由于类的元数据可以在本地内存(native memory)之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。

注:永久代的移除并不意味者类加载器泄露的问题就没有了。因此,你仍然需要监控你的消费和计划,因为内存泄露会耗尽整个本地内存,导致内存交换(swapping),这样只会变得更糟。

JVM中方法调用的值传递

本节中我们主要讨论两个内容:
1:基本类型的值传递
2:引用类型的值传递

一.基本类型的值传递

示例代码:

public class ByValueDemo1 {
    public static void main(String[] args) {
        int num = 10;
        changeValue(num);
        System.out.println("main:num="+num); // ?
    }
    public static void changeValue(int num) {
        num = 5;
        System.out.println("changeValue:num="+num); // ?
    } 
}

思考:
最后打印出的结果是多少?
通过运行代码我们可以发现打印出来的内容是:
main:num=10
changeValue:num=5
为什么呢?
1:每一个方法被调用,都会在栈内存中,开辟一块用于当前方法使用的空间(栈帧),该空间中存储了当前方法中用到的局部变量,也就是说JVM调用main方法时,在栈内存中开启了一块main方法使用的内存空间,里面有个叫num的变量,该变量被赋予了值为10
2:在调用changeValue方法时,把main方法中num变量存储的值复制了一份传递给了changeValue方法中的num变量,此时changeValue方法中num变量的值也是10,但是main方法中的num和changeValue方法中的num是相互独立的两个空间
3:把changeValue方法中num的值改成5,然后执行了打印语句,打印的内容是changeValue方法中的num变量的值,所以为5
4:changeValue方法调用结束后,该方法的内存从栈内存中消失,这块内存中所有的数据也销毁了
5:打印main方法中num变量的值,而main方法中num变量的值重来就没有改变过,所以依然不变还是10
我们一起来看看这些数据在JVM内存中主要分布和操作,如下图:

Java8的JVM(一)_第4张图片
基本类型的值传递

二.引用类型的值传递

示例代码:

public class User {
    long id;
}
public class ByValueDemo2 {
    public static void main(String[] args) {
        User u = new User(10L);
        changeValue(u);
        System.out.println("main:u.id="+u.id); // ?
    }
    public static void changeValue(User u) {
        u.id = 5;
        System.out.println("changeValue:u.id="+u.id); // ?
    } 
}

思考:
最后打印出的结果是多少?
通过运行代码我们可以发现打印出来的内容是:
main:u.id=5
changeValue:u.id=5
为什么同样是传递值调用方法,但是结果却完全不一样呢?
1:JVM调用main方法时,在栈内存中开启了一块main方法使用的内存空间,里面有个叫u的变量,该变量存入的是堆内存中User对象的内存地址,假设该地址是:0xabc,0xabc地址中有块叫id的内存空间,用于存储id的值
2:在调用changeValue方法时,把main方法中u变量存储的值复制了一份传递给了changeValue方法中的u变量,此时changeValue方法中num变量的值也是0xabc,同理main方法中的u变量和changeValue方法中的u变量也是相互独立的两个空间
3:changeValue方法中把堆内存上0xabc地址中id的变量的值改成5,然后再访问0xabc地址中id空间的值,当然打印出来是5,然后changeValue方法结束,同样该栈帧销毁
4:在main方法中也再次访问0xabc地址中id的变量的值,该值在之前的changeValue方法中已经被改成了5,所以在main方法也打印出来的结果自然也是5啦
我们一起来看看这些数据在JVM内存中主要分布和操作,如下图:

Java8的JVM(一)_第5张图片
引用类型的值传递

Java8的JVM(一)_第6张图片
WechatIMG7.jpeg

你可能感兴趣的:(Java8的JVM(一))