JVM底层知识

  • Post author:
  • Post category:其他


6-1 谈谈你对Java的理解

Java思维导图
Java思维导图

谈谈你对Java的理解:

  • 平台无关性 — 一次编译到处运行

  • GC — 垃圾回收机制

  • 语言特性 — 泛型、反射、lambda表达式

  • 面向对象 — 封装、继承、多态

  • 类库 — 集合、并发库、网络、IO、NIO

  • 异常处理

6-2 平台无关性如何实现

  1. Compile Once,Run AnyWhere如何实现

    • 编译时 使用javac指令编译java源码,即将源码编译生成字节码并存入到对应的.class文件中

    • 运行时

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

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

    • 准备工作:每次执行都需要进行各种检查,如语法校验

    • 兼容性:也可以将别的语言解析成字节码

  3. 如何查看.class字节码:使用javap -c指令

6-3 JVM如何加载class文件

Java虚拟机:JVM内存结构模型、GC垃圾回收机制

  • Class Loader:依据指定格式,加载class文件到内存

  • Execution Engine:对命令进行解析

  • Native Interface:融合不同开发语言的原生库为Java所用

  • Runtime Data Area:JVM内存空间结构模型

Class.forName()

JVM架构主要由Class Loader、Runtime Data Area、Execution Engine、Native Interface四部分组成

答:JVM通过Class Loader将符合其格式要求的class文件加载到内存里,并通过Execution Engine去解析class文件里边的字节码,并提交给操作系统去执行。

6-4 什么是反射

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

写一个反射的例子:

public class Robot {
    private String name;
    public void sayHi(String helloSentence) {
        System.out.println(helloSentence + " " + name);
    }
    private String throwHello(String tag) {
        return "Hello " + tag;
    }
}
public class ReflectSample {
    public static void main(String[] args) throws Exception {
        Class<?> rc = Class.forName("com.tuyrk.reflect.Robot");
        Robot robot = (Robot) rc.newInstance();
        System.out.println("Class name is " + rc.getName());
​
        //private私有方法
        Method getHello = rc.getDeclaredMethod("throwHello", String.class);//可以获取所有(包括私有方法)自定义的方法,不能获取继承或者实现接口的方法
        getHello.setAccessible(true);//throwHello方法为私有方法,默认为false
        Object str = getHello.invoke(robot, "Bob");
        System.out.println("getHello result is " + str);
​
        //public公有方法
        Method sayHi = rc.getMethod("sayHi", String.class);//只能获取public方法,但是可以获取继承或接口的方法
        sayHi.invoke(robot, "Welcome");
​
        //私有属性
        Field name = rc.getDeclaredField("name");
        name.setAccessible(true);
        name.set(robot, "Alice");
        sayHi.invoke(robot, "Welcome");
    }
}

6-5 谈谈ClassLoader

  1. 类从编译到执行的过程:

    • 编译器将Robot.java源文件编译为Robot.class字节码文件

    • ClassLoader将字节码转换为JVM中的Class<Robot>对象

    • JVM利用Class<Robot>对象实例化为Robot对象

  2. 谈谈什么是ClassLoader

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

  3. ClassLoader的种类:

    • BootStrapClassLoader:C++编写,加载核心库java.*

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

    • AppClassLoader:Java编写,下载程序所在目录

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

  4. 自定义ClassLoader的实现

    关键函数:

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError {
        return defineClass(null, b, off, len, null);
    }
    public class MyClassLoader extends ClassLoader {
        private String path;
        private String classLoaderName;
    ​
        public MyClassLoader(String path, String classLoaderName) {
            this.path = path;
            this.classLoaderName = classLoaderName;
        }
    ​
        //用于寻找类文件
        @Override
        protected 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 b = 0;
                while ((b = in.read()) != -1) {
                    out.write(b);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    out.close();
                    in.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        }
    }
    public class ClassLoaderChecker {
        public static void main(String[] args) throws Exception {
            MyClassLoader m = new MyClassLoader("E:\\java\\imooc_coding\\303-java-offter\\src\\main\\resources\\", "Wali");
            Class c = m.loadClass("Wali");
            System.out.println(c.getClassLoader());
            c.newInstance();
        }
    }

