带着新人看java虚拟机01(推荐)

1.前言(基于JDK1.7)  

最近想把一些java基础的东西整理一下,但是又不知道从哪里开始!想了好久,还是从最基本的jvm开始吧!这一节就简单过一遍基础知识,后面慢慢深入。。。

水平有限,我自己也是很难把jvm将清楚的,我参考一本书《深入java虚拟机第二版》(版本比较老,其实很多大佬的博客都是参考的这本书的内容。。。),电子档pdf文件链接:https://pan.baidu.com/s/1bxs4i0gnVpz7Lkjl2fxS9g 提取码:n5ou ,有兴趣的小伙伴可以自己下载自己好好看看;

所谓jvm,又名java虚拟机。我们平常写java程序的时候几乎是感觉不到jvm的存在的,我们只需要根据java规范去编写类,然后就可以运行程序了,当然只有我们程序出现bug了,我们才有可能在控制台上看到一些jvm报错的信息,比如内存溢出异常等。

java之所以能够跨平台,就是因为jvm屏蔽了各个操作系统之间的差异,举个形象的例子,我们手机要充电吧,但是充电的方式有很多种,你可以直接数据线插到插座充电,也可以用数据线插到电脑USB口充电,一个是电脑一个是插座,为什么都能给手机充电呢?原因就是有数据线屏蔽了插座和电脑的差异,对于手机来说,它是看不到数据线另外一头连接的是什么设备,只知道有电通过数据线向自己传过来就ok了,顺便一提,这也是所谓的适配器的原理!

开始之前首先要明确一点,每一个java程序运行就会创建一个jvm实例!比如我同时在eclipse中同时运行三个程序,那么就会创建三个jvm实例,三个程序运行于自己的jvm中,互不干扰,当程序运行完毕,那么jvm也会销毁。

2.简单看看类加载过程

大家知道一个类加载到jvm大概是经过了几个步骤的吧!编译成字节码文件,加载,链接(验证,准备,解析),初始化....,我就简单的用下面这个图一起看看;

在这里,我们重点看看字节码文件到jvm这一段,为什么字节码文件能够被加载到jvm中呢?类加载器又是什么呢?加载的具体过程又是什么呢?链接,初始化又具体的是在做些什么事呢?Class对象又是什么鬼?jvm中的具体结构又是什么样子的,各有什么用处?假如执行一个类中的方法,在jvm中到底是什么流程呢?等等很多问题

这些问题有的是了解一点,有的是真不知道,反正就是迷迷糊糊的一个类就加载成功了,然后我们就能成功调用那些方法了,平常用起来很舒服,但是细细想来难道不觉得奇怪吗?

反正我最初看到jvm的时候,最想吐槽的一句话就是:玛德,为什么啊?我感觉我已经要化身成十万个为什么了,咳咳,不说废话了,开始往后学吧!

下面我大概说一下这些步骤到底是做了什么事,有个大概的流程,然后我们慢慢的深入探究每一个步骤到底是干了什么事!

2.1 编译器编译

这个没什么好说的,由于java是静态语言,在执行java程序之前会先把我们写的java文件给转化成特殊的二进制码的形式,编译器就是做这个转化的工作的工具,而且在我们写代码的时候,还没运行程序之前,就会报错,在某处代码下面会有红线标识,做这个工作的就是编译器,还有最重要的源文件中泛型,是会在编译器编译这个阶段就会进行擦除,所以字节码文件中是没有任何泛型信息的;

顺便提一下动态语言,比如Python,我们写一个python程序运行,是不需要进行编译的,会读取第一行源文件中代码就运行这一行的代码,然后读取第二行代码,运行第二行代码...

2.2 类加载器的分类和加载顺序

什么是类加载器呢?我有一个很生动很形象的例子:假如字节码文件是一个人,而jvm就是地府,你说人死了会怎么进入地府呢?自己肯定找不到地府的位置,于是要让黑白无常请你过去了,类加载器在这里就是黑白无常!

