聊到JVM(还怕面试官问JVM吗?)

点击上方"程序员历小冰",选择“置顶或者星标”css

   你的关注意义重大!
java


前言 
不管什么级别的Java从业者, JVM都是进阶时必须迈过的坎。无论是工做仍是面试中,JVM都是必考题。 若是不懂JVM的话,薪酬会很是吃亏(近70%的面试者挂在JVM上了)。

  • 请你谈谈你对JVM的理解?c++

  • JVM类加载器是怎么样的?有几种?程序员

  • 什么是OOM,什么是StackOverFlowError? 怎么分析?web

  • JVM经常使用调优参数有哪写?面试

  • GC有几种算法?分别是怎么执行的?算法

  • 你知道JProfiler吗,怎么分析Dump文件?编程


第一次看到这些真真实实的面试题的时候,我~



这都什么玩意???????
通过一段时间的研究!!接下来,我将以大白话从头至尾给你们讲讲 Java虚拟机 !!
不对的地方还请你们指正~

一、什么是JVM?在哪? 


JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是经过在实际的计算机上仿真模拟各类计算机功能来实现的。

百度的解释云里雾里,对于咱们Java程序员,说白了就是:
  • JVM本质上是一个程序,它能识别.class 字节码文件(里面存放的是咱们对.java编译后产生的二进制代码),而且可以解析它的指令,最终调用操做系统上的函数,完成咱们想要的操做!安全


  • 关于Java语言的跨平台性,就是由于JVM,咱们能够将其想象为一个抽象层,只要这个抽象层JVM正确执行了.class文件,就能运行在各类操做系统之上了!这就是一次编译,屡次运行微信


对于JVM的位置
  • JVM是运行在操做系统之上的,它与硬件没有直接的交互






二、JVM、JRE、JDK 的关系 


JDK(Java Development Kit):Java开发工具包

JRE(Java Runtime Environment):Java运行环境

JDK = JRE + javac/java/jar 等指令工具

JRE = JVM + Java基本类库






三、JVM体系结构 

Java虚拟机主要分为五大模块:

    • 类装载器子系统

    • 运行时数据区

    • 执行引擎

    • 本地方法接口

    • 垃圾收集模块

    

    


  • 方法区是一种特殊的堆

  • 栈里面不会有垃圾,用完就弹出了,不然阻塞了main方法

  • 垃圾几乎都在堆里,因此JVM性能调优%99都针对于堆



四、三种JVM(了解) 


Sun公司  HotSpot (咱们都用的这个)


BEA公司  JRockit
IBM公司  J9 VM

五、类加载器  


做用:加载 .Class 字节码文件

一、回顾new对象的过程

public class Student {
    //私有属性
    private String name;

    //构造方法
    public Student(String name) {
        this.name = name;
    }
}

类是模板、模板是抽象的;对象是具体的,是对抽象的实例化

//运行时,JVM将Test的信息放入方法区
public class Test{
    //main方法自己放入方法区
  public static void main(String[] args){
        //s一、s二、s3为不一样对象
        Student s1 = new Student("zsr");  //引用放在栈里,具体的实例放在堆里
        Student s2 = new Student("gcc");
        Student s3 = new Student("BareTH");
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
        System.out.println(s3.hashCode());
        //class一、class二、class3为同一个对象
        Class<? extends Student> class1 = s1.getClass();
        Class<? extends Student> class2 = s2.getClass();
        Class<? extends Student> class3 = s3.getClass();
        System.out.println(class1.hashCode());
        System.out.println(class2.hashCode());
        System.out.println(class3.hashCode());
    }
}
根据结果,咱们发现:

    • s一、s二、s3的hashcode是不一样的,由于是三个不一样的对象,对象是具体的



    • class一、class二、class3的hashcode是相同的,由于这是类模板,模板是抽象的

    

咱们画图分析如下 new一个对象的流程
  1. 首先Class Loader读取字节码.class文件,加载初始化生成Student模板类


  2. 经过Student模板类new出三个对象



那么Class Loader具体是怎么执行咱们的.class字节码文件呢,这就引出了咱们类加载器~


二、类加载器的类别

咱们编写这样一个程序



根据返回结果,咱们来说解如下三种加载器:

级别从高到底