6-6 ClassLoader的双亲委派机制

谈谈类加载器的双亲委派机制:

Xbootclasspath是java -jar命令的一个参数


native方法实现

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

  • 避免多份同样字节码的加载,内存是相当宝贵的,没有必要保存两份一样的类对象,即Class对象。

6-7 loadClass和forName的区别

类的加载方式:

  • 隐式加载:new

  • 显式加载:loadClass,formName等

显式加载当获取到Class对象之后需要调用Class对象的newInstance方法来生成对象的实例,而通过隐式加载则无需调用Class对象的newInstance方法即可获取对象的实例,并且new支持带参数的构造器生成对象实例,而Class对象的newInstance方法则不支持传入参数,需要通过反射调用构造器的newInstance方法才能支持参数。

类的装载过程:

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

  • 链接:检验:检查加载的class的正确性和安全性,如检查class文件的格式是否正确 准备:为类变量分配存储空间并设置类变量初始值,类变量随类型信息存放在方法区中,生命周期很长。 解析:JVM将常量池内的符号引用转换为支持引用

  • 初始化:执行类变量赋值和静态代码块

loadClass和forName的联系: 他们都能够在运行时对任意一个类进行动态加载,都能够知道该类的所有属性和方法,对于任意一个对象都能够调用它的任意方法和属性。

loadClass和forName的区别: Class.forName得到的class是已经初始化完成的,即已经完成装载的整个过程 ClassLoader.loadClass得到的class是还没有链接的,即只完成装载的第一步

public class Robot {
    static {
        System.out.println("Hello Robot");
    }
}
public class LoadDifference {
    public static void main(String[] args) throws ClassNotFoundException {
//        ClassLoader cl = Robot.class.getClassLoader();
        Class c = Class.forName("com.tuyrk.reflect.Robot");
    }
}

区别产生的作用: 例如程序中需要使用MySQL需要加载driver,这里只能使用forName,而不能使用loadClass

有了forName装载类,为什么还要使用loadClass呢? 在Spring IOC中,在资源加载器获取要读入的资源的时候,即读取Bean的配置文件的时候,如果是以ClassPath的方式来加载就需要使用ClassLoader.loadClass来加载。这是因为SpringIOC使用了lazy loading延迟加载,Spring ICO为了加快初始化速度因此大量使用了延迟加载技术,而使用ClassLoader不需要执行类中的初始化代码和链接步骤,从而加快了加载速度,把类的初始化工作留到实际使用到这个类的时候再做。

6-8 Java内存模型之线程独占部分

内存简介:

32位处理器:2^32的可寻址范围 64位处理器:2^64位可寻址范围

地址空间划分: 内核空间:内核是主要操作系统程序和运行时空间,包含用于连接计算机调度程序以及提供联网,虚拟内存等服务的逻辑和进程 用户空间:Java进程实际运行时的内存空间

你了解Java的内存模型么

JVM内存模型-JDK8

线程私有:程序计数器、虚拟机栈、本地方法栈 线程共享:MetaSpace、Java堆

程序计数器(Program Counter Register)

  • 当前线程所执行的字节码行号指示器(逻辑)

  • 改变计数器的值来选取下一条需要执行的字节码指令,包括分支、循环、跳转、异常处理、线程恢复等基础功能

  • 和线程是一对一的关系,即“线程私有”

  • 对Java方法技术,如果是Native方法则计数器值为Undefined

  • 不会发生内存泄漏

程序计数器是逻辑计数器,而非物理计数器;为了线程切换后都能恢复正确的执行位置,每个线程都有一个独立的程序计数器,是线程独立的,只对Java方法计数,对Native方法则为Undefined,不会发生内存泄漏。

Java虚拟机栈(Stack)

  • Java方法执行的内存模型

  • 包含多个栈帧,方法运行期间的基础数据结构,用于存储局部变量表、操作栈、动态链接、返回地址等。

局部变量表和操作数栈

  • 局部变量表:包含方法执行过程中的所有变量

  • 操作数栈:入栈、出栈、复制、交换、产生消费变量

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

  • 递归过深,栈帧数超过虚拟机栈深度。限制递归次数,使用循环替代。