大概了解类加载器的用处之后,我们就随意看看类加载器的种类和运行原理;

顺便提一下,我们还记得最开始配置的jdk环境变量吧!我的JAVA_HOME=D:\java\jdk1.7;

话说大家知道jar包到底是什么吗?其实就是一种压缩文件的格式,跟zip,gz等压缩格式没有多大区别,可以用360压缩打开。。。

进入正题,类加载器分为四种,启动类加载器(Bootstrap ClassLoader):最顶级的类加载器,还是用C++写的;在我们编写java程序的时候,编译器会自动的帮我们导入一下常用的jar包,用的就是这个类加载器,比如我们最熟悉的lang包下的Object,String,Integer等都是我们可以直接用的,而不需要我们手动导入;具体的会导入哪些jar包呢,这就需要我们配置环境变量JAVA_HOME,编译器会去环境变量中找%JAVA_HOME%\jre\lib ,这下面所有jar包然后进行加载到内存中,注意不是加载在JVM中;而且出于安全考虑,启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader):父类加载器是启动类加载器,java语言实现,负责加载%JAVA_HOME%\jre\lib\ext 路径下的jar包,这个不会自动加载,只有在需要加载的时候才去加载。

应用类加载器(Application ClassLoader):父类加载器是扩展类加载器,java语言实现,也可以叫做系统类加载器(SystemClassLoader),这个类加载器主要是加载我们在写项目时编写的放在类路径下的类,比如maven项目中src/main/java/所有类

自定义类加载器:需要我们自己实现,当特殊情况下我们需要自定义类加载器,只需要实现ClassLoader接口,然后重写findClass()方法,我们就能够自己实现一个类加载器,而且自己实现类加载器之后可以去加载任何地方的类。假如我新建一个类放在F盘的随便一个角落里也可以指定类路径去加载,有兴趣的小伙伴可以去试试。

不考虑自定义类加载器,可以看到,启动、扩展、应用这三个加载器就像是爷爷,爸爸,儿子一样的关系,所以要加载一个类的话,选用哪个类加载器呢?肯定是有什么好吃的先让儿子吃呀,然而儿子又很有孝心,会把到手的好吃的给爸爸吃。爸爸又会给爷爷吃,爷爷会尝试着吃,假如一看这东西糖分太高于是就又给爸爸吃,爸爸也尝试着吃,发现这东西不好吃,于是最后还是给儿子吃....这就是类加载器的双亲委托机制,随便找了一幅图看看:

2.3.JVM内部结构

其实大多数人对JVM是很熟悉了,不就是那几个块吗?本地方法栈,java栈,java堆,方法区,pc计数器,我这里就先大概说一下这几个部分的用处;

方法区:类加载器其实就是将字节码文件给丢到这里,并解析出字节码文件中包含的一些信息,比如全类名,类变量,方法有关的信息,父类信息,是不是接口等等这类信息

由于方法区很重要,我就随意画个草图:

常量池(属于方法区):由于方法区比较厉害能把字节码文件中很多信息给解析出来,但其中可能有很多常量比如18,“helloworld”,以及一些符号引用,常量池就存这些东西;但是什么又是符号引用呢?我就大概说一下吧,假如两个类Animal和Dog,在Animal类中有个方法里面是这样的:Dog dog = new Dog();dog.run(); 这个时候问题来了,在加载Animal类的时候发现了要用到Dog类,肯定是要去加载Dog类的,那么有两种做法,第一种先暂停Animal类的加载去加载Dog类,加载完之后再加载Dog类,第二种,Animal类继续加载的同时顺便加载Dog类,只是Animal中只要是用到了Dog类、方法、字段的所有地方我随便用xxx来表示,等Dog类加载完之后我再把xxx指向方法区Dog类对应的地址就ok了;我们当然用第二种方法啦,并且在这里我们随便用的xxx就是符号引用,而加载完成后方法区中的Dog类地址就是直接引用

