Java底层-JVM

Java的理解

  • 平台无关性
  • GC 垃圾回收机制
  • 语言特性:反射、范型、lambda表达式等
  • 面向对象:多态、继承、封装
  • 类库:集合、并发库、IO库等
  • 异常处理

平台无关性

Jvm可以从软件层屏蔽不同的操作系统在底层硬件与指令上的区别。

.java文件通过javac编译生成.class文件,.class文件中包含:编译生成的二进制码、java类中的属性、方法和静态属性。可以通过java自带的javap命令反编译查看.class文件,了解java编译器内部机制。

  • javap指令(javap -help查看), -c 对代码进行反汇编。
    在这里插入图片描述

JVM架构

Java虚拟机JVM,抽象化的、运行在内存中的虚拟化计算机,通过模拟仿真各种计算机功能。JVM屏蔽了与具体操作系统平台相关的信息,使得java编译后可以在各种平台运行。

在这里插入图片描述

Class Loader(类装载系统):依据特定格式,加载class文件到Runtime Data Area。

Execution Engine(字节码执行引擎):对命令进行解析。

Native Interface(本地方法接口):融合不同开发语言的原生库为java所用。

Runtime Data Area(运行时数据区):JVM内存空间结构模型。

JVM如何加载class文件?

Java反射机制

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有方法和属性;对于任意一个对象都能够调用任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

定义反射类Robot,包括私有属性name、公共方法sayHi、私有方法throwHello。

package com.Interview.javabasic;

public class Robot {
    private   String name;
    public void sayHi(String helloSentence){
        System.out.println(helloSentence + "" +name);
    }
    private String throwHello(String tag){
        return "hello" + tag;
    }
}

定义反射实例类,通过forName获取反射类、newInstance实例化反射类、getDeclaredMethod和getMethod获取反射类的方法、getDeclaredField获取反射类的属性、invoke调用反射类的方法并且可以传入参数。

package com.Interview.javabasic;

import javax.swing.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectSample {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //forName根据类的路径获取类对象
        Class rc = Class.forName("com.Interview.javabasic.Robot");
        //通过newInstance获取类的实例,因为newInstance返回的实例类型是范性,需要强转。
        Robot  r = (Robot) rc.newInstance();
        System.out.println("class name "+rc.getName());
        //getDeclaredMethod能够获取反射类的所有包括公共和私有方法,但是不能获取继承的方法和实现接口的方法。
        Method getHello = rc.getDeclaredMethod("throwHello",String.class);
        //getMethod只能获取公共public方法,但是可以获取反射类继承的方法和实现接口的方法。
        Method sayHi = rc.getMethod("sayHi", String.class);
        Field name = rc.getDeclaredField("name");

        //反射类的throwHello为私有方法、name为私有属性,因此需要设置setAccessible为true
        getHello.setAccessible(true);
        name.setAccessible(true);
        //给name赋值,需要传入实例化的反射类对象
        name.set(r,"alice");

        //使用Object接受返回的值,invoke()返回的是Object对象,
        Object str = getHello.invoke(r,"Bob");
        sayHi.invoke(r,"welcome");

        System.out.println("getHello result" + str);
    }

}

类从编译到执行的过程

反射获取类的class对象,必须先获取该类的字节码文件对象。

  • 编译器将Robot.java源文件编译为Robot.class字节码文件。
  • ClassLoader将字节码(byte数组格式)转换为JVM中的Class对象。
  • JVM利用Class对象实例化为Robot对象。

ClassLoader简介

ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。他是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载近系统,然后交给Java虚拟机进行连接、初始化等操作。

ClassLoader的种类

  • BootStrapClassLoader:C++编写,加载核心库java.*,由JVM实现。
  • ExtClassLoader:Java编写,加载扩展库javax.*,可以将自定义的class进行加载。
  • AppClassLoader:Java编写,加载程序所在目录。用于加载ClassPath路径下的内容 ,例如上述的Robot.class和ReflectSample.class文件。
  • 自定义ClassLoader:Java编写,定制化加载。

findClass()根据名称和路径加载.class字节码,然后它会调用defineClass()解析定义clss字节流并返回class对象。

自定义ClassLoader实例

