[Java]一篇文章浅入深出了解「JVM原理」以及常见问题

前言

知识学了又丢,不得不开个专场,把「JVM」的内容专门放在这里,常更新,欢迎收藏~

JVM

为什么要先编译成字节码

  • 为了实现跨平台「一次编写,到处运行」

JVM如何加载.class文件

  • JVM 全称是Java Virtual Machine ,Java 虚拟机,也就是在计算机上再虚拟一个计算机,这和我们使用 VMWare 不一样,那个虚拟的东西你是可以看到的,这个JVM 你是看不到的,它存在内存中。

  • 内存中的虚拟机,JVM 的命令集则是可以到处运行的,因为JVM 做了翻译,根据不同的CPU ,翻译成不同的机器语言。

  • JVM的组成部分

在这里插入图片描述

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

  • Execution Engine:对命令进行解析

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

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

  • 参考

谈谈反射

native方法(Class.forName)

  • 对反射的最初接触是学习jdbc时,加载数据库驱动时会这样写:Class.forName("com.mysql.jdbc.Driver")

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

  • 通过代码理解原理

    • 创建类com.dai.Robot

      • 该类创建的目的,是方便类com.dai.ReflectSample的反射调用
      package com.dai;
      
      public class Robot {
          private String name;
          static {
              System.out.println("I am first");
          }
      
          public Robot() {
              this.name = "robot";
          }
      
          public void sayHi(String helloSentence) {
              System.out.println(helloSentence + " " + name);
          }
          private String throwHello(String tag) {
              return "Hello " + tag;
          }
      
      }
      
    • 创建类com.dai.ReflectSample

      • 引入一个类"com.dai.Robot",反射实例化该类;
        • forName()这个静态方法调用了启动类加载器(就是加载javaAPI的那个加载器,也就是下文的ClassLoader)时,VM会执行该类的静态代码段
        • newInstance()只能调用无参构造函数(new():强类型。相对高效。能调用任何public构造函数)。
        • newInstance()是实现IOC、反射、依赖倒置 等技术方法的必然选择,new 只能实现具体类的实例化,不适合于接口编程。类里面就是通过这个类的默认构造函数构建了一个对象,如果没有默认构造函数就抛出InstantiationException, 如果没有访问默认构造函数的权限就抛出IllegalAccessException
      • 引入一个类"com.dai.Robot",反射实例化类方法,
        • getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。
        • getMethod*()获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。
        • 对于private的函数,需要设置可访问为true,再唤醒invoke执行实例对应的方法
      package com.dai;
      
      import java.lang.reflect.Method;
      
      /**
       * author daioo
       * create 2019-03-05 00:20
       */
      public class ReflectSample {
          public static void main(String[] agrs) throws ReflectiveOperationException {
              Class rc = Class.forName("com.dai.Robot");
              System.out.println("Class name is " + rc.getName());
              Robot r = (Robot) rc.getDeclaredConstructor().newInstance();
              r.sayHi("hello");
              Method getHello = rc.getDeclaredMethod("throwHello", String.class);
              getHello.setAccessible(true);
              Object str = getHello.invoke(r, "Bob");
              System.out.println("getHello result is " + str);
      
          }
      }
      
      
  • 参考

谈谈ClassLoader

