JVM

常见面试题

  • 谈谈对JVM的理解?java8虚拟机和之前的变化更新?
  • 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析
  • JVM常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?
  • JVM中,类加载器的认识?

JVM位置

alt

JVM的体系结构

alt

alt

java栈、本地方法栈、程序计数器不可能存在垃圾,所以JVM调优99%是对堆和方法区(方法区属于特殊的堆)的优化

类加载器

作用:加载class文件 new xx();

负责加载class文件(classs文件在文件开头有特定的文件标识),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构;ClassLoader只负责加载class文件的加载,至于它是否可以运行,则由Execution Engine决定。

1、BootStrapLoader(引导类加载器)

2、Extension ClassLoader(扩展类加载器)

3、Application ClassLoader(应用程序类加载器)

4、虚拟机自带的加载器

Car c1 = new Car();
Car c2 = new Car();
Car c3 = new Car();
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
Class<? extends Car> c1Class = c1.getClass();
System.out.println(c1Class);
System.out.println(c1Class.hashCode());
System.out.println(c1Class.getClassLoader()); //AppClassLoader      
System.out.println(c1Class.getClassLoader().getParent()); //ExtClassLoader   jre/lib/ext/*.jar
System.out.println(c1Class.getClassLoader().getParent().getParent()); //null,  Java程序获取不到,用C写的  rt.jar

String s = "kaka";
System.out.println(s.getClass().getClassLoader());

//===========================================460141958
1163157884
1956725890
class com.kaka.jvm.pojo.Car
356573597
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@677327b6
null
460141958
1163157884
1956725890
class com.kaka.jvm.pojo.Car
356573597
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@677327b6 
null   
null  //根加载器

双亲委派机制

package java.lang;
public class String {
    /**
     * 双亲委派机制:安全考虑
     * APP===》EXT ===》 BOOT(最终执行根加载器中的)
     * 只有当EXT和BOOT中没有这个类的时候,才会使用APP
     *
     *
     * 1、类加载器收到类加载的请求
     * 2、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
     * 3、启动类加载器检查是否能加载当前这个类,能加载就结束,否则抛出异常,通知子类加载器进行加载
     * 4、直到ApplicationClassLoad,如果也不能加载,抛异常 ClassNotFoundException
     * @return
     */
    public String toString(){
        return "kaka";
    }
    public static void main(String[] args) {
        String s = new String();
        s.toString();
    }
}
//=====================================
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
       


    

沙箱安全机制

组成沙箱的基本组件:

  • 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类
  • 类加载器(ClassLoader):类加载器在三个方面对沙箱起作用
    • 防止恶意代码干涉善意代码;
    • 守护了被信任的类库边界;
    • 将代码归入保护域,确定代码可以进行哪些操作

虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被加载的类将有一个名字,这个命名空间是由java虚拟机为每一个类加载器维护的,他们之间也是不可见的。

类加载器采用的机制是双亲委派机制:

  • 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
  • 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内部类,破坏代码就自然无法生效。
  • 存取控制器(AccessController):可以控制核心API对操作系统的存取权限,而控制的策略设定可以由用户指定;
  • 安全管理器(SecurityManager):是核心API和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高;
  • 安全软件包(SecurityPackage):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
    • 安全提供者
    • 消息摘要
    • 数字签名 keytools
    • 加密
    • 鉴别

native

new Thread(()->{
    System.out.println("猎杀时刻开始了");
},"my thread").start();

//java.lang.Thread#start0
private native void start0();

凡是带了native关键字的,说明java的作用范围达不到了,会去调底层c语言的库!