java堆:根据方法区中存的这么丰富的信息,这里就会创建每一个类的Class对象,话说这个Class对象用的最多的就是反射,那么这个Class对象到底是个什么呢?其实不用想的太难理解了,你就把它看作字节码文件在内存中的另外一种形式呗,就好像大米,在电饭煲里的表现形式就是米饭,在高压锅里的表现形式就是粥了.....;假如程序运行的话,还会在堆中创建对象并且存放在堆中,所有的同类型的类的实例对象共享一个Class对象,我也随意画了一个草图来看看如下所示,所以同一个类的不同实例对象的xx.getClass()都是一样的,而且根据获得的Class对象可以利用反射创建新的对象和获取其中的方法,可以说Class对象为我们程序员提供了一个操作堆中对象的一个安全通道

pc寄存器:对于多线程来说,你就可以把这个看作一个计数器,每个线程一个,里面写着1,2,3,4,5....记录着各个线程执行代码的行号,为什么要记这个行号呢?莫非是闲的蛋疼?当然不是!因为对于多线程来说,cpu首先执行一号线程,然后停止,去执行二号线程,又停止,又去执行一号线程...这个时候问题来了,cpu怎么知道上一次一号线程执行到哪里来了?于是啊,这个pc寄存器用处就来了,因为每个线程都有一个,而且记录着当前执行的行号,下次cpu来了根据这个行号就可以接着执行了啊!

java栈:对象已经创建完毕放在堆中,然后我们调用一个java方法,就会在java栈中开辟一小块空间(就是所谓的压栈),俗称栈帧,栈帧可以有多个,因为一个方法中可以调用其他方法嘛!总之一个方法就对应一个栈帧,栈帧里面放着我们这个要运行方法内的局部变量,方法返回值等等参数,等这个方法执行完之后这个栈帧就退出去了(这就是所谓的弹栈),然后栈就恢复原样

本地方法栈:不知道大家有没有打开JDK的一些类的源码看看,很多类都有Native方法(本地方法),我的理解是就是调用操作系统中一些c语言实现的方法或者其他语言实现的方法....

2.4.加载

说了这么久的类加载器的种类还有类加载器的使用顺序,然后也简单说了JVM内部结构以及各自的作用,现在就是选好了的类加载器去加载字节码文件丢到JVM中的方法区中了。

用伪代码随便看看加载大概步骤,参数name就是我们传进去的类的全名:

