Java 虚拟机

https://www.bilibili.com/video/BV1yE41187A3?p=2

JDK

Java Development ToolKit

Java Development ToolKit(Java开发工具包),包括了JRE,一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)

Java Runtime Enviromental

Java Runtime Enviromental(java运行时环境),与JDK相比,它不包含开发工具——编译器、调试器和其它工具,如javac

Java Virtual Mechinal

Java Virtual Mechinal(JAVA虚拟机),JVM是JRE的一部分,JVM 的主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 的指令集或 OS 的系统调用

Java 跨平台与C/C++的区别

  • c/c++: 源码跨平台,不同平台需要重新编译
  • Java:字节码跨平台,一次编译在不同平台只需要安装jvm即可运行字节码

JVM生命周期

  • 启动

启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点

  • 运行

main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程

  • 销毁

当程序中的所有非守护线程都终止时,JVM才退出

JVM体系结构

执行引擎

执行字节码,或者执行本地方法

类加载器

作用:加载.class文件

三种类加载器

  • AppClassLoader:应用类加载器

负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)

  • ExtClassLoader:扩展类加载器

负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)

  • BootstrapClassLoader:启动类加载器

负责加载JDK中的核心类库,由c++来写的,加载的是Javahome/jre/lib/rt.jar等

JVM什么时候加载.class文件

  1. new

实例化对象时会触发类加载器去类加载对应的路径去查找对应的.class文件,并创建Class对象

  1. 反射

反射时,加载字节码到内存后生产的只是一个Class对象,要得到具体的对象实例还需要使用Class对象的newInstance()方法来创建具体实例

双亲委派机制

ClassLoader源码

 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) {
                        // 获取父类加载器直到父类为null
                        c = parent.loadClass(name, false);
                    } else {
                        // 使用根加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父类都未找到,则交给自定义类加载器
                   c = findClass(name);
                }
    }

即委派给父类加载器(AppClassLoader =》 ExtClassLoader =》 BootstrapClassLoader)加载。这样就不允许用户串改jdk的源码,也保证了代码的安全

  • Class.getClassLoader():获取当前Class类的 类加载器
public class ClassLoader {

    public static void main(String[] args) {
        ClassLoader myclass = new ClassLoader();

        Class<? extends ClassLoader> aClass = myclass.getClass();

        /**
         * 1. 类加载器收到加载请求,加载ClassLoader类
         * 2. AppClassLoader将这个请求委托给父类加载器去完成,一直向上委托,直到BootstrapClassLoader
         * 3. BootstrapClassLoader、ExtClassLoader都未找到ClassLoader
         * 4. 最终由AppClassLoader在用户目录找到该类,并完成加载
         */
        System.out.println(aClass.getClassLoader());  // AppClassLoader

        System.out.println(aClass.getClassLoader().getParent());  // ExtClassLoader

        System.out.println(aClass.getClassLoader().getParent().getParent()); // BootstrapClassLoader,返回null,java不可见
    }
}

自定义类加载器

  • 继承ClassLoader
  • 重写findClass方法,使用双亲委派模式,委托其父类去加载(因此需要删除父加载器工作目录中的class,让自定义加载器加载)
  • (重写loadClass:不使用双亲委派)

重写findClass

  1. 获取.class字节码文件的字节数组
  2. this.defineClass将.class的字节数组转化为Class类实例,并返回
public class MyClassLoad extends ClassLoader{
    // 字节码存储地址
    private String path;

    public MyClassLoad(String path) {
        this.path = path;
    }



    public MyClassLoad(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File(path);

        try {
            // 获取字节码文件的字节数组
            byte[] bytes = getClassBytes(file);
            // 将.class的字节数组转化为Class类实例
            Class<?> myclass = this.defineClass(name, bytes, 0, bytes.length);
            return myclass;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    // 将文件转换为字节数组
    private byte[] getClassBytes(File file) throws IOException {
        // 获取.class 文件的字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }

        fis.close();

        return baos.toByteArray();
    }
}

测试

  • 需要删除父类加载器加载目录中的.class文件(双亲委派会使用父类进行加载)
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoad myClassLoad = new MyClassLoad("D:\Person.class");
        Class<?> aClass = myClassLoad.loadClass("com.reflect.Person");
        System.out.println(aClass.getClassLoader());
        Object o = aClass.newInstance();

    }

Java内存区域(运行时数据区域)

Java运行时数据区域和内存模型是不一样的东西,内存区域是指Jvm 运行时将数据分区域存储,强调对内存空间的划分,而内存模型定义了JVM 在计算机内存(RAM)中的工作方式

Java堆

Java堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例

堆内存划分

  • 年轻代

主要是用来存放新生的对象。新生代又细分为 Eden区、SurvivorFrom区、SurvivorTo区
新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中)
Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空

  • 老年代

随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常

  • 永久代(元空间)

值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度

设置堆参数

设置初始化大小为500M,最大可用内存为500M,并打印GC详细信息

  • -Xms:设置初始分配大小,默认为物理内存的“1/64”
  • -Xmx:最大分配内存,默认为物理内存的“1/4”
VM options:-Xms500m -Xmx500m -XX:+PrintGCDetails

在内存溢出时Dump文件

  • -XX:+HeapDumpOnXXX:在xx时生成dump文件
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError

设置年轻代阈值,到达此阈值进入老年代,默认15

-XX:MaxTenuringThreshold=20

虚拟机栈

  • 线程隔离
  • 每个方法都会产生一个栈帧(Stack Frame)
  • 每个栈帧中存储局部变量表、操作数栈、动态链接、方法出口等信息

本地方法栈

和虚拟机栈区别是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务

public class Thread implements Runnable {
     ...
      
    /**
     * Thread类中的native方法
     * 测试是否有线程被中断
     */
    private native boolean isInterrupted(boolean ClearInterrupted);

    ...
}

元空间

元空间用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,线程共享

  • JDK8之前,方法区的实现是永久代(Perm),JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之
  • 线程共享
  • 存储类的基本信息(运行时常量池、静态变量、接口信息等)

在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

运行时常量池
运行时常量池(Runtime Constant Pool)是元空间的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器

  • 线程私有
  • 占少量内存空间
  • 提供当前线程所执行的字节码的行号指示器,为了线程切换后能恢复到正确的执行位置

* 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

  • 可以通过 -XX:MaxDirectMemorySize 参数来设置最大可用直接内存,如果启动时未设置则默认为最大堆内存大小,即与 -Xmx 相同

JVM参数

参数 含义 默认值
-Xms 初始堆大小 物理内存的1/64(<1GB)
-Xmx 最大堆大小 物理内存的1/4(<1GB)
-XX:NewSize 设置年轻代大小(for 1.3/1.4)
-XX:PermSize 设置持久代(perm gen)初始值 物理内存的1/64
-XX:MaxPermSize 设置持久代最大值 物理内存的1/4
-Xss 每个线程的堆栈大小
-XX:MaxTenuringThreshold 垃圾最大年龄
-XX:+CollectGen0First FullGC时是否先YGC false
-XX:+PrintGCDetails 打印GC详细

Java 内存模型(JMM)

参考:
https://www.cnblogs.com/czwbig/p/11127124.html

原文地址:https://www.cnblogs.com/xiongyungang/p/13649276.html