Java底层知识:JVM

JAVA底层知识:JVM

一 谈谈你对Java的理解

  • 平台无关性,即一次编译,到处运行
  • GC,垃圾回收机制,不必像C++那样手动释放内存了
  • 语言特性:泛型、反射、Lambda表达式
  • 面向对象:封装、继承、多态
  • 类库、Java本身自带的一些集合和一些并发库,网络库、IO/NIO
  • 异常处理

二 Compile Once,Run Anywhere如何实现

  • 编译时

javac编译,生成字节码文件,JVM解析,转换成特定平台的执行指令

Java源码首先被变异成字节码文件,再有不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

如何查看java字节码: javap

2.1 为什么JVM不直接将源码解析成机器码去执行

  • 准备工作: 每次执行都需要各种检查
  • 兼容性:也可以将别的语言解析成字节码
  • 首先这样做有两点好处:可以节省大量的准备工作,提升效率,其次可以提升兼容性;

三 JVM如何加载.class文件

  • 运行时

首先这个问题我们需要弄明白Java虚拟机,虚拟机是跑在内存中的,虚拟机核心的有两点:

内存模型,②GC;

先来看一下JVM的架构图

  1. Class Loader : 负责加载符合格式的.class文件;
  2. Execution Engine : 对命令进行解析;
  3. Native Interface : 融合不同开发语言的原生库为Java所用;
  4. Runtime Data Area : Java内存空间模型;

四 谈谈反射

4.1 反射的概念

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

4.2 写一个反射的例子

public class Robot {
   
    //私有name getDeclaredField()
    private String name;
    //公用方法 getMethod()
    public void sayHi(String helloSentence){
   
        System.out.println(helloSentence+" "+name);
    }
    //私有方法 getDeclaredMethod()
    private String throwHello(String tag){
   
        return "hello " + tag;
    }
}
public class ReflectSample {
   
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
   
        Class<?> rc = Class.forName("com.interview.javabasic.reflect.Robot");
        Robot r =(Robot) rc.newInstance();
        System.out.println("Class name is: " + rc.getName());

        //private方法
        //getDeclaredMethod:获取当前类的所有声明的方法,包括public、protected和private修饰的方法。
        //需要注意的是,这些方法一定是在当前类中声明的,从父类中继承的不算,实现接口的方法由于有声明所以包括在内。
        Method getHello = rc.getDeclaredMethod("throwHello", String.class);
        getHello.setAccessible(true);//setAccessible(true)取消了Java的权限控制检查
        Object str = getHello.invoke(r,"zpf");
        System.out.println("getHello result is: " + str);

        //public方法
        //getMethod:获取当前类和父类的所有public的方法。
        // 这里的父类,指的是继承层次中的所有父类。比如说,A继承B,B继承C,那么B和C都属于A的父类。
        Method sayHi = rc.getMethod("sayHi", String.class);
        sayHi.invoke(r,"Welcome");

        //getDeclaredFiled 仅能获取类本身的属性成员(包括私有、共有、保护)
        //getField 仅能获取类(及其父类可以自己测试) public属性成员
        Field name = rc.getDeclaredField("name");
        name.setAccessible(true);
        name.set(r,"Alice");
        sayHi.invoke(r,"Welcome");

    }
}

4.3 类从编译到执行的工程

  1. 编译器将.ava源文件编译成Class文件;
  2. ClassLoader将字节码转换成JVM中的Class< T >对象;
  3. JVM利用Class< T >对象实例化为T对象;

五 谈谈ClassLoader类加载器

5.1 ClassLoader类加载器

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

5.2 ClassLoader的种类

BootStrapClassLoader : C++编写, 加载核心库Java.*;

ExtClassLoader : Java编写,加载扩展库javax.*;

AppClassLoader : Java编写,加载程序所在目录;

自定义ClassLoader : Java编写,定制化加载;

5.3 自定义ClassLoader的实现

  1. 重写findClass() : 这个函数是用来寻找Class文件的;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
         
        throw new ClassNotFoundException(name);
    }
    
  2. 重写defineClass() : 解析字节码返回对象;

    protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
    {
         
        return defineClass(null, b, off, len, null);
    }
    
  3. 自定义ClassLoader

    public class MyClassLoader extends ClassLoader{
         
        private String path;
        private String classLoadName;
    
        public MyClassLoader(String path, String classLoadName){
         
            this.path = path;
            this.classLoadName = classLoadName;
        }
    
        //用于寻找类文件
        @Override
        public Class findClass(String name){
         
            byte[] b = loadClassData(name);
            return defineClass(name,b,0,b.length);
        }
    
        //用于加载类文件
        private 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 (IOException e) {
         
                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        }
    
    }
    
  4. 测试自定义MyClassLoader

    public class ClassLoaderChecker {
         
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
         
            
            MyClassLoader m = new MyClassLoader("F:/","myClassLoader");
            Class<?> c = m.loadClass("Zpf");
            System.out.println(c.getClassLoader());
            c.newInstance();
        }
    }
    

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

    6.1 什么是双亲委派机制?

    当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

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

    1. 避免重复字节码的加载;
    2. 提供了JDK核心类加载的沙箱环境;

    6.3 双亲委派机制的作用

    1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
    2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

