JVM(一)

一、平常开发出来的.java文件是怎么编译成class文件的?

1、将java文件编译、打包成class文件

方式一、采用maven进行打包
利用maven的package打包生命周期对项目进行打包。

//cmd进入项目目录
//先clean一下项目,主要是删除项目生成的一些项目文件,然后在用package进行打包
mvn clean     //编译
mvn package   //打包

或者可以在idea中直接点击package一键打包。

然后会生成打包后的jar包。(之后就可以手动部署到服务器上去)

方式二、使用jar命令进行打包

1)使用cmd找到项目编译输出的路径
在这里插入图片描述

在这里插入图片描述
(2) 在该目录下运行命令 jar -cvf helloworld.jar .
-c 表时要创建文件
-v 在控制台打印压缩详情
-f 指定压缩文件名
helloworld.jar 文件名可以自定义
. 表示helloworld目录下的所有文件,这里一定要写“.”,其他可能出错

方式三、pom文件加上插件自动打包

    <!--打包插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

然后在构建的时候使用mvn命令进行编译打包。(这种方式可以用jenkins使用mvn命令对gitlab的代码进行打包部署)

2、使用类加载器加载编译好的class文件,并进行执行内存中相应的类

最后会由JVM的字节码执行引擎来执行加载到内存中的类,例如你的代码中有一个“main()”方法,那么JVM就会从这个“main()”方法开始执行里面的代码。他需要哪个类的时候,就会使用类加载器来加载对应的类,反正对应的类就在“.class”文件中。

那么细化一下,class文件是怎么加载到JVM内存中的?类是怎么分块的存储在JVM内存中?JVM是怎么对一段代码进行执行的?

二、Class文件是怎么加载到JVM内存中的?

上面中粗略的说明了JVM通过类加载器对class文件进行加载到JVM内存中。下面会对类加载过程进行细说

一个class文件要被类加载器加载到JVM内存中,需要有以下几步:
加载、验证、准备、解析、初始化、使用、卸载等7步

1、加载

首先需要知道,什么时候JVM会去加载一个类(即去class文件中加载这个类)——————就是使用到这个类的时候才去class文件中加载他到内存中。

举一个例子:例如你有一个Demo01的类,其中有main方法(程序入口),此时在JVM启动后main方法一定会被加载到JVM中(每个JVM进程启动都需要有一个程序入口,一般是main方法,比如springboot项目中的启动类等),然后以该入口开始执行代码,如果在执行过程中需要加载其他类,就会去class文件中加载相应的类到JVM内存中。

就如下面JVM进程启动时会将Demo01加载到内存中,并以main方法为入口继续向下执行,后面需要实例化一个Demo02的对象,此时就会触发JVM去class文件中加载Demo02这个类。

public void Demo01{
    public static void main(String[] args){
        Demo02 demo = new Demo02();
    } 
}

此时提一个问,类加载到内存就可以直接使用了?答案是否定的,一个类要被真正的使用还需要其他步骤

2、验证

该步骤主要是根据Java虚拟机规范来验证你加载进来的“.class”文件的内容是否符合规范,只要内容符合规范才能被后续使用

3、准备

给类、类变量分配一定内存,并给类变量赋予初始值(例如0、null等)

public void Demo02{
    public static int flushIn;
}

此时就会给Demo02类、flushIn分配一定的内存,然后再给flushIn赋予初始值0.。

4、解析

该阶段是将符号引用转换为直接引用,该阶段涉及jvm底层实现,后面再1对其进行细说。

5、初始化(核心阶段)

前面说到,在准备阶段会给类和类变量分配内存和初始值,那如果类变量是这样:

public void Demo02{
    public static int flushIn = 100;
}

此时100则是在初始化阶段才会给flushIn赋值的,而且在这个阶段也会对静态代码块进行执行。

那么初始化有什么规则?

(1)、初始化一个类时,如果其父类还没有初始化必须先初始化其父类

(2)、main方法的类在JVM启动后必须完成加载到初始化过程

(3)、在代码中使用“new Demo02()”时,会触发JVM加载到初始化该类。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

类加载器和双亲委派机制

1、类加载器

有四种类加载器:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器

(1)、启动类加载器

Bootstrap ClassLoader,加载jdk安装路径lib目录下的核心类库

(2)、扩展类加载器

Extension ClassLoader,加载jdk路径下lib\ext目录下的类库

(3)、应用程序类加载器

Application ClassLoader , 加载ClassPath环境变量所指定的路径中的类(可以认为是加载你写好的java代码到内存中)

(4)、自定义类加载器

自己定义类加载器去自定义的加载类库

2、双亲委派机制