创建一个自定义ClassLoader

  • Java的核心组件。ClassLoader主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流,通过上文说的反射技术,将其装载进系统,然后交给Java虚拟机进行连接、初始化等操作

  • IDEA中,MAC默认按键COMMAND+O查找类"classloader"源文件,我们将要用的函数"findClass"、“loadClass”

  • 通过代码理解原理

    • 创建系统外部Class二进制文件

      • 在桌面创建一个外部引用文件Wali.class(具体操作是:先写wali.java文件,在用命令javac编译为.class文件

      • 先写wali.java文件

        public class Wali{
        	static{
        		System.out.print("hello Wali");
        	}
        }
        
      • 编译java文件为class文件

    • 创建类com.dai.MyClassLoader

      • 函数findClass,重写ClassLoader类的函数,读取二进制流,返回类文件

      • 函数loadClassData,从系统外部获得Class二进制数据流

        package com.dai;
        
        import java.io.File;
        import java.io.FileInputStream;
        import java.io.InputStream;
        
        /**
         * author daioo
         * create 2019-03-05 14:33
         */
        public class MyClassLoader extends ClassLoader {
            private String path;
            private String classLoaderName;
        
            public MyClassLoader(String path, String classLoaderName) {
                this.path = path;
                this.classLoaderName = classLoaderName;
            }
            //用于寻找类文件
            @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;
                byte[] out = null;
                try{
                    in = new FileInputStream(new File(name));
                    out = new byte[in.available()];
                    in.read(out);
        
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        in.close();
            
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
        
                }
                return out;
            }
        }
        
    • 创建类com.dai.ClassLoaderChecker

      • 测试类,通过反射调用外部系统.class实例化的类的函数

        package com.dai;
        
        /**
         * author daioo
         * create 2019-03-05 14:43
         */
        public class ClassLoaderChecker {
            public static void main(String[] args) throws Exception{
                MyClassLoader m = new MyClassLoader("/Users/daioo/Desktop/", "myClassLoader");
                Class c = m.loadClass("Wali");
                System.out.println(c.getClassLoader());
                c.getDeclaredConstructor().newInstance();
        
            }
        }
        

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

通过openjdk查看对应的c代码

类加载器的双亲委派机制

  • 双亲委派模型的源码实现

    主要体现在ClassLoader的loadClass()方法,思路很简单:先检查是否已经被加载,若没有被加载则调用父类的LoadClass()方法,若父类加载器为空,则默认使用启动类加载器作为父类加载器,如果父类加载器加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载

  • 举个栗子
    例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。

  • 通过代码理解原理

    • 依然用上文用到的类com.dai.ClassLoaderChecker文件

      package com.dai;
      
      /**
       * author daioo
       * create 2019-03-05 14:43
       */
      public class ClassLoaderChecker {
          public static void main(String[] args) throws Exception{
              MyClassLoader m = new MyClassLoader("/Users/daioo/Downloads/", "myClassLoader");
              Class c = m.loadClass("Wali");
              System.out.println(c.getClassLoader());
              System.out.println(c.getClassLoader().getParent());
              System.out.println(c.getClassLoader().getParent().getParent());
              System.out.println(c.getClassLoader().getParent().getParent().getParent());
              c.getDeclaredConstructor().newInstance();
          }
      }
      
  • 参考

loadClass和forName的区别

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

  • 链接:其中解析步骤是可以选择的

    • 检查:检查载入的class文件数据的正确性
    • 准备:给类的静态变量分配存储空间 并设置类变量初始值
    • 解析:JVM将常量池的符号引用转成直接引用
  • 初始化:对静态变量,静态代码块执行初始化工作

  • 透过源码分析
    • Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);
    • ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);
    • 第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行
  • 参考

你了解Java的内存模型么

  • 内存简介

    image-20190306164612116

  • 地址空间的划分

    • 内核空间
    • 用户空间——java运行时实际运行的空间
  • 内存模型

    • Runtime Data Area 运行时数据区

在这里插入图片描述

  • 线程私有:程序计数器、虚拟机栈、本地方法栈

  • 线程共享:MetaSpace、Java堆

  • 程序计数器

    • 当前线程所执行的字节码行号指示器(逻辑),不是物理计数器
    • 改变计数器的值来选取下一条需要执行的字节码指令
    • 和线程是一对一的关系即“线程私有”,即每一个线程都有一个独立的程序计数器
    • 对Java方法计数,如果是Native方法则计数器值为Undefined
    • 不会发生内存泄露
  • 局部变量表和操作数栈

    • 局部变量表:包含方法执行过程中的所有变量
    • 操作数栈:入栈、出栈、复制、交换、产生消费变量
  • 虚拟机栈

    • java方法执行的内存模型,包含多个栈帧