public Class<?> loadClass(String name) {
  try {
   if (parent != null) {
    //如果存在父类加载器,就委派给父类加载器加载
    c = parent.loadClass(name, false);
   } else {
    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类, 通过调用本地方法native findBootstrapClass0
    c = findBootstrapClass0(name);
   }
  } catch (ClassNotFoundException e) {
   // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
   c = findClass(name);
  }

所以假如我自定义一个类加载器MyClassLoader,那么就可以用这种方式去加载我随意放在F盘myclass目录里面,com.wyq.test包下的一个Student类:

MyClassLoader myClassLoader=new MyClassLoader("F:\\myclass");
Class c=myClassLoader.loadClass("com.wyq.test.Student");

然后我们得到了这个类的Class对象就可以用反射对这个类为所欲为了,嘿嘿嘿嘿~

2.5.链接

链接中分为三步:验证,准备,解析;

随便说说这三步大概干些什么,验证:这一步其实没什么大的用处,就是虚拟机会检查一下我们的字节码文件有没有问题,具体的就是看看你字节码文件格式有问题吗?语法有没有问题?等等

准备:给类的静态变量分配内存空间,并设置初始值;大家都知道静态变量是放在方法区中的吧,比如我java类中有个静态变量static int age = 18 那么这这个阶段首先会分配4个字节的内存空间,然后设置初始值为0,八大基本数据类型都有初始值,可以了解了解

解析:比较专业一点的说法就是,在解析阶段,JVM会把类的二进制数据中的符号引用替换为直接引用!这句话怎么理解请看上面介绍的常量池

2.6 初始化

还是用准备阶段那个静态变量,根据字节码文件,将准备那个阶段的初始值覆盖成真正的值18;

顺便说一句,加载、链接、初始化三个步骤不是一定要按照这个顺序完成的,只是开始的顺序是这个,但是在执行过程中可能会有弯道超车的现象

3.例子分析

这里我们写一个最简单的例子来总结一下上面这么多知识;

public class Animal{
 private int age=18;
 public void run() {} 

}

publci class Test{
 public static void main(String[] args){
  Animal animal = new Animal();
  animal.run();

 }
}

运行这个main方法的步骤:

1.首先是编译器会将这两个类都编译成字节码文件并放在你的项目存放路径

2.Test这个类会以某种方式告诉JVM自己的类名“Test”,虚拟机就会以某种牛逼的方法可以找到你这个Test.class放在那个目录下面

3.调用类加载器,采用双亲委托机制去加载这个类,最后不出意外应该是应用类加载器去加载这个Test.class,以二进制流的形式加载进JVM方法区

4.在加载之后会去验证这个Test.class是否符合规范,没问题的话就会解析这个加载进来的Test.class,将其中很多信息都保存下来,常量和符号引用保存在常量池中,其他的比如访问修饰符,全类名,直接父类的全类名,方法和字段信息,除了常量以外的所有静态变量,以及指向类加载器和Class对象的指针等都存在常量池外面

5.通过保存在方法区中的字节码,JVM可以执行main()方法,在执行这个方法的时候,会一直持有有一个指向Test的常量池的指针;

6.在执行main方法的第一条指令的时候,就是告诉JVM为Test常量池的第一个类型分配足够内存;由于main方法一直持有执行Test常量池指针于是很迅速的找到了常量池第一项,发现它是一个对Animal类的符号引用,然后就会先检查方法区看有没有Animal类有没有被加载,假如没有的话就要去找到这个Animal类;这里就有了一个算法的小知识,怎么才能够让虚拟机最快速度找到Animal类所在位置呢?可以用散列表,搜索树等算法。

7.加载Animal.class到方法区并提取其中有用的信息保存在方法区,然后替换Test常量池第一个类型的符号引用,变为直接引用;注意,这个时候还没有创建对象,直接引用指向的是方法区中Animal所在的地址

8.JVM在堆中为创建Animal对象分配足够内存,怎么确定这个内存多大合适呢?其实JVM比较牛,已经设好了可以根据方法区中存放的信息确定一个类创建对象要用到多少堆空间;

9.对象创建好了会设置Animal实例变量的默认初始值:age = 0

10.创建一个栈帧(里面有一个指向Animal对象的引用),压入java栈中,到此main方法第一条指令就执行完毕;还记得一个方法一个栈帧么

11.然后根据这个栈帧调用java代码,将age的值初始化为正确的值:18

12.通过这个栈帧执行run()方法,又会开辟一个栈帧存放run()方法内部的所有信息

13.run()方法执行完毕,释放这个栈帧;然后main()执行完毕,释放栈帧;然后就是程序执行完毕,清理回收堆中所有对象以及方法区

大概就是这么一个流程,其中最后的那个清理回收过程其实很重要,由于java栈和方法区的清理内存效率非常好,我们可以不用在意,重点是在堆中清理内存,而且由于有的程序是会运行很久的,不可能每次都等程序执行完毕之后再一起清理,肯定是要一边运行程序一边清理堆内存中没用的对象,那么又该怎么进行处理呢?又会涉及到很多的算法以及堆内部到底是什么结构,后面我们会逐渐挖掘...

以上所述是小编给大家介绍的java虚拟机详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 详解Java虚拟机30个常用知识点之1——类文件结构

    1. Java文件 ClassFileTest.java package com.zxs.ssh.template.service; public class ClassFileTest { int m = 1; public int inc(){ return m+1; } } 2. Class文件ClassFileTest.class javac  ClassFileTest.java  编译.java文件得到.class文件 JDK版本  1.8.0_201 .class文件可以用WinH

  • java命令调用虚拟机方法总结

    java命令调用虚拟机 java的虚拟机调用,按住Win+r命名,如图所示: 继续点击确定按钮,如图所示: 可以看到后台命令,如图所示: 调用虚拟机编译Test.java代码:如图所示: Test.java可以看到在E盘JavaTest文件夹下,,如图所示: 回到命令后台,输入:E: 按回车键,然后在输入:cd JavaTest,按回车键, 然后输入javac Test.java,按回车键,这个是调用虚拟机编程的java代码, 最后输入:java Test,按回车键,可以看到后台输出:Hello

  • java虚拟机学习笔记基础篇

    1.前言(基于JDK1.7) 最近想把一些java基础的东西整理一下,但是又不知道从哪里开始!想了好久,还是从最基本的jvm开始吧!这一节就简单过一遍基础知识,后面慢慢深入... 水平有限,我自己也是很难把jvm将清楚的,我参考一本书<深入java虚拟机第二版>(版本比较老,其实很多大佬的博客都是参考的这本书的内容...) 所谓jvm,又名java虚拟机.我们平常写java程序的时候几乎是感觉不到jvm的存在的,我们只需要根据java规范去编写类,然后就可以运行程序了,当然只有我们程序出现bu

  • 作为程序员必须掌握的Java虚拟机中的22个重难点(推荐0

    Java虚拟机一直是比较重要的知识点,是Java高级开发必会的.本文为你总结了关于JVM的22个重点.难点,图文并茂的向你展示和JVM有关的重点知识.全文共7000字左右. 概念 虚拟机:指以软件的方式模拟具有完整硬件系统功能.运行在一个完全隔离环境中的完整计算机系统 ,是物理机的软件实现.常用的虚拟机有VMWare,Visual Box,Java Virtual Machine(Java虚拟机,简称JVM). Java虚拟机阵营:Sun HotSpot VM.BEA JRockit VM.IB

  • Java虚拟机处理异常的最佳方式

    前言 欢迎来到Under The Hood专栏.本专栏旨在让Java开发人员一瞥在运行Java程序底层的神秘机制.本月的文章继续讨论Java虚拟机的字节码指令集,方法是检查Java虚拟机处理异常抛出和捕获的方式,包括相关的字节码.本文不讨论finally条款 - 这是下个月的主题.后续文章将讨论字节码系列的其他成员. 下面话不多说了,来一起看看详细的介绍吧 Exceptions Exceptions允许您顺利处理程序运行时发生的意外情况.要演示Java虚拟机处理异常的方式,请考虑一个名为NitP

  • java虚拟机学习笔记进阶篇

    上一节是把大概的流程给过了一遍,但是还有很多地方没有说到,后续的慢慢会涉及到,敬请期待! 这次我们说说垃圾收集器,又名gc,顾名思义,就是收集垃圾的容器,那什么是垃圾呢?在我们这里指的就是堆中那些没人要的对象. 1.垃圾收集器的由来 为什么要有垃圾收集器啊?不知道有没有想过这个问题,你说我运行一个程序要什么垃圾收集器啊? 随意看一下下面两行代码: User user = new User("root","123456") user = new User("

  • 带着新人看java虚拟机01(推荐)

    1.前言(基于JDK1.7) 最近想把一些java基础的东西整理一下,但是又不知道从哪里开始!想了好久,还是从最基本的jvm开始吧!这一节就简单过一遍基础知识,后面慢慢深入... 水平有限,我自己也是很难把jvm将清楚的,我参考一本书<深入java虚拟机第二版>(版本比较老,其实很多大佬的博客都是参考的这本书的内容...),电子档pdf文件链接:https://pan.baidu.com/s/1bxs4i0gnVpz7Lkjl2fxS9g 提取码:n5ou ,有兴趣的小伙伴可以自己下载自己好好

  • 老生常谈Java虚拟机垃圾回收机制(必看篇)

    在Java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的内存就是再堆内存中.如果在Java程序运行过程中,动态创建的对象或者数组没有及时得到回收,持续积累,最终堆内存就会被占满,导致OOM. JVM提供了一种垃圾回收机制,简称GC机制.通过GC机制,能够在运行过程中将堆中的垃圾对象不断回收,从而保证程序的正常运行. 垃圾对象的判定 我们都知道,所谓"垃圾"对象,就是指我们在程序的运行过程中不再有用的对象,即不再存活的对象.那么怎么来判断堆中的对象是"垃圾&q

  • 一篇文章带你深入理解JVM虚拟机读书笔记--锁优化

    目录 1. Java语言中的线程安全 1.1 不可变 1.2 绝对线程安全 1.3 相对线程安全 1.4 线程兼容 1.5 线程对立 2. 线程安全的实现方法 2.1 互斥同步 3. 锁优化 3.1 自旋锁与自适应自旋 3.2 锁消除 3.3 锁粗化 3.4 轻量级锁 3.5 偏向锁 总结 1. Java语言中的线程安全 按照线程安全的"安全程度"由强至弱来排序,可以将Java语言中各种操作共享的数据分为以下五类:不可变.绝对线程安全.相对线程安全.线程兼容和线程对立. 1.1 不可变

  • 解析Java虚拟机中类的初始化及加载器的父委托机制

    类的初始化 在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值. 在程序中,静态变量的初始化有两种途径: 1.在静态变量的声明处进行初始化: 2.在静态代码块中进行初始化. 没有经过显式初始化的静态变量将原有的值. 一个比较奇怪的例子: package com.mengdd.classloader; class Singleton { // private static Singleton mInstance = new Singleton();// 位置1 // 位置1输

  • java虚拟机运行时数据区分析

    JVMmemorymodel 这篇文章主要介绍在JVM规范中描述的运行时数据区(RuntimeDataAreas).这些区域设计用来存储被JVM自身或者在JVM上运行的程序所是用的数据. 我们先总览JVM,然后介绍下字节码,最后介绍不同的数据区域. 总览 JVM作为操作系统的抽象,保证同样的代码在不同的硬件或操作系统上的行为一致. 比如: 对于基本类型int,无论在16位/32位/64位操作系统上,都是一个32位有符号整数.范围从-2^31到2^31-1 无论操作系统或者硬件是大字节序还是小字节

  • 用IntelliJ IDEA看Java类图的方法(图文)

    看代码的遇见子类或者接口的实现时,如果有个类图工具就能让我们层次和关系一目了然,如果您的IDE是IntelliJ IDEA,推荐使用其自带的类图功能: 工具版本 社区版不带类图功能,所以请使用完整版,以下是我用的版本信息: 使用类图功能 以Spring源码的工程为例,假设我已经打开了ApplicationContext.java,在这个类的大括号内的区域点击右键,选择Diagrams -> Show Diagram,即可打开类图,如下图红框所示: 打开的效果如下图所示: ApplicationC

  • Java虚拟机内存溢出与内存泄漏

    一.基本概念 内存溢出:简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出. 内存泄漏:内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其他程序,于是就发生了内存泄漏. 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory: 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间

  • 详解Java 虚拟机垃圾收集机制

    1 垃圾收集发生的区域 之前我们介绍过 Java 内存运行时区域的各个部分,其中程序计数器.虚拟机栈.本地方法栈三个区域随线程共存亡.栈中的每一个栈帧分配多少内存基本上在类结构确定下来时就已知,因此这几个区域的内存分配和回收都具有确定性,不需要考虑如何回收的问题,当方法结束或线程结束,内存自然也跟着回收了 而 Java 堆和方法区这两个区域则有显著的不确定性,只有在程序运行时我们才能知道程序究竟创建了哪些对象,创建了多少对象,所以这部分内存的分配和回收是动态的,垃圾收集器所关注的正是这部分内存该

随机推荐