1.启动类(根)加载器:BootstrapClassLoader
    1. c++编写,加载java核心库 java.*,构造拓展类加载器应用程序加载器

    2. 根加载器加载拓展类加载器,而且将拓展类加载器的父加载器设置为根加载器

    3. 而后再加载应用程序加载器,应将应用程序加载器的父加载器设置为拓展类加载器

    4. 因为引导类加载器涉及到虚拟机本地实现细节,咱们没法直接获取到启动类加载器的引用;这就是上面那个程序咱们第三个结果为null的缘由。

    5. 加载文件存在位置



    
2. 拓展类加载器:PlatformClassLoader
    1. java编写,加载扩展库,开发者能够直接使用标准扩展类加载器。

    2. java9以前为ExtClassloader,Java9之后更名为PlatformClassLoader

    3.     加载文件存在位置




3.应用程序加载器:AppClassLoader
            1 . java 编写,加载程序所在的目录
    

        2. 是Java默认的类加载器


4.用户自定义类加载器:CustomClassLoader
            1 . java 编写,用户自定义的类加载器,可加载指定路径的 class 文件


六、双亲委派机制 

一、什么是双亲委派机制

    • 类加载器收到类加载的请求


    • 将这个请求向上委托给父类加载器去完成,一直向上委托,直到根加载器BootstrapClassLoader


    • 根加载器检查是否可以加载当前类,能加载就结束,使用当前的加载器;不然就抛出异常,通知子加载器进行加载;自加载器重复该步骤。

    

二、做用

举个例子:咱们重写如下java.lang包下的String类



发现报错了,这就是 双亲委派机制 起的做用,当类加载器委托到 根加载器 的时候, String类 已经被 根加载器 加载过一遍了,因此不会再加载,从必定程度上防止了危险代码的植入!!


做用总结
    1.  防止重复加载同一个 .class 。经过不断委托父加载器直到根加载器,若是父加载器加载过了,就不用再加载一遍。保证数据安全。


    2. 保证系统核心.class,如上述的String类不能被篡改。经过委托方式,不会去篡改核心.class,即便篡改也不会去加载,即便加载也不会是同一个.class对象了。不一样的加载器加载同一个.class也不是同一个class对象。这样保证了class执行安全。


    

七、沙箱安全机制 

这里引用了这篇博文引用连接,了解即

什么是沙箱?

Java安全模型的核心就是Java沙箱(sandbox)


    1. 沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,而且严格限制代码对本地系统资源访问,经过这样的措施来保证对代码的有效隔离,防止对本地系统形成破坏。


    沙箱主要限制系统资源访问,系统资源包括CPU、内存、文件系统、网络。不一样级别的沙箱对这些资源访问的限制也能够不同。


全部的Java程序运行均可以指定沙箱,能够定制安全策略。

java中的安全模型演进

在Java中将执行程序分红 本地代码 远程代码 两种
  • 本地代码可信任,能够访问一切本地资源。

  • 远程代码不可信信在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。

以下图所示



如此严格的安全机制也给程序的功能扩展带来障碍,好比当用户但愿远程代码访问本地系统的文件时候,就没法实现。
所以在后续的  Java1.1  版本中,针对安全机制作了改进,增长了 安全策略 ,容许用户指定代码对本地资源的访问权限。
以下图所示
Java1.2 版本中,再次改进了安全机制,增长了 代码签名
  • 不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不一样的运行空间,来实现差别化的代码执行权限控制。

以下图所示


当前最新的安全机制实现,则引入了 域 (Domain)  的概念。
  • 虚拟机会把全部代码加载到不一样的系统域应用域

  • 系统域部分专门负责与关键资源进行交互

  • 应用域部分则经过系统域的部分代理来对各类须要的资源进行访问。

  • 虚拟机中不一样的受保护域 (Protected Domain),对应不同的权限 (Permission)。存在于不一样域中的类文件就具备了当前域的所有权限,以下图所示

组成沙箱的基本组件

1. 字节码校验器(bytecode verifier)

确保Java类文件遵循Java语言规范。这样能够帮助Java程序实现内存保护。但并非全部的类文件都会通过字节码校验,好比核心类(如上述java.lang.String)。

2. 类装载器(class loader)