public class Fibonacci {
    //F(0) = 0, F(1) = 1, 当n >= 2的时候, F(n) = F(n - 1) + F(n - 2),
    //F(2) = F(1) + F(0) = 1, F(3) = F(2) + F(1) = 1 + 1 = 2
    //F(0) - F(N)依次为 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
    public static int fibonacci(int n) {
        if (n == 0) {return 0;}
        if (n == 1) {return 1;}
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
    public static void main(String[] args) {
        System.out.println(fibonacci(100000));
    }
}

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

public static void stackLeakByThread() {
    while (true) {
        new Thread() {
            public void run() {
                while (true) {
                }
            }
        }.start();
    }
}

虚拟机栈也是Java虚拟机自动管理的,栈类似于一个集合,但是有固定的容量,是由多个栈帧合起来的。每调用一个方法Java虚拟机就会在内存中分配对应的一块空间,这块空间也就是一块栈帧,当方法调用结束后,对应的栈帧就会被自动释放掉。栈的内存不需要通过GC去回收,而会自动释放。

本地方法栈:

  • 与虚拟机栈相似,主要作用于标注了native的方法。

6-10 Java内存模型之线程共享部分

元空间(MetaSpace)和永久代(PermGen)的区别:

  • 元空间使用本地内存,而永久代使用的是jvm的内存

MetaSpace相比PermGen的优势:

  • 字符串常量池存在于永久代中,容易出现性能问题和内存溢出

  • 类和方法的信息大小难以确定,给永久代的大小指定带来困难

  • 永久代会为GC带来不必要的复杂性,并且回收效率低

  • 方便HotSpot与其他JVM如Jrockit的集成

Java堆(Heap):

  • 对象实例的分配区域

  • GC管理的主要区域-新生代、老年代

6-11 Java内存模型之 常考题解析

JVM三大性能调优参数-Xms、-Xmx、-Xss的含义: java -Xms128m -Xmx128m -Xss256k -jar xxx.jar

  • Xss:规定了每个线程虚拟机栈(堆栈)的大小,256k足够。影响并发线程数的大小。

  • Xms:堆的初始值,

  • Xmx:堆能扩展达到的最大值,

一般讲Xms和Xmx设置成一样的,因为当Heap不够用发生扩容时会发生内存抖动,从而影响程序运行的稳定性。

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

  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求,不允许有可变数据结构、嵌套、递归存在

  • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定

  • 堆式存储:编译时或运行时模块入口都无法确定,动态分配

Java内存模型中堆和栈的区别: 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址 区别: 管理方式:栈自动释放,堆需要GC 空间大小:栈比堆小 碎片相关:栈产生的碎片远小于堆 分配方式:栈支持动态和静态分配,而堆只支持动态分配 效率:栈的效率比堆高

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

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.sayHelo();
    }
}

.intern()添加进字符串常量池,JDK6在永久代引发OutOfMemoryError异常,JDK7+则不受永久代约束。

/*
VM options:-XX:MaxPermSize=6M -XX:PermSize=6M
 */
public class PermGenErrTest {
    public static void main(String[] args) {
        for (int i = 0; i <= 1000; i++) {
            //将返回的随机字符串添加到字符串常量池中。
            getRandomString(1000000).intern();
        }
        System.out.println("Mission Complete");
    }
    //返回指定长度的随机字符串
    private static String getRandomString(int length) {
        //字符串源
        String str = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890";
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }
}
public class InternDifference {
    public static void main(String[] args) {
        String s = new String("a");
        s.intern();
        String s2 = "a";
        System.out.println(s == s2);//比较字符串的地址

        String s3 = new String("a") + new String("a");
        s3.intern();
        String s4 = "aa";
        System.out.println(s3 == s4);
    }
}
/*
JDK7+:false true
JDK6:false false
 */

不同JDK版本之间的intern()方法的区别-JDK6:

不同JDK版本之间的intern()方法的区别-JDK7+:

JDK6使用intern()向字符串常量池添加的是字符串的副本;而JDK7+添加的是字符串的引用

知识点结构图:



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