【面经——JVM和类加载机制】讲故事一样讲述java的类加载机制和JVM

JVM是怎么分配内存的?

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。按照线程和线程共享来分,线程私有包括:程序计数器、虚拟机栈、本地方法栈。线程共享包括:java堆和方法区。
说的直白一些,只要我们创建了一个线程,那就有一套与之对应的程序计数器、虚拟机栈和本地方法栈。但是,一个进程中,只有一个java堆和方法区。一个进程中可以有多个线程,所以就会有多套与之对应的线程私有部分。可见,线程私有的部分和线程共享的部分是多对一的关系。下图是对上面文字的概括:
在这里插入图片描述程序计数器:记录的就是代码的执行顺序。
虚拟机栈:首先它的数据结构是栈,那么最小的单元就是栈帧。每个栈帧存放了:局部变量表,操作数栈,动态链接,方法的返回值,锁记录等。
本地方法栈:它的数据结构和虚拟机栈一样,也是栈,但是存放的是本地的方法,也就是hotspot虚拟机(默认)中的方法。在java中调用的是有native关键字修饰的方法。比如我们调用thread.start();方法,start()里面调用的是start0();方法。而start0()前面就有native关键字。但凡是带了native关键字的,说明java的范围达不到了,会去调用底层的c语言库!会进入本地方法栈。为什么需要调用本地方法呢?这里有一段历史,因为在java刚出现的时候,c和c++正在盛行,java想要生存,必须要调用一些用c和c++写好的方法。我们可以自己下载一个hotspot虚拟机源代码,可以发现,他就是一个c++工程。而现在,我们可以调用其他的接口,比如可以通过socket,或者RPC或者WebService(本质上都是用到socket网络传输这一套)去调用如PUP等的接口,去执行方法。
java堆:到目前为止,我们所有的java对象,都是存放在Java堆中的。但是未来可不一定,因为有一个逃逸理论,它分为线程逃逸和方法逃逸。这个理论主要说的是,假如,我可以保证,我在某个线程或者方法中新创建的对象,这个对象不会在别的线程或者方法中被用到,那么,我这个对象的生命周期其实就是这个方法或者是这个线程,那我就可以不用在堆上申请空间,而是在虚拟机栈中,因为虚拟机栈中申请的空间会随着方法的执行完毕出栈而出栈,就不需要额外的垃圾回收了。
堆又分为新生代、老年代、永久代(1.8之后改为元空间)。新生代又分为:伊甸园区、survive 0区、survive 1区。
在这里插入图片描述

方法区:对于一个类来说,类中除了含有方法之外,剩下的就是成员,类的本身的信息等等。既然类的方法已存放在了虚拟机栈的栈帧中,而java堆是存对象的,那么剩下的这些成员变量、类的本身的信息等等就都存放在方法区中。
 

什么是类加载机制?

类加载机制指的是:将class文件加载到内存,然后对数据进行校验、准备、解析和初始化,从而形成可以被虚拟机直接使用的Java类型。这个过程称为类加载机制。
对于类加载机制,需要明确的两点是:1.在程序运行期间完成,在编译期间不完成。2.加载的是 .class文件。

一个类的生命周期:
在这里插入图片描述这个图表明了一个类从被加载到内存开始,到卸载出内存为止,所经历的生命周期。