JVM的类加载是有亲子层级结构的,然后基于这个亲子层级结构,就有一个双亲委派的机制。

图中启动类加载器属于顶级类加载器,扩展、应用程序、自定义等依次排序下来。

那么什么叫做双亲委派机制?

————————就是假设你要加载一个类,此时不是由当前类加载器去直接加载,而是全部委派给自己的父类,依次委派最后启动类加载器拿到所有要加载的类,挑选自己要加载的类(属于其加载范围的类),然后剩下的依次交给下面的类加载器进行选择加载。(可以避免重复加载某些类)

说到双亲委派机制,其实也存在一定的弊端,所以有一些框架的类加载机制采用的是反双亲委派机制。例如tomcat等,这个后面再做分析

aa

小问题1:如何对“.class”文件处理保证不被别人拿到后反编译获取公司的源代码?
————首先你在编译时,可以使用一些小工具对字节码进行加密或者做混淆等处理(有很多第三方公司有提供企业级的字节码文件加密服务),然后在类加载的时候对加密的类采用自定义的类加载器进行解密操作。这样就能保证源代码不被被人窃取了。

小问题2:tomcat是怎么设计其类加载机制的?

————首先,tomcat自定义了很多类加载器,其类加载体系如下:

tomcat中自定义了Common、Catalina、Shared等类加载器,其实就是tomcat用来加载机子的一些核心基础类库的。

然后tomcat为每个部署在里面的Web应用都有一个自己对应的WebApp类加载器,负责加载我们部署的这个Web应用的类。

JSP类加载其则是用来加载JSP文件的。

tomcat采用的是打破双亲委派机制的。

每个WebApp独立负责加载自己对应的那个Web应用的class文件,也就是我们写好的某个系统打包好的war包中的class文件,并不会传导给上层类加载器去加载。

三、JVM的内部分区及类是怎么分块的存储在JVM中的?

前面说明了一个java文件是怎么被加载到JVM内存中,那这些类被加载到内存中又是放在哪里?是怎么放的?


一、JVM的区域分区

jvm运行时数据区域:

如上,JVM的区域大致被分为5个部分,其中又分为线程共享区域和非共享(线程独立)区域

共享:方法区、堆(线程共享,不安全)

独立:栈(虚拟机栈)、本地方法栈、程序计数器

其中各个区域放置的东西不同;

1、方法区:运行时常量池、存储已被虚拟机加载的类信息(类的版本,字段,方法,接口等描述信息),常量,静态变量,即时编译器编译后的代码等数据。(常量池无法再申请到内存、方法区内存满了则会抛出内存异常OutOfMemoryError)

注意:“方法区”是1.8之前的称呼,jdk1.8之后,改为“Metaspace”(元空间)。

2、程序计数器:当.java文件被编译成.class文件后,一段程序员看的懂的代码则被变成了JVM执行引擎才看的懂的字节码文件。

例如:

最终被编译成如下的字节码指令:

public java.lang.String getName();
 descriptor: ()Ljava/lang/String;
 flags: ACC_PUBLIC
 Code:
 stack=1, locals=1, args_size=1
 0: aload_0
 1: get_field #2
 4: areturn

然后这些字节码指令会被JVM执行引擎一步一步的执行,那么程序计数器就是用来记录下一步要执行哪一行的指令

例如,此时线程1在执行执行第6行指令,此时程序计数器就会指向7(下一行指令)。

如果cpu时间片轮换,导致线程1不能继续执行,此时线程2获取到cpu资源执行完后释放cpu资源,这时线程1又重新获得到cpu资源。那这里提一个问题?线程1是怎么知道自己上次执行到哪行指令的?

————————这时候记录上次线程1执行到哪行指令的就是程序计数器(程序计数器是线程独有的)

3、本地方法栈:

本地方法:一般是native修饰的类,一般由c、c++编写,本地方法栈与虚拟机栈的作用相似,它们之间的区别在于java虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈为虚拟机栈使用到的Native方法服务(在SUN HotSpot虚拟机中将虚拟机栈和本地方法栈合二为一)。

4、虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期与线程相同,虚拟机栈描述的是java方法执行的内存模型:每一个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法从调用到执行 完成的过程就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表中存放着编译时期的各种基本数据类型(long/double,int,boolean,byte,char,short,float),对象引用(reference类型,它不等于对象本身,可能是指向对象的一个指针,也可能是一个指向对象的一个句柄或者其他与位置有关的信息)和returnAddress类型(指向了一条字节码指令的地址)。其中long和double类型的数据会占用两个局部变量空间(slot),其余数据类型只占用一个局部变量空间,局部变量表在编译时期完成分配,在方法运行期间不会改变局部变量表的空间大小。
在java虚拟机栈中,规定了两种异常状况:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度,将抛出该异常。
OutOfMemoryError:如果虚拟机栈可以动态扩展,当无法申请过到足够的内存,就会抛出该异常。

