JVM运行以及内存分配

大家好,我今天给大家分享一下,:JVM运行以及内存分配

 

1.背景介绍

Java核心四要素:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。

JVM(Java Vitual Machine)Java虚拟机,用软件虚拟的一台计算机,可以把字节码文件翻译成机器指令。

Java API(Application Programming Interface):简单来说我们编程就是站在前人肩膀上,通过前人编写好的代码来实现自己程序的功能,API就是使用前人代码的途径,是程序的接口。理解此概念需要理解jar包依赖的概念。

Java编译时环境:我们使用Java编程语言编写出java文件,使用Java编译器编译成class字节码文件,字节码文件可以通过网络传给别人(打包)也可以自己使用。

Java运行时环境:将其放在类装载器中,验证后就会通过其中的API来调用Java类库,通过java命令运行程序时就会启动JVM,然后就可以通过不同系统上的JVM将class文件转换为机器可以识别的二进制语言,起到调用其他程序的作用。

JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。

在Java平台的结构中,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;

在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。

 

2.知识剖析

JVM的体系结构

(1) Class Loader类加载器:负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。

① 定位和导入二进制class文件
② 验证导入类的正确性
③ 为类分配初始化内存

④ 帮助解析符号引用.

(2) Native Interface本地接口:本地接口的作用是融合不同的编程语言为Java所用,现在很少使用了

(3) Execution Engine 执行引擎:执行包在装载类的方法中的指令,也就是方法。

(4) Runtime data area 运行数据区:

      虚拟机内存或者Jvm内存,从整个计算机内存中开辟一块内存存储Jvm需要用到的对象,变量等,运行区数据有分很多小区,分别为:方法区,虚拟机(java)栈,本地方法栈,堆,程序计数器。

①方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间。静态变量+常量+类信息+运行时常量池存在方法区中,实例变量存在堆内存中。在jvm创建时就被加载进来。

②java堆也是线程共享的区域,我们的类的实例就放在这个区域,可以想象你的一个系统会产生很多实例,因此java堆的空间也是最大的。如果java堆空间不足了,程序会抛出OutOfMemoryError异常。

③java栈是每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈,这里面有很多细节,我们今天不讲。如果java栈空间不足了,程序会抛出StackOverflowError异常,想一想什么情况下会容易产生这个错误,对,递归,递归如果深度很深,就会执行大量的方法,方法越多java栈的占用空间越大。基本类型的变量和对象的引用变量以及类文件方法等都是在函数的栈内存中分配。

④本地方法栈角色和java栈类似,只不过它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。

⑤程序计数器

每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

 

3.常见问题 

首先我们来看一个例子

首先是pojo类

 

public class Person {
    String name;
    int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //构造方法存放在方法区
    public Person() {

    }

//    @Override
//    public String toString() {
//        return "Person{" + "name=" + name + ",age=" + age + "}";
//
//    }

    //非特殊的类方法存放在栈区
    public void showInfo() {
        System.out.println("姓名:" + name);
        System.out.println("年龄:" + age);

    }
}

一个小的demo

 

public class demo2 {
    public static void main(String[] args) {
        Person p1=new Person();
        p1.name="zhangsan";
        p1.age=18;
        System.out.println(p1);
        p1.showInfo();

        Person p2=new Person();
        p2.name="lisi";
        p1=p2;
        p1.showInfo();

        p1.name="wangwu";
        p1.age=25;
        p1.showInfo();
        p2.showInfo();
    }
}

运行结果为,大家可以先思考一下,再看看是不是和你认为的结果一致

JVM运行以及内存分配_第1张图片

那么问题就来了

此例子中第一个输出的是什么?

在此例子中,内存是怎么分配的?

为什么最后p2输出的姓名会是wangwu?

4.解决方案

 

Person p1=new Person();

①在栈区中给p1变量分配存储空间;

②在堆中给对象分配一块内存,给对象的各成员变量进行默认的初始化,所以此时p1的年龄是0;

③把堆中对象的内存起始地址赋值给变量p1,这代表p1存储的实际上一个内存地址,也就是第一个sout输出的结果。所以在堆中的才是真正的对象,p1准确的称为对象的引用,它只是对象名;

p1.name="zhangsan";

在方法区的常量区分配一块区域,将"zhangsan"存进去,再将其地址存在堆区的name变量中

p1.age=18;

与上面稍微有点区别,因为age是基本类型,所以其直接将堆区中age的存储值从0变为18;

p1.showInfo();

在栈区中划分一块区域给此方法,方法结束则释放内存;

Person p2=new Person();

在栈区内存中划分一块区域给p2,其余同上;

p1=p2;

①将p2的引用赋值给p1,此时两者指向同一个引用,这就体现出了堆区的线程共享;

②p1原来在堆区中的内存地址已无变量引用,等到垃圾回收器发现它,就会被释放,而原本name中储存的”zhangsan"也会因无变量引用而被清理;

p1.name="wangwu";

此时将"wangwu"在方法区储存,并将地址赋值给堆区中的name,name是p1,p2共同指向的,所以p1单方面改变name后,p2的name也改变了。此时,方法区中的"lisi"因为没有变量引用就会被清理;

这就是为什么最后p2输出的姓名是wangwu而不是lisi了;

p1.age=25;

原理同上,只是它是直接在堆区中改变,所以不存在垃圾清理的机制。

5.参考文献

https://www.cnblogs.com/leefreeman/p/7344460.html

https://blog.csdn.net/luomingkui1109/article/details/72820232

6.更多思考

① 那个垃圾回收器到底是什么时候才工作的

其实垃圾回收器是一直都在工作的,只是在回收没有引用指向的内存时我们才感觉得到它的存在,就像城管其实一直都在巡视街道,而不是我们看到他没收无证商贩的作案工具时他才在工作一样。

②那之前的p1的引用没有被调用的话 ,是需要等待一会,才能被清理吗?

可以把垃圾回收器看成是一个流动巡逻的城管,将整个内存区域看成是巡逻的区域,将没有引用指向的内存看成是无证小贩,所以,当城管巡逻看见了无证小贩,就会去处理,如果没看到就不会管,小贩也就一直在卖东西。但是城管什么时候遇见小贩,就要看二者的相对位置和缘分了,巡查区域越小,巡查速度越快,发现小贩的速度肯定越快,相对应的,垃圾清理也就越快,所以并没有固定的时间。

③循环引用的时候,垃圾回收器如何发现并回收?

强引用并不能保证对象不被回收。垃圾回收机制除了检查对象是否被引用外,还要看对象是否被至少一个GC roots对象直接或者间接引用。GC roots对象包括以下一些类容:

a 每个线程当前的函数调用栈,从栈顶到栈底的每个函数里的局部变量。

b 静态的变量

c 被jni中引用到的变量。

所以,两个循环引用的对象,虽然都存在一个强引用,但是不被任何GC root对象直接或者间接引用到,垃圾回收机制能够发现这个问题。

你可能感兴趣的:(JVM运行以及内存分配)