在这里插入图片描述

目的:自定义一个类加载器,实现加载自定义claa文件的功能。

流程:

  • 1.在工程以外创建一个Wail类(Wail.java),使用javac编译为Wail.class文件。

    public class Wail{
    	static{
    		System.out.println("Hello wail");
    	}
    }
    
  • 2.编写一个自定义的ClassLoader类加载器,通过类的路径和名称,读取class文件的字节流解析返回class对象。

    package com.Interview.javabasic;
    
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.InputStream;
    
    public class MyClassLoader extends ClassLoader {
        private String path;
        private String classLoaderName;
    
        public MyClassLoader(String path, String classLoaderName){
            this.path = path;
            this.classLoaderName = classLoaderName;
        }
    
        public Class findClass(String name){
            byte [] b = loadClassData(name);
            return defineClass(name,b,0, b.length);
        }
    
        public byte[] loadClassData(String name) {
            name = path + name + ".class";
            InputStream in = null;
            ByteArrayOutputStream out = null;
            try {
                in = new FileInputStream(new File(name));
                out = new ByteArrayOutputStream();
                int i = 0;
                while ((i = in.read()) != -1) {
                    out.write(i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try{
                    out.close();;
                    in.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        }
    }
    
  • 3.编写ClassLoaderChecker.java文件进行验证。

    package com.Interview.javabasic;
    
    public class ClassLoaderChecker {
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            MyClassLoader m = new MyClassLoader("class文件所在目录","MyClassLoader");
            Class c = m.loadClass("Wail");
            System.out.println(c.getClassLoader());
            //newInstance()触发自定义的加载类里的static代码块
            c.newInstance();
        }
    }
    

类加载机制的双亲委派机制

  • 1.自底向上检查类是否已经加载,如果最终在Bootstrap ClassLoader中没有检查到该类,则会尝试加载类。
  • 2.自顶向下尝试加载类,首先在-Xbooclasspath路径下的Bootstrap ClassLoader管理的Jar包中进行加载,最终在自定义的Custom ClassLoader按照findClass的方式寻找对应的class文件的类,如果有直接加载到JVM中。
  • 3.如果在顶向下还没有加载到类,就会抛出异常。

在这里插入图片描述

为什么会使用双亲委派机制去加载类?

避免同样字节码的加载从而浪费内存,例如:对于system.out字节码,如果没有双亲委派机制,类A打印会加载一份,类B打印也会加载一份,那JVM中会出现两份同样的字节码,浪费内存。如果有在类A打印时,system.out字节码已经从bootstrap ClassLoader中加载到内存,类B打印时,会自底向上进行检查,检查到bootstrap ClassLoader中system.out字节码已经被加载到内存中,直接引用就行。

类的加载方式

  • 1.隐式加载:new

    隐式加载使用new,将类加载到JVM中,支持带参数的构造器生成对应的实例。

  • 2.显式加载:loadClass、forName等

    通过显式加载到类的class对象后,需要调用class对象的newInstance()方法来获取类的实例,不支持传参。

loadClass和forName的区别

  • 1.在运行时,都能够调用类的任意属性和方法。

  • 2.Class.forName得到的class是已经初始化完成的。

    Class r = Class.forName("com.Interview.javabasic.Robot"); //使用forName()方法查看静态代码块是否被执行
    
  • 3.ClassLoader.loadClass得到的class是还没有链接的。

    ClassLoader cl = Robot.class.getClassLoader(); //使用loadClass()方法查看静态代码块是否被执行
    
  • 4.例如,准备好Robot.java文件,并编译为Robot.class文件。如果Robot类被初始化,则静态代码块会被执行。

    package com.Interview.javabasic;
    
    public class Robot {
        private   String name;
        public void sayHi(String helloSentence){
            System.out.println(helloSentence + "" +name);
        }
        private String throwHello(String tag){
            return "hello" + tag;
        }
      static {
            System.out.println("hello robot");
        }
    }
    

类的两种显式加载的应用场景

  • 1.loadClass()应用场景

    SpringIOC中 ,在资源加载器获取要读入bean的配置文件时,如果以classpath的加载方式就需要使用loadClass()进行加载。因为这与SpringIOC的(lazz loader)懒加载机制有关,SpingIOC为了加快初始化速度,大量使用了懒加载技术,使用loadClass()不需要执行装载类的链接和初始化,并将类的初始化工作留在使用的时候在进行。

  • 2.forName应用场景

    目的:程序连接Mysql时,需要加载对应的数据库驱动Driven,此时需要用forName进行Driven类的加载。

    流程:1.导入jar包,file->Structure->Modules->Dependencies->"+"进行导入。

    ​ 2.使用forName加载Driver类。

    package org.gjt.mm.mysql;
    
    import java.sql.SQLException;
    
    public class Driver extends com.mysql.jdbc.Driver {
        public Driver() throws SQLException {
        }
    }
    

类的装载过程

类的加载过程是类的装载过程的一部分。

  • 1.加载,通过ClassLoader加载class文件字节码,生成Class对象。

    ClassLoader通过loadClass方法将class文件字节码加载到内存中,并将这些静态数据转化为Runtime Data Area(运行时数据区)中Method Area(方法区)的类型数据。在Runtime Data Area(运行时数据区)的Heap(堆)中生成一个代表这个类的java.lang.class对象,作为Method Area(方法区)类数据的访问入口。

  • 2.链接

    • 校验:检查加载的class的正确性和安全性。
    • 准备:为类变量分配存储空间并设置类变量初始值。
    • 解析:JVM将常量池内的符号引用转换为直接引用。
  • 3.初始化,执行类变量赋值和静态代码块。

Java内存模型

线程私有和线程共享

在这里插入图片描述

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:MetaSpace(元空间)、Java堆

PC Register(程序计数器)

  • 当前线程所执行的字节码行号指示器(逻辑)。
  • 改变计数器的值来选取下一条需要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等功能都需要依赖PC计数器)。
  • 线程私有,每个线程独有程序计数器,程序计数器在线程挂起时记录当前位置,线程调用CPU时从当前记录位置运行。
  • 对Java方法计数,记录的是虚拟机执行的字节码指令的地址。如果是Native方法则计数器值为Undefined。
  • 不会发生内存泄漏。

Stack(Java虚拟机栈)

在这里插入图片描述

  • Java方法执行的内存模型
  • JVM中的栈(虚拟机栈)由一个个栈帧组成,每个方法执行时都会产生一个栈帧
  • 栈帧中包括局部变量表、操作数栈、动态链接、方法出口(返回地址)等。
  • 首先压入到虚拟机栈中的一般时main()栈帧,依次是main()中调用的函数(compute())栈帧。

栈帧结构

  • 1.局部变量表,包含方法执行过程中的所有变量(基本数据类型的变量)。存放的是方法体中定义的局部变量,如果局部变量为对象,则局部变量中存放的是堆中对象存储的地址。

    函数方法中的局部变量会存储在栈内存空间中,当main()中的线程启动时会在虚拟机栈中获得一小块内存空间存放局部变量,main()中调用其他函数会再开启一个线程,同时获得栈中内存空间存放局部变量。

  • 2.操作数栈,在执行字节码指令时被用到,包括入栈、出栈、复制、交换、产生消费变量。

  • 3.动态链接

  • 4.方法出口,被调方法存放方法执行完成后,回到调用方法代码执行的位置

实例

编写Math.java文件

public class Math{
    public static final int d = 100;
    public static User user = new User();
        public int compute(){
        int a = 1;
        int b = 2;
        int c = (a + b)*10;
        return c;
    }
    public static void main(String[] args){
        Math math = new Math();
        math.compute();
        System.out.println("test");
    }
}
class User{

}

通过 javap -verbose Math.class口水话语言反编译Math.class

  Last modified 2020-10-7; size 675 bytes
  MD5 checksum f1cfc18c54f979f9383988d20a27e696
  Compiled from "Math.java"
public class Math			//描述类信息,公有、继承Object
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:			//常量池信息
   #1 = Methodref          #11.#29        // java/lang/Object."<init>":()V
   #2 = Class              #30            // Math
   #3 = Methodref          #2.#29         // Math."<init>":()V
   #4 = Methodref          #2.#31         // Math.compute:()I
   #5 = Fieldref           #32.#33        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #34            // test
   #7 = Methodref          #35.#36        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #37            // User
   #9 = Methodref          #8.#29         // User."<init>":()V
  #10 = Fieldref           #2.#38         // Math.user:LUser;
  #11 = Class              #39            // java/lang/Object
  #12 = Utf8               d
  #13 = Utf8               I
  #14 = Utf8               ConstantValue
  #15 = Integer            100
  #16 = Utf8               user
  #17 = Utf8               LUser;
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               compute
  #23 = Utf8               ()I
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               <clinit>
  #27 = Utf8               SourceFile
  #28 = Utf8               Math.java
  #29 = NameAndType        #18:#19        // "<init>":()V
  #30 = Utf8               Math
  #31 = NameAndType        #22:#23        // compute:()I
  #32 = Class              #40            // java/lang/System
  #33 = NameAndType        #41:#42        // out:Ljava/io/PrintStream;
  #34 = Utf8               test
  #35 = Class              #43            // java/io/PrintStream
  #36 = NameAndType        #44:#45        // println:(Ljava/lang/String;)V
  #37 = Utf8               User
  #38 = NameAndType        #16:#17        // user:LUser;
  #39 = Utf8               java/lang/Object
  #40 = Utf8               java/lang/System
  #41 = Utf8               out
  #42 = Utf8               Ljava/io/PrintStream;
  #43 = Utf8               java/io/PrintStream
  #44 = Utf8               println
  #45 = Utf8               (Ljava/lang/String;)V
{
  public static final int d;		
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 100

  public static User user;			//类的初始化信息
    descriptor: LUser;
    flags: ACC_PUBLIC, ACC_STATIC

  public Math();					//类的初始化信息
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int compute();	
    descriptor: ()I				//表示不接收变量 ,返回值为Int类型
    flags: ACC_PUBLIC			//public类型的方法
    Code:									//代码前面的编号方便程序计数器调用
      stack=2, locals=4, args_size=1	//操作数栈的深度为2、本地变量容量为4、参数数量为1
         0: iconst_1		//将int类型常量1压入操作数栈 
         1: istore_1		//将int类型值存入局部变量1 将常量1赋值给局部变量表中的第一个变量a
         2: iconst_2		//将int类型常量2压入操作数栈
         3: istore_2		//将int类型值存入局部变量2 将常量2赋值给局部变量表中的第二个变量b
         4: iload_1		//从局部变量1中装载int类型值 将常量1装载到操作数栈
         5: iload_2		//从局部变量2中装载int类型值 将常量2装载到操作数栈
         6: iadd			//执行int类型的加法 常量1与常量2相加 
         7: bipush        10 //将一个8位带符号整数压入栈 将10压入操作数栈
         9: imul			// 执行int类型的乘法 常量3
        10: istore_3		//将int类型常量3压入操作数栈
        11: iload_3		//从局部变量3中装载int类型值
        12: ireturn		//从方法中返回int类型的数据
      LineNumberTable:	//行号表
        line 5: 0			//代码第5行对应字节码的第0行
        line 6: 2
        line 7: 4
        line 8: 11

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: ldc           #6                  // String test
        18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        21: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 13
        line 14: 21

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #8                  // class User
         3: dup
         4: invokespecial #9                  // Method User."<init>":()V
         7: putstatic     #10                 // Field user:LUser;
        10: return
      LineNumberTable:
        line 3: 0
}
SourceFile: "Math.java"

栈的内存会自动释放,不需要GC。

为什么递归会引发java.lang.StackOberflowError异常?

当线程执行一个方法时,就会创建一个栈帧并压入虚拟机栈,方法执行完毕后,将栈帧出栈。正在运行的方法,位于虚拟机栈的栈顶,递归函数会不断的调用自身,并往虚拟机栈中压入。当递归过深,栈帧数超出虚拟机栈深度时,就会抛出异常。

本地方法栈

  • 与虚拟机栈相似,主要作用于标注了native的方法,是调用本地方法时所需的内存空间。

  • 本地方法栈中的方法底层用C语言来实现,当java运行到native方法时,会到操作系统方法库中调用xx.dll(类似于jar包)方法,来实现跨语言的方法调用,现在一般不常用。常用的是基于服务来实现跨语言方法调用,直接调用服务的接口。

    private native void start0();	//用native来修饰的方法
    

MetaSpace元空间

jdk1.8版本之前称之为方法区(又叫永久方法区),jdk1.8之后称之为元空间。元空间使用本地内存,而永久代使用的是JVM的内存;字符串常量池存在永久代中,容易出现性能问题和内存溢出,元空间中将常量池移到堆中。

  • 方法区
  • 常量
  • 静态变量:静态变量如果为对象,那静态变量中存储的是对象在堆中的地址
  • 类信息

Heap堆

堆,存放对象实例的内存区域,内存区域可划分为:old区、eden区、from-to survivor区。同样也是GC主要操作的区域。

  • eden区:新创建的对象都会放在eden区,当eden区的存储空间不足时,会触发minor GC(范围为eden、from或to)。通过垃圾回收,释放掉垃圾对象,未被回收的对象全部存放至survivor区的from或to中并且分代年龄+1.
  • from-to区:from和to区统称为survivor区,经过minor GC后,from区和eden区未被回收的对象全部移入to区或者to区和eden区未被回收的对象移入from区。当survivor区中由对象的分代年龄超过15次,会移入到old区。
  • old区 :随着程序的不断运行,old区空间会越来越少。当空间不足以存储survivor区移入的对象,会进行一次full GC(范围为整个堆),如果仍然空间不足,就会抛出异常。

Java的内存模型

JVM三大性能调优参数

  • -Xms,堆能达到的最大值(一般为128M)。通常将Xms和Xmx大小设置为一样,因为当堆扩容时,会产生内存抖动,影响程序的稳定性。
  • -Xmx,堆的初始值(128M),该进程刚创建时专属Java堆的大小,会自动扩容,扩容至堆的最大值。
  • -Xss,规定了每个线程虚拟机栈的大小(一般为256K),配置会影响此进程中并发线程数的大小。

Java内存模型中堆和栈的区别

  • 1.内存分配策略

    • 静态存储,编译时确定每个数据目标在运行时的存储空间要求。
    • 栈式存储,数据区要求在编译时未知,运行时模块入口前确定。
    • 堆式存储,编译时或运行时模块入口都无法确定,动态分配(如:可变字符串、实例化对象等)。
  • 2.联系

    • 引用对象、数组时,栈里定义变量保存对象、数组在堆中目标的首地址,可以运用栈中的引用变量访问堆中的对象和数组。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外会被释放掉。数组和对象本身会在堆中进行分配,分配的内存区域只有在没有引用变量指向的时候,才会被垃圾回收机制释放。

      在这里插入图片描述

  • 3.区别

    • 管理方式,栈自动释放(编译器就可以通过pull操作释放),堆需要GC(Execution Engine执行引擎不会对内存进行释放操作)。
    • 空间大小,栈比堆(需要存放大量java对象数据)小。
    • 碎片相关,栈产生的碎片远小于堆(内存结构较复杂)。
    • 分配方式,栈支持静态和动态分配,而堆仅支持动态分配。
    • 效率,栈的效率比堆高。

元空间、堆、线程独占部分间的联系

public class HelloWorld{
  private String name;
  public void sayHello(){
    System.out.println("Hello"+name);
  }
  public void setName(String name){
    this.name = name;
  }
  public static void main(String[] args){
    int a = 1;
    HelloWorld hw = new HelloWorld();
    hw.setName("test");
    hw.sayHello();
  }
}

当类被装载到内存空间后:

  • 元空间: 存储的是类的信息。Class:HelloWorld-Method:sayHello\setName\main-Fiels:name Class:System
  • Java堆:存储的是类对象的实例。Object:String(“test”) Object:HelloWorld
  • 线程独占:程序执行时,main线程会分配对应的虚拟机栈、本地方法栈和程序计数器。栈里面会存储:
    • Parameter reference:“test” to String object
    • Variable reference:“hw” to HelloWorld object
    • Local Variables:a with 1,lineNo

不同版本之间的intern()方法的区别-JDK6和JDK6+?


版权声明:本文为weixin_43972621原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。