下面来说一下我对于类加载过程和jvm的运行时数据结构两者之间的联系

        类加载过程主要是包含:加载、验证、准备、解析、初始化五个阶段。当我们在某个ide环境下编写代码时,这里我以eclipse环境为例,当我们在eclipse环境中写java代码时,在写代码的过程中,如果我们编写的有错,eclipse会给出错误提示,它会自动进行编译,也叫预编译过程。要知道,我们现在所写的代码都是ASCII码文件。ACCII码文件是我们能直接看懂的文件,我们编写的文件是保存在src目录下,对应的都是xxx.java。
        当我们点击保存时,eclipse会自动在src目录下生成我们编写好的java文件,并且,会在该工程的bin目录下,生成与src目录下所有的java文件对应class文件。这时,.class文件里面保存的就不在是我们人能直接看懂的ASCII码了,而是字节码。我们类加载机制加载的也是 .class文件。
        类的加载阶段:通过一个类的全限定名获取该类的二进制字节流,将二进制字节流所代表的类的静态存储结构加载到方法区的运行时数据结构,并且在内存中(java堆)生成一个该类的对象。 (其实,执行完加载阶段,我们可以发现,这时已经占用了方法区和java堆)
        验证阶段:检验所加载的class文件的正确性。class文件的内容的头四个字节,表明了该文件是否是可被接收的class文件。再四个字节,写明了class文件的版本号等等。 (验证阶段就是为了检验一下class文件是否是可被接收的)
        准备阶段:在方法区中为类中定义的静态变量分配内存,并将其初始化为默认值(0或null)。 (可以发现,这个阶段又是对方法区进行操作)
        解析阶段:把常量池中的符号引用转换为直接引用的过程。 (什么是符号引用转为直接引用呢?我的理解是:在计算机底层,它只有0和1,比如用00表示加,01表示减,10表示乘,11表示除;而在计算机的上一层面,也就是汇编层面,用add表示加,用sub表示减;在我们的Java层面(ASCII码层面),用+表示加,-表示减,x表示乘,/表示除。将符号引用变为直接引用,其实就是一个逆向过程,也就是从java层面一直逆向到计算机层面。为的就是计算的方便,快速,因为现在不进行+变为00,之后肯定还有过程需要将+变为00,因为计算机只认识0和1,不认识别的。所有的我们现在编写的类,接口,方法等都是ACCII码,最终都会编程计算机指令。)
        初始化阶段:因为前面四个阶段已经在java堆和方法区进行了空间申请,现在还有一个地方没有申请空间,那就是虚拟机栈。所以,初始化阶段主要是针对虚拟机栈的。这个地方,书上讲了很大一堆,什么接口中有default方法,一个类实现了该接口并且当它被初始化的时候,那实现的接口先进行初始化等等,说了一系列内容。我的理解是:假如在初始化某个方法的时候,突然发现,方法里面new了一个对象,但是这个对象的类还没有被加载过,那么这时候,就会把刚才加载-验证-准备-解析-初始化再做一遍。说白了,在我初始化过程中,如果我用到了某个没有加载的类,那我就把那个类也进行个完整的类加载过程,加载完之后,我才能保证我这个方法里面的代码能够继续执行。否则,那就继续执行就ok。用到就加载,不用到就不加载。java本身就已经包含了很多的类,假如,每次运行都会把它们进行一次加载,那是很耗时并且很浪费内存的。所以,只有当我们用到了某个类的时候,才会进行一次加载,只要加载一次,之后就直接new对象就好,就不需要再次加载了。

这就是我理解的类加载过程。

双亲委派机制

在说类加载过程的初始化阶段的时候,其实已经提到了双亲委派机制。java的类加载器就遵循双亲委派机制。
在这里插入图片描述
子类要加载一个.class文件,要先去要求父类加载器区加载,父类再要求爷类去加载。若爷类加载不到,就交由父类加载,父类加载不到,交由子类加载。只要一加载到,就立马结束。这个过程就是双亲委派机制。
对于java来说,和上图一样,会有三种类加载器。
在这里插入图片描述
启动类加载器就是上面所说的爷类加载器,它加载的是java.javax.sun开头的类或者说\jre\lib目录下的类库;
扩展类加载器就是上面所说的父类加载器,它加载的是\jre\lib\ext目录下的类库;
启动类加载器就是上面所说的子类加载器,它加载的是我们自己编写好的类;

为什么要进行双亲委派机制呢?
1.防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2.避免对java原来的类进行破坏。
假如,我们在某个工程中,写了一个java.lang包,包里面写了一个类String,可以发现,我们已经和原来的String重名了,假如我们编写程序 String str = new String();调用这个类的时候,编译是不会报错的,但是点击运行的时候,就会报错。
在这里插入图片描述在这里插入图片描述

原因就是,因为双亲委派机制,先看子类,因为子类含有该String类,但是还没有被加载,然后先交由父类加载,发现父类加载器也有String类,并且父类加载器和子类加载器是相同的加载器,那么直接报错,因为这相当于破坏了java原有的类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值