在这里插入图片描述

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

    • 递归过深,栈帧数超出虚拟栈深度,如Fibonacci(1000000)
  • 虚拟机栈过多会引发java.lang.OutOfMemoryError异常

    public void stackLeakByThread(){
        while(true){
            new Thread(){
                public void run(){
                    while(true){
    
                    }
                }
            }.start();
        }
    }
    
  • 本地方法栈

    • 本地方法栈和Java栈所发挥的作用非常相似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
    • 本地方法栈与虚拟机用到的 Native 方法相关,区别就是:
      • Java栈为JVM执行Java方法服务
      • 本地方法栈为JVM执行Native方法服务。
  • 元空间(MetaSpace)

    • 元空间(MetaSpace)与永久代(PermGen)的区别
      • 元空间使用本地内存,而永久代使用的是jvm的内存
      • java.langOutOfMemoryError:PermGen space
    • MetaSpace相比PermGen的优势
      • 字符串常量池存在永久代中,容易出现性能问题和内存溢出(看下文不同JDK版本之间的intern()方法的区别)
      • 类和方法信息大小难以确定,给永久代的大小指定带来困难
      • 永久代会为GC带来不必要的复杂性
      • 方便HotSpot与其他JVM如Jrockit的集成
  • Java堆(Heap)

    • 在JVM启动时创建;堆是存储Java对象的地方;
    • GC管理的主要区域,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代
  • JVM三大新能调优参数-Xms -Xmx -Xss的含义

    • -Xss:规定了每个线程虚拟机栈(堆栈)的大小
    • -Xms:堆的初始值
    • -Xmx:堆能达到的最大值
  • Java内存模型中堆和栈的区别

    • 内存分配策略

      • 静态存储:编译时确定每个数据目标在运行时的存储空间需求
      • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
      • 堆式存储:编译时或运行时模块入口都无法确定,动态分配,比如可变长度串、对象实例
    • 堆和栈的联系

      • 引用对象、数组时,栈里定义变量保存堆中目标的首地址。使得可以在栈中的变量使用堆中的数组

在这里插入图片描述

  • 总结

    • 管理方式:栈自动释放,堆需要GC
    • 空间大小:栈比堆小
    • 碎片相关:栈产生的碎片远小于堆
    • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
    • 效率:栈的效率比堆高;栈操作简单只有push/pop,堆却灵活,动态分配
  • 元空间、堆、线程独占部分间的联系 - 内存角度

    在这里插入图片描述
    在这里插入图片描述

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

    String s = new String("a");
    s.intern();
    
    • JDK6:调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中该字符串的引用。否则,将此字符串对象添加到常量池中(即在池中创建该字符串),并且返回该字符串对象的引用。*

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

    • 通过代码理解区别

      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);
          }
      }
      
      • jdk6输出falase false
      • jdk6+输出false true
    • 通过代码理解「字符串常量池存在永久代中,容易出现性能问题和内存溢出」

      package com.dai;
      
      import java.util.Random;
      
      /**
       * author daioo
       * create 2019-03-06 22:21
       */
      public class PermGenErrTest {
          public static void main(String[] args) {
              for(int i = 0; i <= 1000; i++) {
                  gerRandomString(1000000).intern();
              }
              System.out.println();
          }
          private static String gerRandomString(int length) {
              String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
              Random random = new Random();
              StringBuffer sb = new StringBuffer();
              for (int i = 0; i < length; i++) {
                  int number = random.nextInt(62);
                  sb.append(str.charAt(number));
              }
              return sb.toString();
          }
      }
      
      • 如果使用IDEA编译器,可以配置JAVA虚拟机内存大小,操作:菜单栏run->Edit Configurations,在VM options写入-XX:MaxPermSize=6M -XX:PermSize=6M
        在这里插入图片描述

      • 输出

        Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
        	at java.lang.String.intern(Native Method)
        	at com.dai.PermGenErrTest.main(PermGenErrTest.java:12)
        
        Process finished with exit code 1
        
    • 参考

  • 参考

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值