其中类装载器在3个方面对Java沙箱起做用
  • 它防止恶意代码去干涉善意的代码;

  • 它守护了被信任的类库边界;

  • 它将代码纳入保护域,肯定了代码能够进行哪些操做。

虚拟机为不一样的类加载器载入的类提供不一样的命名空间,命名空间由一系列惟一的名称组成,每个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。


  1. 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而没法使用;


  2. 因为严格经过包来区分了访问域,外层恶意的类经过内置代码也没法得到权限访问到内层类,破坏代码就天然没法生效。


  • 存取控制器(access controller):存取控制器能够控制核心API对操做系统的存取权限,而这个控制的策略设定,能够由用户指定。

  • 安全管理器(security manager):是核心API和操做系统之间的主要接口。实现权限控制,比存取控制器优先级高。

  • 安全软件包(security package):java.security下的类和扩展包下的类,容许用户为本身的应用增长新的安全特性,包括:


    1. 安全提供者

    2. 消息摘要

    3. 数字签名

    4. 加密

    5. 鉴别


八、Native本地方法接口 

JNI:Java Native Interface

本地接口的做用是融合不一样的编程语言为Java所用,它的初衷是融合C/C++程序


native :凡是带native关键字的,说明java的做用范围达不到了,会去调用底层c语言的库!进入本地方法栈,调用 本地方法接口JNI ,拓展Java的使用,融合不一样的语言为Java所用
    • Java诞生的时候C、C++横行,为了立足,必需要能调用C、C++的程序


    • 因而在内存区域中专门开辟了一块标记区域:Native Method Stack,登记Native方法


    • 最终在执行引擎执行的的时候经过JNI(本地方法接口)加载本地方法库的方法


目前该方法使用的愈来愈少了,除非是与硬件有关的应用,好比经过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。由于如今的异构领域间通讯很发达,好比可使用 Socket通讯,也可使用 Web service等等,了解便可!


九、PC寄存器  

程序计数器 :Program Counter Register
    • 每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个很是小的内存空间,几乎能够忽略不计




十、方法区 

方法区 :Method Area
  • 方法区是被全部线程共享,全部字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,全部定义的方法的信息都保存在该区域,此区域属于共享区间


  • 方法区与Java堆同样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,可是它却有一个别名叫作Non-Heap(非堆),目的应该是与Java 堆区分开来。



1. 方法区中有啥?

  1. 静态变量(static)

  2. 常量(final)

  3. 类信息(构造方法、接口定义)

  4. 运行时的常量池


2. 建立对象内存分析



  • 建立一个对象时,方法区中会生成对应类的抽象模板;还有对应的常量池、静态变量、类信息、常量


  • 咱们经过类模板去new对象的时候


    • 堆中存放实例对象

    • 栈中存放对象的引用,每一个对象对应一个地址指向堆中相同地址的实例对象


例如这个例子中,生成了对应的Person模板类,name常量“zsr”放在常量池中,三个对象的引用放在栈中,该引用指向放在堆中的三个实例对象。
这就是堆、栈、方法区的交互关系

十一、栈  

又称 栈内存 ,主管程序的运行,生命周期和线程同步,线程结束,栈内存就释放了, 不存在垃圾回收
  • 栈:先进后出

  • 队列:先进先出(FIFO)

一、栈中存放啥?

  1. 8大基本类型

  2. 对象引用

  3. 实例的方法


二、栈运行原理

  • 栈表示Java方法执行的内存模型


  • 每调用一个方法就会为每一个方法生成一个栈帧(Stack Frame),每一个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。


  • 程序正在执行的方法必定在栈的顶部





三、堆栈溢出StackOverflowError

举个例子

public class Test {
    public static void main(String[] args) {
        new Test().a();
    }

    public void a() {
        b();
    }

    public void b() {
        a();
    }
}


最开始,main()方法压入栈中,而后执行a(),a()压入栈中;再调用b(),b()压入栈中;以此往复,a与b方法不断被压入栈中,最终致使栈溢出




十二、堆  

Heap,一个JVM只有一个堆内存(栈是线程级的),堆内存的大小是能够调节的



一、堆中有啥?

实例化的对象

二、堆内存详解


一、Young 年轻代

