JVM入门之十万个为什么

349 阅读15分钟

基础

什么是JVM?

JVM:java虚拟机.它是一种利用软件方法实现的抽象的计算机基于下层的操作系统和硬件平台,可以在上面执行java字节码程序.java编译器生成JVM能理解的字节码文件,JVM将指令翻译成不同平台的机器码,在特定平台上运行.

JVM的作用是什么?

  1. 加载.class文件
  2. 管理和分配内存
  3. 执行垃圾收集

JVM的位置在哪?

基于操作系统之上的.操作系统又基于硬件.

JVM位置

JVM的体系结构?

  1. 类加载器
  2. 运行时数据区
  3. 执行引擎
  4. 本地方法接口

体系结构

JVM的运行时数据区是什么样的?

运行时数据区

名称 特征 作用 配置参数 异常
程序计数器 占用内存小,线程私有,生命周期与线程相同 大致为字节码行号指示器
虚拟机栈 线程私有,生命周期与线程相同,使用连续的内存空间 Java 方法执行的内存模型,存储局部变量表、操作栈、动态链接、方法出口等信息 -Xss StackOverflowError,OutOfMemoryError
java堆 线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址 保存对象实例,所有对象实例(包括数组)都要在堆上分配 -Xms,-Xsx,-Xmn OutOfMemoryError
方法区 线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 -XX:PermSize:16M,-XX:MaxPermSize64M OutOfMemoryError
运行时常量池 方法区的一部分,具有动态性 存放字面量及符号引用

本地方法栈和本地方法接口的区别是什么?

  1. 本地方法接口: 可以执行本地方法的接口.通过本地方法接口来调用本地方法.
  2. 本地方法栈: 本地方法执行的栈.

运行时数据区哪里不会产生垃圾?

  1. 程序计数器
  2. 本地方法栈 3, 栈

为什么栈里不会有垃圾?

执行完了就会弹出去.

JVM调优是调哪里?

  1. 方法区

什么是类的加载?

将类的class文件二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在堆中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.

什么是类加载器?

Java运行时环境的一部分,负载动态加载class文件.

有哪些类加载器?

  1. 启动类加载器
  2. 拓展类加载器
  3. 应用类加载器
  4. 自定义类加载器 测试代码如下:
public class Car {
    public int age;
    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();
        System.out.println(car1.hashCode());//356573597
        System.out.println(car2.hashCode());//1735600054
        System.out.println(car3.hashCode());//21685669
        Class<? extends Car> aClass = car1.getClass();
        Class<? extends Car> bClass = car1.getClass();
        System.out.println(aClass.hashCode());//1956725890
        System.out.println(bClass.hashCode());//1956725890
        ClassLoader classLoader = aClass.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        ClassLoader parent = classLoader.getParent();
        System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@7f31245a \jre\lib\ext
        ClassLoader parent1 = parent.getParent();
        System.out.println(parent1);//null:1.不存在 2.程序获取不到 rt.jar
    }
}

总结: 默认得到的类加载器是应用类加载器,它的父类是拓展类加载器.再取父类取不到为null,因为启动类加载器是用C语言实现的,无法合适的返回.

启动类加载器的作用是什么?

Bootstrap ClassLoader,使用C++实现,负责加载rt.jar下的文件.

扩展类加载器的作用是什么?

Extension ClassLoader.负责加载\jre\lib\ext下的文件.

应用程序类加载器的作用是什么?

Application ClassLoader.负责加载ClassPath所指定的类.如果应用程序中没有自定义过自己的类加载器,一般情况下就是程序中默认的类加载器.

什么是双亲委派机制?

当一个类加载器收到类加载的请求,首先不会自己尝试去加载这个类,而是请求委托给父加载器去完成,依次向上.所有的类加载请求最终都会传递到启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,子加载器才会尝试自己去加载该类.