会进入本地方法栈 ----》调用本地方法接口(JNI java native interface

JNI作用:扩展java的使用,融合不同的编程语言为java所用,现在如果想要使用其他语言的功能,可以使用socket、webservice、http

java专门为native开辟了一块标记区域-本地方法栈(native method stack),负责登记native方法,在执行引擎(Execution Engine)最终执行的时候,通过JNI加载本地方法库中的方法

PC寄存器

程序计数器:program counter register

每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是即将执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

方法区

method area:

  • 方法区是被所有线程共享,所有字段和方法字节码以及一些特殊的方法,如构造函数、接口代码,简单的说所有定义的方法的信息都保存在该区域,此区域属于共享区间
  • ==静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池也在方法区中,但是实例变量存在于堆内存中,与方法区无关==
  • static 、final、Class类模板、常量池

栈内存,主观程序的运行,生命周期和线程同步,线程结束,则栈内存释放,所以对于栈来说,不存在垃圾回收问题

内容:

  • 八大基本类型
  • 对象引用
  • 实例方法

运行原理:栈帧(一个方法可以不认为就是一个栈帧)

alt

栈如果满了,就会抛出错误:StackOverflowError

三种JVM

① Sun公司的HotSpot ② BEA公司的JRockit ③ IBM公司的J9 JVM
	在JDK1.7及其以前我们所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收购,
		jdk1.8将采用Sun公司的HotSpot和BEA公司的JRockit两个JVM中精华形成jdk1.8的JVM

堆 heap

一个JVM只有一个堆内存,堆内存的大小是可以不调节的。

类加载器读取了类文件后,会将 类、方法、常量、变量等所有引用类型的真实对象保存到堆内存中!

堆内存分类:

  • 新生区(伊甸园区)young/new youngGC
  • 养老区old FullGC
  • 永久存储区(JDK8以后,改成元空间,归并到方法区)

堆内存满了,也会抛出错误OOM(OutOfMemoryError)

新生区

  • 类:诞生和成长的地方,甚至死亡
  • 分为伊甸园区(Eden space)、幸存0区、幸存1区,比例默认是8:1:1

老年区

youngGC没有清理掉的

永久区

  • jdk6之前:永久代,常量池在方法区中
  • jdk7:永久代,慢慢退化,提出去永久代,常量池在堆中
  • jdk8:无永久代,常量池在元空间

这个区域是常驻内存的,用来存放jdk自带的Class对象、Interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭JVM虚拟机就会释放这个区域的内存。

崩溃情况

  • 一个启动类加载了大量的三方jar包
  • Tomcat部署了太多的应用
  • 大量动态生成的反射类,不断的被加载,知道内存满就会出现oom

alt

//
long maxMemory = Runtime.getRuntime().maxMemory();
//返回jvm的初始化总内存
long totalMemory = Runtime.getRuntime().totalMemory();

System.out.println("max:"+maxMemory/1024.0/1024);
System.out.println("total:"+totalMemory/1024.0/1024);
//=====================
max:1792.0
total:121.0
16:1
     
//调整JVM参数   
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails;
max:981.5
total:981.5
Heap
 PSYoungGen      total 305664K, used 26214K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 10% used [0x00000000eab00000,0x00000000ec499be8,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3290K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K

//PSYoungGen + ParOldGen = 305664K + 699392K = 981.5m = total;
//Metaspace : 不在堆中

在一个项目中,突然出现oom故障,name如何排查?

  • Debug,一行行分析代码(线上问题不好排查)
  • 内存快照工具:MAT 、Jprofiler

MAT 、Jprofiler作用

  • 分析Dump内存文件,快速定位内存泄漏;
  • 获取堆中的数据
  • 获得大的对象

alt

下载安装打开客户端:

alt

打开idea

alt

配置jvm参数:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid124.hprof ...
Heap dump file created [7719145 bytes in 0.023 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.kaka.jvm.Demo03.<init>(Demo03.java:7)
	at com.kaka.jvm.Demo03.main(Demo03.java:15)

用Jprofiler打开该文件:java_pid124.hprof

alt

alt

alt

public class Demo03 {

    byte[] arr = new byte[1*1024*1024];  //1m

    public static void main(String[] args) {
        ArrayList<Demo03> list = new ArrayList<>();
        int count = 0;
        try{
            while(true){
                list.add(new Demo03());
                count++;
            }
        }catch (Error e){
            System.out.println("失败次数:"+count);
            e.printStackTrace();
        }
    }
}
//=================================
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid11272.hprof ...
Heap dump file created [7722385 bytes in 0.017 secs]
失败次数:6
java.lang.OutOfMemoryError: Java heap space
	at com.kaka.jvm.Demo03.<init>(Demo03.java:7)
	at com.kaka.jvm.Demo03.main(Demo03.java:15)

GC

JVM在进行GC时,并不是对三个区域统一回收,大部分时候都是回收新生代

  • 新生代
  • 幸存区(from、to)
  • 老年代

GC两种类:轻GC、重GC,也可以认为是youngGC和fullGC

面试题:

  • JVM的内存模型和分区,详细到每个分区放什么?
  • 堆里面的分区有哪些?Eden、from、to、old,说说各区特点?
  • GC的算法有哪些?标记清除法、标记压缩、复制算法、引用计数器,怎么用的?
  • 轻GC和重GC分别在什么时候发生?

引用计数(Java没有采用)

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,但无法解决对象相互循环引用的问题,如下图所示。

①当出现左边绿色的情况时,假设外部对A的引用消除,此时因为A引用计数从1减为0,A将被清除。从而对B的引用也消除,B的计数减为0,GC将正确回收对象{A, B}

②然而若出现右边橙色状况,假设外部对E的引用消除,外部对于对象集{C,D,E}不再有引用,但他们之间出现循环引用现象,计数始终保持为1,导致{C,D,E}无法被回收。 alt

复制算法(jvm新生代回收)

  • 每次GC都会将Eden活的对象移到幸存区中,一旦Eden被GC,就会变为空的!

  • 当一个对象经历了15次GC,都还没有被清理,就会到老年代。这个次数由下面这个参数控制

  • 适用场景:对象存活度较低的时候

    -XX:MaxTenuringThreshold=15
    

alt

MaxTenuringThreshold设置垃圾的最大年龄. 默认为15 . 最大也是15,在jdk8中. 范围为 0到15.

标记清除法(jvm老年代回收)

alt

标记压缩(jvm老年代回收)

优化标记清除算法,防止内存碎片产生

alt

总结

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

内存整齐度:复制算法=标记压缩算法>标记清除算法

内存利用率:标记压缩算法=标记清除算法>复制算法

疑问:没有最优算法么?

答:没有最好的算法,只有最合适的算法------》GC:分代回收算法

新生代:

  • 存活率低
  • 复制算法

老年代:

  • 区域大:存活率
  • 标记清除(内存碎片不是太多)+标记压缩混合实现

JMM

Java Memory Model(JMM)java内存模型,区别与java内存结构。JMM定义了一套在多线程读写共享数据(变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。

参考:https://www.jianshu.com/p/51768fcdf6cb

synchronize和volatile对比

1、volatile是线程同步的轻量级实现,性能比synchronize好 2、volatile只能修饰变量,而synchronize可以修饰方法、代码块和变量 3、volatile多线程时不会发生阻塞,而synchronize会阻塞线程 4、volatile可以保证可见性和有序性(禁止指令重排),无法保证原子性,而synchronize都可以保证 总结:volatile就是保证变量对其他线程的可见性和防止指令重排序 而synchronize解决多个线程访问资源的同步性