对象诞生、成长甚至死亡的区

  • Eden Space(伊甸园区):全部的对象都是在此new出来的

  • Survivor Space(幸存区)

    • 幸存0区From Space)(动态的,From和To会互相交换)

    • 幸存1区To Space


Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1


二、Tenured 老年代

三、Perm 元空间

存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域内存!

这个区域常驻内存,用来存放JDK自身携带的Class对象、Interface元数据。

名称演变
  • jdk1.6以前:永久代

  • jdk1.7:永久代慢慢退化,去永久代

  • jdk1.8以后:永久代更名为元空间

注意:元空间在逻辑上存在,在物理上不存在
新生代 + 老年代的内存空间 = JVM分配的总内存
如图所示:


三、什么是OOM?

内存溢出java.lang.OutOfMemoryError

产生缘由
  1. 分配的太少

  2. 用的太多

  3. 用完没释放

四、GC垃圾回收

GC垃圾回收,主要在年轻代和老年代

首先,对象出生再 伊甸园区
  • 假设伊甸园区只能存必定数量的对象,则每当存满时就会触发一次轻GC(Minor GC)


  • 轻GC清理后,有的对象可能还存在引用,就活下来了,活下来的对象就进入幸存区;有的对象没用了,就被GC清理掉了;每次轻GC都会使得伊甸园区为空



  • 若是幸存区伊甸园都满了,则会进入老年代,若是老年代满了,就会触发一次重GC(FullGC)年轻代+老年代的对象都会清理一次,活下的对象就进入老年代


  • 若是新生代老年代都满了,则OOM


Minor GC:伊甸园区满时触发;从年轻代回收内存
Full GC:老年代满时触发;清理整个堆空间,包含年轻代和老年代
Major GC:清理老年代

什么状况永久区会崩?
一个启动类加载了大量的第三方Jar包,Tomcat部署了过多应用,或者大量动态生成的反射类
这些东西不断的被加载,直到内存满,就会出现 OOM

1三、堆内存调优 

一、查看并设置JVM堆内存

查看咱们jvm的 堆内存

public class Test {
    public static void main(String[] args) {
        //返回jvm试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        //返回jvm的初始化内存
        long total = Runtime.getRuntime().totalMemory();
        //默认状况下:分配的总内存为电脑内存的1/4,初始化内存为电脑内存的1/64
        System.out.println("max=" + max / (double) 1024 / 1024 / 1024 + "G");
        System.out.println("total=" + total / (double) 1024 / 1024 / 1024 + "G");
    }
}



默认状况下
  • JVM最大分配内存为电脑内存的1/4

  • JVM初始化内存为电脑内存的1/64

咱们能够手动调堆内存大小


VM options 中能够指定 jvm试图使用的最大内存 jvm初始化内存 大小
  
  
   
   
            
   
   

  
  
   
   
            
   
   
-Xms1024m -Xmx1024m -Xlog:gc*


  • -Xmx用来设置jvm试图使用的最大内存,默认为1/4

  • -Xms用来设置jvm初始化内存,默认为1/64

  • -Xlog:gc*用来打印GC垃圾回收信息




二、怎么排除OOM错误?

1. 尝试扩大堆内存看结果

利用上述方法指定jvm试图使用的最大内存jvm初始化内存大小

2. 利用内存快照工具JProfiler

内存快照工具
  • MAT(Eclipse)

  • JProfiler

做用:
  • 分析Dump内存文件,快速定位内存泄漏

  • 得到堆中的文件

  • 得到大的对象

3. 什么是Dump文件?如何分析?

Dump文件是进程内存镜像,能够把程序的执行状态经过调试器保存到dump文件中

import java.util.ArrayList;

public class Test {
    byte[] array = new byte[1024 * 1024];//1M

    public static void main(String[] args) {
        ArrayList<Test> list = new ArrayList<>();
        int count = 0;
        try {
            while (true) {
                list.add(new Test());
                count++;
            }
        } catch (Exception e) {
            System.out.println("count=" + count);
            e.printStackTrace();
        }
    }
}

运行该程序,报错OOM


接下来咱们设置如下堆内存,并附加生成对应的dump文件的指令


-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError 表示当JVM发生OOM时,自动生成DUMP文件。

再次点击运行,下载了对应的Dump文件



咱们右键该类,点击 Show in Explorer