双亲委派机制的作用是什么?

  1. 防止重复加载一个class文件.加载前会询问是否已经加载过了.
  2. 保证核心class文件不被覆盖.即使有同包同名的文件,也只会让父加载器去加载,父加载器加载的时候又只会加载指定文件夹下的class文件,这样,外部的同名文件不会被加载,保证安全.

怎么自定义类加载器?

  1. 将字节码文件放在classpath文件路径下.
  2. 自定义类加载器.重写findClass方法来加载字节码文件 测试类代码如下:
public class Hello {
    public Hello(){
        System.out.println("hello world ");
    }
}

自定义的classLoader如下:

public class MyClassLoader extends ClassLoader{
    public MyClassLoader() {
    }
    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }
    @Override
    //重写findClass方法而不是loadClass方法,防止破坏双亲委派机制
    protected Class<?> findClass(String name) throws ClassNotFoundException {
       //从自定义的位置取到class文件
        File file = getClassFile(name);
       try{
           //读取class文件内容到内存
           byte[] bytes = getClassBytes(file);
           Class<?> c = this.defineClass(name,bytes,0,bytes.length);
           return c;//返回Class模板类
       }catch (Exception 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();
    }
    private File getClassFile(String name) {
        File file = new File("D:/Hello.class");
        return file;
    }
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader myClassLoader = new MyClassLoader();//自定义类加载器
        //加载类.注意,该类不能在classpath下,否则仍然会被默认的AppClassLoader加载
        Class<?> cl = myClassLoader.loadClass("com.guohao.classLoader.Hello");
        Object obj = cl.newInstance();
        System.out.println(obj.getClass().getClassLoader());
    }
}

执行结果如下:

hello world 
com.guohao.classLoader.MyClassLoader@1540e19d

总结: 要加载的类不能在classpath目录下,否则仍然由默认的application ClassLoader加载.loadclass方法写全类名.

自定义类加载器的作用是什么?

  1. 读取非classpath下的class文件
  2. 读物网络传输的class文件,需要进行加密和解密操作
  3. 自定义类的实现机制,实现热部署等功能.

什么是沙箱安全机制?

参考: java中的安全模型(沙箱机制)

什么是native方法?

java通过本地方法接口(JNI)调用底层C语言的库,这些方法会进入本地方法栈.类如多线程的start0()方法.

什么是PC寄存器?

PC(Program Counter)寄存器,即程序计数器.每个线程都有它自己的PC寄存器,是该线程启动时创建的,保存下一条将要执行的指令地址.每一个线程都有一个程序计数器.如果执行的是Native方法,程序计数器是空的.

什么是方法区?

别名: 非堆,持久代,永生代. 存储类的信息.是线程共享的区域. 存放的内容概括就是: static,final,Class,常量池 JDK 1.8之后使用元空间(MetaSpace)替代持久代,元空间并不在JVM中,而是使用本地内存.

方法区

方法区

拓展: 方法区中到底是什么样的 拓展: 方法区之1:方法区介绍

JDK1.8以后已经没有方法区了吗?

方法区是虚拟机规范中定义的一个概念.hotspot虚拟机用持久代来实现方法区的功能.后来又提议取消持久代,只是方法区的实现不再由持久代来实现了,JDK1.8之后原来持久代中类的元信息会被放入本地内存的元数据区,而类的静态变量和内部字符串将放入到Java堆中.

什么是栈?

栈是一种数据结构.栈: 先进后出.队列: 先进先出. 虚拟机栈: 存放Java方法执行的内存模型:栈帧.Java虚拟机栈是线程私有的,它的生命周期与线程相同.

虚拟机栈

什么是栈帧?

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构.它是虚拟机运行时数据区中java虚拟机栈的栈元素.我们主要关注的是栈帧中的局部变量表部分.每一个栈帧对应着方法区中的一个方法.

栈帧中存储了哪些内容?

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法返回地址

什么是局部变量表?

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量. 局部变量表可以存放的内容有: 8种数据类型,对象的引用和returnAddress类型(指向一条字节码指令的地址).