5、堆:

在Java虚拟机中,java堆是在这部分内存中最大的一块,java堆是被所有线程共享的一块内存区域,这部分内存用于存放对象的实例,几乎所有对象的实例都在这里分配内存。java堆同时也是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆(Garbage Collected Heap)”,从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以java堆还细分为新生代和年老代,再细致点就是Eden空间,From Survivor空间,To Survivor空间等,从内存分配的角度来看,java堆有时还回创建一个多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),但是无论如何分配,都与存放内容无关,都存放的是对象实例,只是为了更有利回收和分配内存,对java堆的内存,可以是一块连续的内存,也可以是不连续的内存,只要逻辑上连续即可,在实际中,这部分内存既可以是固定的内存,也可以是可扩展的(可以通过-Xmx和-Xms控制),如果在堆中没有内存完成实例的分配,并且堆也无法扩展,将会抛出OutOfMemoryError异常。

6、其他内存区域

其实在JDK很多底层API里,比如IO相关的,NIO相关的,网络Socket相关的
如果大家去看他内部的源码,会发现很多地方都不是Java代码了,而是走的native方法去调用本地操作系统里面的一些方法,
可能调用的都是c语言写的方法,或者一些底层类库
比如下面这样的:public native int hashCode();
在调用这种native方法的时候,就会有线程对应的本地方法栈,这个里面也是跟Java虚拟机栈类似的,也是存放各种native方
法的局部变量表之类的信息。
还有一个区域,是不属于JVM的,通过NIO中的allocateDirect这种API,可以在Java堆外分配内存空间。然后,通过Java虚拟
机里的DirectByteBuffer来引用和操作堆外内存空间。
其实很多技术都会用这种方式,因为有一些场景下,堆外内存分配可以提升性能。

二、一个类是怎么被分配到这些区域的?

对于这一部分的详细可以看我的另一篇文章:https://blog.csdn.net/qq_37142346/article/details/79141210

四、总结

1、方法走完,引用消失,堆内存还未必消失。好多人在做报表导出的时候,就会在for循环里不断的创建对象,很容易造成堆溢出,请问这种大文件导出怎么破?

——不要在for循环中创建对象,可以在外面建一个集合对象,for循环里对集合对象进行操作改数据即可。(在循环中可能因为循环次数太多,创建太多对象,导致jvm内存不足)

2、java支持多线程,每个线程都有自己的java虚拟机栈和本地方法栈?新建的实例在堆内存,实例变量也在堆内存?

——都是

3、加载子类需要先加载其父类

4、Object Header(4字节) + Class Pointer(4字节)+ Fields(看存放类型),但是jvm内存占用是8的倍数,所以结果要向上取整到8的倍数

5、为什么必须要一级一级类加载器的往上找,直接从顶层类加载器开始找不就行了吗?

——其实关于这个问题,不用过于纠结,每一层类加载器对某个类的加载,上推给父类加载器,到顶层类加载器,如果发现自 己加载不到,再下推回子类加载器来加载,这样可以保证绝对不会重复加载某个类。

至于为什么不直接从顶层类加载器开始找,那是因为类加载器本身就是做的父子关系模型,你想一下Java代码实现,他最底下的子类加载器,只能通过自己引用的父类加载器去找。如果直接找顶层类加载器,不合适的,那么顶层类加载器不就必须硬编码规定了吗?

这就是一个代码设计思想,保证代码的可扩展性。

6、tomcat本身是java程序,那么tomcat的实现程序的class是由应用类加载器加载的,用户自己的java程序war包,放入tomcat的程序的classpath中,这样用户的程序和tomcat的程序都是由应用类加载器加载了,也就是处于一个jvm中了

7、-XX:+TraceClassLoading 可以看加载了哪些类,动手实验了一下,jre\lib\rt.jar下的类全部加载了,其他都是用到时候加载。

8、先加载class文件进内存,执行的时候直接使用加载好的类。

 

上面对于JVM内存区域和基本概念进行了说明,这里提出一个问题?如果我们不停的往JVM内存中放数据,然而JVM的内存又是有限的,那么有一天JVM内存满了怎么办?
————答案当然是JVM的垃圾回收机制。他是一个后台自动运行的线程,你只要启动一个JVM进程,他就会自带这么一个垃圾回收的后台线程。 这个线程会在后台不断检查JVM堆内存中的各个实例对象,该机制可以对堆中的无用的对象进行回收。对于这方面的在下面再进行说明

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值