一直点击上级目录,直到找到.hprof文件,与src同级目录下



咱们双击打开,能够看到每块所占的大小,便于分析问题



点击Thread Dump,里面是全部的线程,点击对应的线程能够看到相应的错误,反馈到具体的行,便于排错



每次打开Dump文件查看完后,建议删除,能够在idea中看到,打开文件后生成了不少内容,占内存,建议删除



附:安装Jprofiler教程

1.idea中安装插件

2.下载客户端 https://www.ej-technologies.com/download/jprofiler/files



3.安装客户端
选择自定义安装,注意:路径不能有中文和空格


这里name和Company任意,License Key你们能够寻找对应版本的注册机得到



后续默认,安装成功便可!!!
4. 安装完成后,重启IDEA,能够看到咱们的内存快照工具

 打开IDEA的设置,找到Tools里面的JProfiler,没有设置位置则设置位置

此时则所有安装完成!


1四、GC垃圾回收 

一、回顾

Garbage Collection:垃圾回收



在12.4中,咱们已经对GC的流程进行了大概的讲解,这里作一些总结
  • JVM在进行GC时,并非对年轻代老年代统一回收;大部分时候,回收都是在年轻代

  • GC分为两种

    • 轻GC(清理年轻代)

    • 重GC(清理年轻代+老年代)

二、GC算法

一、引用计数算法(不多使用)

  • 每一个对象在建立的时候,就给这个对象绑定一个计数器。

  • 每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。

  • 这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操做。



二、复制算法

复制算法主要发生在 年轻代 (  幸存0区  和  幸存1区
  • 当Eden区满的时候,会触发轻GC,每触发一次,活的对象就被转移到幸存区,死的就被GC清理掉了,因此每触发轻GC时,Eden区就会清空;

  • 对象被转移到了幸存区,幸存区又分为From SpaceTo Space,这两块区域是动态交换的,谁是空的谁就是To Space,而后From Space就会把所有对象转移到To Space去;

  • 那若是两块区域都不为空呢?这就用到了复制算法,其中一个区域会将存活的对象转移到令一个区域去,而后将本身区域的内存空间清空,这样该区域为空,又成为了To Space

  • 因此每次触发轻GC后,Eden区清空,同时To区也清空了,全部的对象都在From区

这也就是幸存0区幸存1区总有一块为空的缘由



好处:没有内存的碎片(内存集中在一块)
坏处
  1. 浪费了内存空间(浪费了幸存区一半空间)

  2. 对象存活率较高的场景下(好比老年代那样的环境),须要复制的东西太多,效率会降低。

最佳使用环境:对象存活度较低的时候,也就是 年轻代

三、标记–清除算法

为每一个对象存储一个标记位,记录对象的生存状态
  1. 标记阶段:这个阶段内,为每一个对象更新标记位,检查对象是否死亡;

  2. 清除阶段:该阶段对死亡的对象进行清除,执行 GC 操做。



缺点:两次扫描严重浪费时间,会产生内存碎片
优势:不须要额外的空间

四、标记–整理算法

标记-整理法  是  标记-清除法  的一个改进版。
又叫作  标记-清楚-压缩法
  1. 标记阶段,该算法也将全部对象标记为存活和死亡两种状态;

  2. 不一样的是,在第二个阶段,该算法并无直接对死亡的对象进行清理,而是将全部存活的对象整理一下,放到另外一处空间,而后把剩下的全部对象所有清除。


能够进一步优化,在内存碎片不太多的状况下,就继续标记清除,到达必定量的时候再压缩.


总结

内存(时间复杂度)效率:复制算法 > 标记清除算法 > 标记压缩算法
内存整齐度:复制算法 = 标记压缩法 > 标记清除法
内存利用率:标记压缩法 = 标记清除法 > 复制算法

思考:有没有最优的算法?

没有最优的算法,只有最合适的算法
GC 也称为  分代收集算法
对于 年轻代
  • 对象存活率低

  • 用复制算法

对于 老年代
  • 区域大,对象存活率高

  • 标记清除+标记压缩混合实现



结束!


    

    做者: Baret H

    原文连接:http://i8n.cn/iWLG4r






-关注我


本文分享自微信公众号 - 程序员历小冰(gh_a1d0b50d8f0a)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索