局部变量表

参考链接: 深入理解JVM-java虚拟机栈

为什么main方法先执行,后结束?

因为main方法也是作为一个栈帧存放在虚拟机栈中.它先入栈,所以它最后出栈.

如果线程请求的栈深度大于虚拟机所允许的深度会发生什么?

抛出StackOverflowError异常.

怎么看用的是什么JVM?

使用java -version命令即可.

查看虚拟机类型

什么是堆?

Java 堆是JVM管理的最大的一块内存空间,主要用于存放各种类的实例对象.堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建. 堆被划分为两个不同的区域: 新生代,老年代.新生代又被划分为三个区域: Eden,From Survivor,To Surivor.

什么是新生区?

又名: 年轻代. 新生区又分为伊甸园区和幸存区.新生区主要存储新创建的对象,当新生区内存占满后,会触发Minor GC(轻GC),清理年轻代内存空间.

什么是养老区?

又名: 老年代. 存储长期存活的对象和大对象.年轻代中存储的对象,经过多次GC后仍然存活的对象会被移动到养老区.养老区占满后,会触发Full GC(重GC),即清理整个堆空间,包括新生区和养老区.如果Full GC后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常.

什么是元空间?

jdk1.6中的永久代从jdk1.7开始四分五裂了.到了jdk1.8: 符号引用存储在native heap中,字符常量和静态类型变量存储在普通的堆中(这个影响了String的intern()方法的行为),元空间中只存储类加载器的元数据信息了. 元空间是实现方法区的一种方式.

什么是永久区?

JDK1.8之前HotSpot虚拟机实现方法区的一种方式,现在永久区已经取消.而由元空间代替.

jdk1.6之前,jdk1.7,jdk1.8永久区的区别是什么?

jdk1.6及之前,方法区=永久区,永久区在堆中一个独立的空间(所以称为非堆). jdk1.7: 永久区的常量和静态变量移动到堆中. jdk1.8: 取消了永久区,改为元空间.而且元空间的位置在直接内存中(不在堆中).

什么时候触发Minor GC?

Eden区满的时候.

什么时候触发Full GC?

  1. 调用System.gc
  2. 老年代空间不足
  3. 通过Minor GC后进入老年代的对象大于老年代的可用内存
  4. Eden区,From 区向to区复制时,对象大于to区可用内存,转移到老年代的时候,老年代的内存也不够.

新生区中为什么要设置幸存区的from和to?

  1. 默认新生区占堆内存的1/4,比较小.而且新生区幸存者少,垃圾清理更为频繁.因此需要有轻GC来单独清理新生区,而不是花费时间清理全部堆内存.而且存活对象少,适合复制算法.
  2. 选择复制算法,就需要有一块空余空间. 如果五五分成,那么有一半空间浪费.如果不五五分成,那么下次空间from和to反转的时候,可用空间很小,而且很快又会触发Minor GC.因此,在from和to之外又增加了一块Eden区.

什么是复制算法?

将内存按照内存容量划分为大小相等的两块,每次只使用其中的一块.当这一块内存用完了,将还存活的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉.

复制算法

复制算法有哪些好处和坏处?

优点: 实现简单,运行高效. 缺点: 内存利用率低.

垃圾回收主要发生在哪个区?

堆和方法区.

垃圾回收机制?

什么是轻量级的垃圾回收?

Minor GC.就是发生在新生代中的垃圾回收动作,采用的是复制算法.当新生代内存满了,就会进行轻量级垃圾回收.由于java中的大部分对象不会长久的存活,因此新生代是垃圾回收的频繁区域.

轻量级垃圾回收的过程是怎样的?

对象在Eden出生以后,如果经过一次Minor GC后,还存活,并且to区域有足够内存空间来存储Eden和from区域的对象,那么复制算法将这些仍然存活的对象复制到to区域.当对象经历15次垃圾回收仍然存活,被放入养老区.

