jvm的工作流程

    世界上并没有完美的程序,但我们并不因此而沮丧,因为写程序本来就是一个不断追求完美的过程。
    代码编译的结果从本地机器码转变为字节码,是存储格式的一小步,却是编程语言发展的一大步。
JDK
jdk的版本发布时间记忆,javaSE的各个版本规范集合: https://docs.oracle.com/javase/specs/index.html
JDK版本
名称
发布时间
1.0
Oak(橡树)
1996-01-23
1.1
 
1997-02-19
1.2
Playground(运动场)
1998-12-04
1.3
Kestrel(美洲红隼)
2000-05-08
1.4.0
Merlin(灰背隼)
2002-02-13
Java SE 5.0 / 1.5
Tiger(老虎)
2004-09-30
Java SE 6.0 / 1.6
Mustang(野马)
2006-04
Java SE 7.0 / 1.7
Dolphin(海豚)
2011-07-28
Java SE 8.0 / 1.8
Spider(蜘蛛)
2014-03-18

1、了解JDK和JRE的关系

Oracle has two products that implement Java Platform Standard Edition (Java SE)8: Java SE Development Kit (JDK) 8 and Java SE Runtime Environment (JRE) 8.
JDK 8 is a superset of JRE 8, and contains everything that is in JRE 8,
 plus tools such as the compilers and debuggers necessary for developing applets and applications. 
JRE 8 provides the libraries, the Java Virtual Machine (JVM), and other components to run applets and applications written in the Java programming
language.
 Note that the JRE includes components not required by the Java SE specification, including both standard and non-standard Java components.

        Oracle有两个实现Java Platform Standard Edition(Java SE)8的产品:Java SE Development Kit(JDK)8和Java SE Runtime Environment(JRE)8。JDK 8是JRE 8的超集,包含JRE 8中的所有内容,以及开发小程序和应用程序所需的编译器和调试器等工具。jre8提供了函数库、Java虚拟机(JVM)和其他组件来运行用Java编程语言编写的applet和应用程序。注意,JRE包含javase规范不需要的组件,包括标准和非标准Java组件。

The relation of JDK/JRE/JVM: https://docs.oracle.com/javase/8/docs/index.html

2、JVM虚拟机的工作流程

     JVM(Java Virtual Machine)是JRE(Java Runtime Environment)的一部分,和JMM一样只是一个规范,我们实现了这个规范,就可以更好的应用这个虚拟机,只是一个逻辑实现,并非真实的存在,例如Hotspot是JVM的一个经典的实现。
    一段代码能够运行,首先利用Java语言特性和JDK编写Java文件,JRE提供必要的运行环境,经过javaC编译成.class文件,*.class文件被装载到JVM虚拟机中,运行于操纵系统的kernel中;

2.1、源文件的编译

    Order.java源文件经过编译器javac的编译生成平台无关性的Order.class文件。具体流程如下:
    Order.java -> 词法分析器 -> tokens -> 语法分析器 -> 语法树 / 抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> Order.class 文件。
字节码
    java代码在进行javac编译的时候,并不像C/C++这样有 “链接“ 这一步,而是在虚拟机 加载Class文件的时候进行动态链接。也就是说在class文件中不会保存各个方法和字段的最终内存布局,因此这些字段和方法如果不经过转化的话是不能被虚拟机直接使用的。此时只有class类对象,而非实例对象,例如会在 链接过程中出现的类无法找到ClassNotFound的错误。
    Class文件结构:一组以8位字节的二进制流,超过8位,按高低分割存储, 字节码具体内容:
  • 0123字节:魔数:0xCAFEBABY
  • 45字节:此版本号
  • 67字节:主版本号
  • 89字节:常量池
  • 10-11字节:访问标志位:类还是接口等等
使用十六进制的编辑器打开编译后的字节码可以看出,源文件被编译成了一组16进制流,开头的四个字节cafebabe这个魔数代表这个文件是一个字节码文件,第5-5个字节0000代表次版本,第7-8字节代表0034主版本十进制的52。
编译: javac Person.java ---> Person.class
字节码,class文件 字节码( ByteCode )是构成平台 无关性的基石
The class File Format详细信息参考 :https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
将我们编写的程序编译成 二进制本地机器码 Native Code,已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令无关,平台中立的格式作为程序编译后的存储格式,实现了“ 一次编写,到处运行。 Write Once,Run Anywhere.

2.2、类加载过程