七 类的加载方式

7.1 类的加载方式

  1. 隐式加载 : new;
  2. 显式加载 : LoadClass —> forName进行加载;

7.2 loadClass和forName的区别

LoadClass源码

// loadClass 
// 第一步
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 {
   
                        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);

                    // 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;
        }
    }
---------------------------------------------------------------------------------
// 第三步
protected final void resolveClass(Class<?> c) {
   
        resolveClass0(c);
    }

由于resolve传的值默认为false,所以只会进行加载不会执行后续两步的装载过程;接下来看一下forName();

// forName
public static Class<?> forName(String className)
                throws ClassNotFoundException {
   
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

从上边的源码可以看出默认传入的 initialize 为 true.说明使用 Class.forName() 方法获得 Class 对象是已经执行完初始化的了(注意这里指的是类加载过程中的最后一步:初始化,而非是实例化对象操作的初始化),所以他们的区别

  • 使用 loadClass() 方法获得的 Class 对象只完成了类加载过程中的第一步:加载,后续的操作均未进行。
  • 使用 Class.forName() 方法获得 Class 对象是已经执行完初始化的了

7.3 两者的使用场景

  • 对于Class.forName

    加载 MySQL 的驱动:Class.forName("com.mysql.jdbc.Driver");

    在驱动的源码会发现在类 Driver 中有一个静态代码块,静态代码块会在类加载过程中的初始化阶段执行。

    static {
         
        try {
         
            //往DriverManager中注册自身驱动
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
         
            throw new RuntimeException("Can't register driver!");
        }
    }
    
  • 对于loadClass

    举个小例子,在 Spring IOC 中,在资源加载器获取要读入的字节的时候,即读取一些 Bean 的配置的时候,如果是以 classpath 的方式来加载,就需要使用 ClassLoader 的 loadClass() 方法来加载。之所以这样做,是和 Spring IOC 的 Lazy Loading 有关,即延迟加载。Spring IOC 为了加快初始化的速度,大量的使用了延迟加载技术,而使用 ClassLoader 的 loadClass() 方法不需要执行类加载过程中的链接和初始化的步骤,这样做能有效的加快加载速度,把类的初始化工作留到实际使用到这个类的时候才去执行

八 Java内存模型😛

8.1 你了解Java的内存模型吗

  1. 内存简介

    32位 : 2^32 的可寻址范围;
    64位 : 2^64 的可寻址范围

  2. 地址空间的划分

    内核空间 : 指操作系统程序和C运行时的空间,包括调度程序等;
    用户空间 : Java程序运行时实际使用的空间;

8.2 Java内存模型JDK1.8

8.3 程序计数器(Program Counter Register)

  1. 当前线程所执行的字节码的行号指示器;
  2. 改变计数器的值来选取下一条需要执行的字节码指令;
  3. 和线程是一对一的关系也就是线程私有的;
  4. 对Java方法技术,如果使用的Native方法则计数器值为Undefined;
  5. 没有内存泄露的问题;

8.4 Java虚拟机栈(Stack)

  1. Java方法执行的内存模型;
  2. 包含多个栈帧;

8.5 局部变量表和操作数栈

  1. 局部变量表:包含方法执行过程中的所有变量
  2. 操作数栈:入栈、出栈、赋值、交换、产生消费变量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOnGn3oQ-1582124488351)(https://i.loli.net/2019/01/27/5c4da5087e34c.png)]

8.6 递归为什么会引发java.lang.StackOverflowError异常

名词解释:StackOverflowError:栈溢出错误

  1. 栈溢出原因 : 函数调用栈太深了,注意代码中是否有了循环调用方法而无法退出的情况

    如果一个线程在计算时所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出StackOverflowError

  2. 递归过深,栈帧数超过虚拟栈深度

  3. 解决办法 : 限制递归深度或者使用循环;

  4. **原理:**StackOverflowError 是一个java中常出现的错误:在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作。在栈中会保存局部变量,操作数栈,方法出口等等。jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。

8.7 虚拟机栈过多会引发java.lang.OutOfMemoryError异常

  1. 概念:OutOfMemoryError:内存不足错误
  2. 内存溢出原因
    如果一个线程可以动态地扩展本机方法栈,并且尝试本地方法栈扩展(没有大于配置允许最大的栈大小),但是内存不足可以提供, 或者如果不能提供足够的内存来为新线程创建初始的堆(如new Object),那么Java虚拟机将抛出OutOfMemoryError。

8.8 本地方法栈(线程共享😛)

  1. 与虚拟机栈相似,主要作用于标注了Native的方法;

8.9 元空间与永久代

  1. 两者区别

    元空间使用的是本地内存,永久代使用的是JVM的内存;

  2. 使用元空间的好处

    1. 字符串常量池存在永久代中,容易出现性能问题和内存溢出;
    2. 类和方法的信息大小难以确认,给永久代的大小指定带来困难;
    3. 永久代会为GC带来复杂度;
    4. 方便与其他JVM继承;

8.10 堆(heap)

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。

  1. 对象实例的分配区域
  2. GC管理的主要区域

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

九 JVM内存模型—常考题解析

9.1 JVM三大性能调优参数 -Xms -Xmx -Xss

  • -Xss:规定了每个线程虚拟机栈(堆栈)的大小,默认JDK1.4中是256K,JDK1.5+中是1M
  • -Xms:堆的初始值
  • -Xmx:堆能达到的最大值(一般设置和-Xms相同大小的值,防止heap不够用发生内存抖动)

9.2 Java内存模型中堆和栈的区别—内存分配策略

  1. 内存分配策略

    按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.

    • 静态存储:编译时驱动每个数据目标在运行时的存储空间需求
    • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
    • 堆式存储:编译时或运行时模块入口都无法确定,动态分配
  2. Java内存模型中堆和栈的区别

    • 堆内存用来存放由new创建的对象实例和数组。(重点)
    • 在栈内存中保存的是堆内存空间的访问地址,或者说栈中的变量指向堆内存中的变量(Java中的指针)

**堆和栈的联系:**引用对象,数组时,栈里定义变量保存堆中目标的首地址

堆与栈区别:

  1. 管理方式:栈自动释放,堆需要GC
  2. 空间大小:栈比堆小**(堆存的是对象数据,栈存的是指针)**
  3. 碎片相关:栈产生的碎片远小于堆
  4. 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  5. 效率:栈的效率比堆高(栈的灵活度较低,堆的灵活度较高,堆操作的复杂度要高)

9.3 元空间、堆、线程独占部分的联系—内存角度

9.4 不同JDK版本之间的intern()方法的区别——JDK6 VS JDK6+

String s = new String("a");
s.intern();

JDK6:当调用intern方法时,如果字符串常量池先前已经创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。

JDK6+:当调用intern方法时,如果字符串常量池先前已经创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。

本质上就是后来改善版的 intern 方法把字符串存到了堆中,寻找的时候也会到堆中去寻找,原因是为了避免由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen;(元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。)

  • JDK6的例子

    解析:首先我们用引号声明的常量(“a”)都会在字符串常量池中创建出来,而new出来的string对象(new String(“a”))都会在Java heap中创建出来,intern的时候会试图把"a"字符串的副本放进常量池,但是由于常量池已有"a",所以放不进去,所有s1和s2引用不同的地址;"aa"则是首先在Java heap中生成,然后 intern 把"aa"的副本放进常量池,成功放进去;所以s4和s3不同。
    注:此处虽然都是false,但是常量池和Java heap中字符串的生成顺序不一样

  • JDK6+的例子

    解析:解析:首先我们用引号声明的常量(“a”)都会在字符串常量池中创建出来,而new出来的string对象(new String(“a”))都会在Java heap中创建出来,intern的时候会试图把"a"字符串引用放进常量池,但是由于常量池已有"a",所以放不进去,所以s1和s2引用不同的地址;"aa"则是首先在Java heap中生成,然后 intern 把"aa"的引用放进常量池,成功放进去;所以s4和s3相同。
    注:新版本的intern与旧版本不同的是:旧版本放进常量池的是字符串的副本,也就是字符串的内容;而新版本则是把字符串的引用放进常量池;所以当常量池不存在字符串的时候,新版本的 intern 之后的引用就和之前的一样

全部评论

相关推荐

点赞 评论 收藏
转发
点赞 收藏 评论
分享
牛客网
牛客企业服务