什么是重量级垃圾回收??

Full GC.整个堆空间进行垃圾回收,采用的是标记-清除算法. 因为养老区的都是从新生区来的,不会那么容易"死掉",因此,Full GC不会那么频繁,并且做一次Full GC比Minor GC的时间更长,是Minor GC的十倍以上.

标记-清除算法的过程是什么样的?

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象.

标记-清除算法

标记清除算法有什么优缺点?

缺点: 1. 效率问题: 标记和清除过程的效率不高 2. 空间问题: 标记清除之后产生大量不连续的内存碎片

什么是标记压缩算法?

类似于"标记-整理"算法,但是后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存.

标记-压缩算法

怎么判断对象是否存活?

  1. 可达性分析.从GC Roots向下搜索,搜索经过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,证明此对象不可用.
  2. 引用计数: 每一个对象有一个引用计数,新增一个引用时,引用计数加1,释放时计数减1,为0时可回收.但是此法不能解决对象循环引用问题. 引用算法无法回收的例子:
public class ReferenceCountingGC {
	private Object instance=null;
	private static final int _1MB=1024*1024;
	private byte[] bigSize=new byte[1024*1024];//1MB的堆空间
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ReferenceCountingGC objA=new ReferenceCountingGC();
		ReferenceCountingGC objB=new ReferenceCountingGC();
		//objA 和 objB相互引用
		objA.instance=objB;
		objB.instance=objA;
		objA=null;
		objB=null;
		System.gc();//调用GC
	}
}

GC的算法有哪些?

  1. 复制算法
  2. 标记-清除算法
  3. 标记-压缩算法

GC算法比较

GC算法比较

怎么获取虚拟机试图使用的最大内存?

long max = Runtime.getRuntime().maxMemory();,字节为单位.

怎么获得jvm的总内存?

long totalMemory = Runtime.getRuntime().totalMemory();

默认情况下,jvm分配的内存大小是多少?

Java 8,一般堆内存的初始容量为物理内存的1/64,最大内存不超过物理内存的1/4或者1G.

怎么设置虚拟机内存大小?

-Xmx8m -Xms8m -XX:+PrintGCDetails

设置虚拟机参数

怎么判断OOM原因?

  1. 加大堆内存看看运行效果
  2. 使用专业的工具分析问题

有哪些OOM错误?

  1. java.lang.OutOfMemoryError: Java heap space
  2. java.lang.OutOfMemoryError: Metaspace
  3. java.lang.OutOfMemoryError: unable to create new native thread
  4. java.lang.OutOfMemoryError:GC overhead limit exceeded

产生上面这些OOM的原因有哪些?

  1. 上面第一种情况是堆空间不足,当应用程序申请更多的内存,而Java堆内存已经满了无法满足应用程序对内存的需求,将抛出这个异常.
  2. 上面第二种情况是元空间内存不足,类或者类加载器的元数据超出了元空间的内存限制.
  3. 上面第三种是创建了太多的线程,而能创建的线程的数量是有限的.
  4. 上面第4中异常是在并行或者并发回收器在GC回收时间过长,超过98%的时间用来做GC并且回收了不到2%的堆内存,然后抛出这种异常提前预警,避免内存过小造成应用不能正常工作. 参考: jvm系列(十):教你如何成为Java的OOM Killer

元空间有什么特点?

不在虚拟机中,使用本地内存.

有哪些内存分析工具?

  1. MAP
  2. Jprofile

内存分析工具的作用是什么?

  1. 分析dump内存文件,快速定位内存泄漏
  2. 获得堆中的数据
  3. 获得堆中的大对象

JVM内存模型分区?

JVM内存模型与分区

什么是JMM?

Java内存模型.它的作用是屏蔽硬件差异,让一套代码在不同的平台能达到相同的访问结果.

Java内存模型

参考文章

参考: 关于Jvm知识看这一篇就够了