当源文件.java经过编译生成.class文件,要想字节码被执行,就必须被加载到虚拟机中生成相应的实例对象。大致步骤 如下:
  • (1) 通过一个类的全限定名获取定义此类的二进制字节流; 
  • (2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • (3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口;
下面展示了从源文件.java -> 源文件.class -> class对象 ->  实例对象的具体过程:
类加载具体过程: 参考文件:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
  • (1). 加载:loading  查找和导入一个class文件
    • 1.1通过一个类的全限定名获取定义此类的二进制字节流
    • 1.2将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    • 1.3在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
  • (2). 连接:linking
    • 验证 :verification-是否符合字节码的规范-cafebaby,字节码-元数据-文件是否准确,有没有危害jvm的代码
      • 文件格式验证
      • 元数据验证
      • 字节码验证
      • 符号引用验证
    • 准备 :preparation  -为类变量【静态变量】分配内存且赋初始值(赋默认:0值)static - 0值(final和没有显示初始化的此时确定值)
      • public static int a = 10; //此时 a = 10;
    • 解析 :Resolution- 将符号引用变为直接引用的过程,常量引用类-接口-方法
  • (3). 初始化:Initialization: (java代码具体实际的值-static的值)- 为static变量赋程序员的值,a=10;
  • (4). 使用:Using :构造方法当我们使用new关键字的时候才调用;
  • (5). 卸载:Unloading :垃圾回收

2.3、类加载器的分类

java开发人员来说,类加载器可以划分的更细一些,绝大部分的java程序都会用到以下四种系统提供的类加载器:
  • bootstrap启动类加载器 Bootstrap ClassLoader,加载虚拟机识别的的类库:<JAVA_HOME>/lib:
    • 核心类库,rt.jar,加载到虚拟机的内存中,名字不符合类库即使放在lib目录中也不会被加载。启动类加载器无法被java程序直接引用。
  • ext扩展类加载 Extension Classloader,它负责加载:
    • <JAVA_HOME>/lib/ext目录中的类库,开发者可以直接使用;
  • app应用程序类加载器:  Application ClassLoader,一般情况下,这个就是程序中默认的类加载器。
    • Djava.class.path所指定的类和jar包;
  • Custom用户自定义类加载器:通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的classLoader;
其关系如下:

2.4、双亲委派机制

加载方式:jvm中class文件的加载是动态按需加载,并不是一次性将所有的文件都加载到内存,否则有可能会撑爆内存;
双亲委派机制:在接受到类加载请求的时候先让父类去加载,一层层往上递交,而不是直接去加载,直至到达顶层加载器—BootstrapClassLoader。
双亲委派机制工作流程:
  • 1、如果一个类加载器收到了类加载的请求,先查询缓存里是否加载过这个类,如果没有则将请求委托给父类:
  • 2、父类加载器也是如此执行,因此所有的加载请求最终都应该传送到顶层的启动类加载器中;
  • 3、只有当父加载器反馈完成不了(搜索的范围内没有这个所需要的类)这个请求时,则将该请求退回给下一级加载器去完成;
  • 4、子加载器才会尝试去加载这个类;
  • 5、如果都没有找到,则抛出ClassNotFoundException;
类加载器的工作流程的伪代码实现:
//类加载器工作流程伪代码
public void ClassLoader{
    findCache();
    loadParent();
    loadSelf();
}

双亲委派机制的作用

  • 确保jvm中类的唯一性;
  • 保证类安全:解决包冲突:如果每个加载器自己去加载则会导致jdk最底层的行为无法保证,保证jvm只有一个Object对象;能编译但不会使用;
  • 优先级的层次分明:解耦,职责分明,为扩展提供可能;每个加载器做自己的事情;
  • 统一管理:rt.jar
破坏双亲委派原则:
  • 1、第一次打破是双亲委派模型未出来之前(JDK1.2发布之前),通过重写loadClass去加载自己的类;
  • 2、第二次是由于双亲委派模型自身的缺陷引起的,利用SPI机制:
  • 3、第三次热替换,直接替换掉类加载器;
总结:
“双亲委派”机制只是Java推荐的机制,并不是强制的机制,我们可以继承java.lang.ClassLoader类,实现自己的类加载器。
  • 如果想保持双亲委派模型,就应该重写 findClass(name)方法;父加载器失败后调用自己的findClass完成类的加载;
  • 如果想破坏双亲委派模型:
  • 可以重写 loadClass(name)方法。具体细节建议看下源码;
  • 线程上下文加载器:META-INF/services/java.sql.Driver—Serviceloader.load();

2.5、ClassLoader类源码分析

public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
*/
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{    //加载保证线程安全
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name); //找类文件,如果类已经加载就不加载了
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {      //父类加载器不为空,递归调用父类的加载器
                    c = parent.loadClass(name, false);
                } else {   //父加载器为空当前的extensionClassLoader,因为bootStrapClassLoader是c++ 写的获取不到
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name); //如果都找不到类,则调用findClass自己定义的classLoader加载器;

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

    //判断是否是同一个类:类加载器 + 类包名 + 类名

问题思考:有两个不同的包名,类名相同的两个类,虚拟机能加载吗?

判断两个类是否是同一个类的标准:
  • 同一个类加载器
  • 全限定名称
  • 类是同一个包名
  • 同一个class名称
问题思考:方法不存在的问题解决?
有时候,你会发现程序里有这个方法,但是调用的时候说找不到,可能是因为这个方法的包存在多个版本,jvm加载的时候加载的是这个不存方法的包,所以就找不到这个方法。

3、运行时数据区

    当类被加载初始化之后就来到第4个部分,运行时数据区。我们了解到在类的加载阶段,会将字节流所代表的静态存储结构转化为方法区的运行时数据结构,之后在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。就是在类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数 据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)
The Java Virtual Machine defines various run-time data areas that are used during execution of a program.
 Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits.
 Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

运行时数据区逻辑图实例:

3.1、运行时数据区 java内存区域划分

代码运行时的数据区域划分,也是相对jvm来划分的。
  • 方法区-元空间 线程共享,存储方法体的二进制代码。存储已被虚拟机加载的类元数据信息,常量,静态变量,动态生成的字节码等;
  • 静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
  • 堆区 :线程共享,new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据,唯一的目的就是存放对象实例。
  • 本地方法栈:线程私有,执行native方法时的栈空间
  • 程序计数器 线程私有,看作是执行字节码的行号指示器,为了保存线程被中断调度后能回到原先执行的位置,线程的上下文切换,每个线程都创建一个自己的pc寄存器(程序计数器),互不影响,如果是native方法,则值为空,唯一个没有outofmemory的区域;
  • java虚拟机栈区:线程私有,一个线程对应着一个虚拟机栈,每一个方法的调用就对应着一个栈帧,存储局部变量,返回值,操作数栈等,主要指局部变量表,数据可以共享,速度仅次于寄存器register,快于堆,由系统自动分配和回收。

3.2、方法的调用-栈帧

  • 每一个线程就是一个虚拟机栈:Each Java Virtual Machine thread has a private  Java Virtual Machine stack, created at the same time as the thread.
  • 每次的方法调用会封装为一个栈帧;A new frame is created each time a method is invoked.
    • 栈帧主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用。
  • java栈的设置:jdk1.5以后,每个栈的大小默认为1M,栈的深度大约在7300,正常3000-5000就够了,所以1M已经够用了。
    • 太大:影响线程的创建个数,没个线程都需要java栈空间,造成空间浪费;
    • 太小:会出现java栈溢出的风险;
  • 栈帧Frame:包含的内容
    • 动态链接;
    • 局部变量表;
    • 操作数栈;
    • 返回值地址;
一个线程是一个java虚拟机栈,一个方法调用是一个栈帧。实例如下:
class CalcTest{
    public static int calc(int op1, int op2){
        int result = oop1 + op2;
        return result;
    }
    public static void main(String[] args){
        calc(10,3);
    }
}

该代码对应的线程栈和栈帧如下图所示:

内存空间在逻辑上分为三部分:
  • 代码区;方法区
  • 静态数据区:全局变量,静态变量
  • 动态数据区,动态数据区又分为:栈区和堆区;
tips:内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是jvm内存区域的划分,数据结构中的堆栈是抽象的数据存储结构。

4、内存参数设置

各个内存区域大小参数的设置:
内存参数设置
问题分析
堆溢出
-Xmx10 最大值10M;
-Xms10 最小值10M;
-XX:+HeapDumpOnOutOfMemoryError
确定是内存泄漏还是溢出:
泄漏 (Memory leak):该回收的对象没有被回收
溢出(Memory overflow):内存不够,增大内存
虚拟机 和本地方法栈
-Xss128k 栈的大小128K
-Xoss:本地方法栈的大小
单个线程都是:StackOverFlowError
多个线程出现:OutOfMemoryError
每个线程的栈分配的越大越容易出现内存溢出
运行时 常量池 溢出
-XX: PermSize=10M
-XX: MaxPermSize=10M
(分配在方法区)属于永久代的一部分
方法区 溢出
用于存放class的类信息:类名+字段描述符 + 方法描述
-XX:PermSize=10M
-XX:MaxPermSize=10M
有反射/ CGlib /jdk动态代理的场景需要注意;
本机 直接内存溢出
-XX:MaxDirectMemmorySize
默认与java堆的最大值-Xmx一样大
-Xmx=20M
-XX:MaxDirectMemorySize=10m
NIO中:
Bytebuffer bb = ByteBuffer.allocateDircet(1024);
堆内使用4 Byte,堆外使用1024个字节,当bb回收后才会回收堆外的内存,还没触发GC,此时容易造成内存泄漏,溢出;
 

5、小结

    至此, 源文件.java -> 源文件.class -> class对象 ->  实例对象->方法的调用,一个java的源文件就在jvm中愉快的跑起来了。
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
 
资料参考
Oracle官网资料参考链接
javaSE的各个版本规范集合: https://docs.oracle.com/javase/specs/index.html
 
 
不惑: 如果不依赖书本和他人就能得到这个问题的答案,那才算是升华到了“不惑”。
Motto: Rome was not build in a day.
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值