目录

一、JVM的位置

二、JVM的体系结构

三、类加载器

1.类加载器举例

2. JVM中提供了三层的ClassLoader

3. 双亲委派机制(重要)

3.1 工作原理

3.2.优点

 四、沙箱安全机制(了解)

1、什么是沙箱

2、组成沙箱的基本组件

五、Native (重要)

八、PC寄存器

九、方法区

十、堆

1、堆里面存放什么

2、新生区

3、养老区

4、永久区

十一、出现OOM(栈溢出)

十二、GC的算法

1. 引用计数法

2. 复制算法

3. 标记清除

3.1 标记清除

3.2 标记压缩

3.3 标记清除压缩

4. 算法总结

十二、JMM

 

JVM的面试题,看你能答出几个?

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

一、JVM的位置

JVM是运行在操作系统之上的,JVM可以理解为一个虚拟软件,JVM是用c语言写的,JRE(java运行环境)包含了JVM,操作系统的底层是硬件体系。

 

二、JVM的体系结构

.java文件编译成.class文件,通过类加载器将文件加载到JVM中,JVM也要给类加载器返回一些内容。

运行时数据区与类加载器、执行引擎、本地方法区交互。

运行时数据区:包括 方法区、Java栈、本地方法栈、堆、程序计数器。

JVM的底层是操作系统,因此JVM与操作系统也相连,具体如下:

  •  因为有本地栈,因此操作系统的本地方法接口与硬件本地方法库相连接 ,与运行时数据区交互。
  • 操作系统层还有执行引擎。

栈不需要垃圾回收机制,因为用完一个数据就出栈。程序计数器也没有垃圾。

方法区是特殊的堆,JVM调优99%就是在堆内进行的。

详图

 

三、类加载器

第三方插件一般在加载引擎上。

作用:加载.class文件

1.类加载器举例

若加载一个Car.class,则Car.class通过类加载器找到Car类,进行实例化。

这些实例化对象的名字在栈中(car1、car2、car3),而实例化的数据引用在堆内引用 (通过栈中存储的堆内地址寻找)

package jvm;

public class Car {
    public int age;
    public static void main(String[] args) {
        // 类是模板,对象是具体的
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();

        car1.age = 1;
        car2.age = 2;
        car3.age = 3;
        System.out.println(car1);  //jvm.Car@15db9742
        System.out.println(car2);	//jvm.Car@6d06d69c
        System.out.println(car1.hashCode()); // 366712642
        System.out.println(car2.hashCode()); // 1829164700
        System.out.println(car3.hashCode()); // 2018699554

        //?是一个未知类型,是一个通配符泛型,这个类型是继承Car即可
        Class<? extends Car> aClass1 = car1.getClass();//getClass(),返回Class类型的对象
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();
        
        System.out.println(aClass1);	//class jvm.Car
        System.out.println(aClass1.hashCode()); // 705927765
        System.out.println(aClass2.hashCode()); // 705927765
        System.out.println(aClass3.hashCode()); // 705927765

    }
}

运行结果:

 

2. JVM中提供了三层的ClassLoader

  • Bootstrap classLoader(根加载器):主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。

  • ExtClassLoade(扩展类加载器:主要负责加载jre/lib/ext目录下的一些扩展的jar。

  • AppClassLoader(应用程序加载器):主要负责加载应用程序的主函数类。

 

3. 双亲委派机制(重要)

package java.lang;

public class String {
    // 双亲委派机制:保证安全
    // 1.APP --> EXT --> BOOT(最终执行)
    
    public String toString() {
        return "Hello";
    }

    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.getClass().getClassLoader());
        s.toString();
        /*
        错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
            public static void main(String[] args)
        否则 JavaFX 应用程序类必须扩展javafx.application.Application
         */
    }
    //明明main方法存在,却说找不到,原因在于双亲委派机制。调用时,加载的是JRE中的rt.jar包下的java.lang包中的String类
   
    /*
         双亲委派机制
    1.类加载器收到类加载的请求
    2.将这个请求向上委托给父类加载器去完成,一直向上委托,知道启动类加载器
    3.启动加载器检查是否能够加载当前这个类,能够加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
    4.重复步骤 3
   
    null : java调用不到~C、C++
   
     */
}

明明main方法存在,却说找不到,原因在于双亲委派机制。调用时,加载的是JRE中的java.lang包下的String类。

 

3.1 工作原理

双亲委派机制的工作原理

(1)如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行;

(2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器;

(3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制;

(4)父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常;

即从应用程序加载器一层一层向上询问,是否已经加载过,若未加载,再从顶层向下加载。即根加载器内找此类,若已加载则结束,若无此类,再在扩展类加载器中找,若存在则结束。若不存在,则在应用程序加载器中找,若存在则结束,若不存在返回null。

java是在c++的基础上去掉了指针内存管理

native是调用本地的方法库,此时java已经处理不了了。

 

3.2.优点

设计双亲委派机制的好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被根加载器加载过了(为什么?

因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader,即java原先的String类),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。(防止人为的篡改)

面试问题:

  • 为什么需要双亲委派机制?(也就是双亲委派的优点)

    ①双亲委派机制使得类加载出现层级,父类加载器加载过的类,子类加载器不会重复加载,可以防止类重复加载

    ②使得类的加载出现优先级,防止了核心API被篡改,提升了安全,所以越基础的类就会越上层进行加载,反而一般自己的写的类,就会在应用程序加载器(Application)直接加载。

  • 如何打破双亲委派?

    ①自定义类加载器,重写loadClass方法

    ②使用线程上下文类加载器

 

 四、沙箱安全机制(了解)

1、什么是沙箱

​ 沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制远程代码对系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络等。不同级别的沙箱对这些资源访问的限制也可以不一样。

​ 所有的Java程序运行都可以指定沙箱,可以定制安全策略。

 

2、组成沙箱的基本组件

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

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

  类装载器采用的机制是双亲委派模式。

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

 

五、Native (重要)

package jvm;
public class Demo {
    public static void main(String[] args) {
    	/* () -> {},其中 () 用来描述参数列表,
    	 * {} 用来描述方法体,-> 为 lambda运算符 ,读作(goes to)。
		
			thread后面的括号是构造方法的括号,构造方法的参数是和runnable接口绑定的,
    	         所以lambda表达式符合该函数类型的接口
    	 */
        new Thread(()->{
        	
        }).start();
    }

    // native:凡是带了native 关键字的,说明java的作用范围达不到了,回去调用底层C语言的库
    // 会进入本地方法栈
    // 调用本地方法接口 JNI
    // JNI作用:扩展Java的使用,融合不同的编程语言为Java所用! 最初:C、C++
    // Java诞生的时候,C、C++ 横行,想要立足,必须要有调用C、C++ 的程序
    // 它在内存区域中专门开辟了一块标记区域:Native Method Stack,登记 native 方法
    // 在最终执行的时候,通过JNI 加载本地方法库中的方法。

    // Java程序驱动打印机,管理系统,掌握即可,在企业级应用中较为少见!

    private native void start0();

    // 调用其他接口:Socket、WebService、HTTP
}

线程的源码: 

 native:凡是带了native 关键字的,说明java的作用范围达不到了,回去调用底层C语言的库,会进入本地方法栈,调用本地方法接口 JNI

本地方法接口JNI的作用:扩展Java的使用,融合不同的编程语言为Java所用! 最初:C、C++。

Java诞生的时候,C、C++ 横行,想要立足,必须要有调用C、C++ 的程序。

Java在内存区域中专门开辟了一块标记区域:本地方法栈Native Method Stack,登记 native 方法。

在最终执行的时候,通过JNI 加载本地方法库中的方法。

Java程序驱动打印机,管理系统,掌握即可,在企业级应用中较为少见!

小结:

Java在内存区域中专门开辟了一块标记区域——本地方法栈,用来登记native方法,凡是带了native关键字的,会进入到本地方法栈中,调用本地方法接口(JNI),在最终执行的时候,加载本地方法库中的方法通过JNI。

它的具体做法是在本地方法栈中登记 native 方法,在执行引擎执行的时候加载本地方法库。 

 

八、PC寄存器

程序计数器:Program Counter Register

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

因为程序计数器的存在,线程从标号1排到8顺序才不会乱.

 

九、方法区

可参考另一篇博文,帮助理解。https://blog.csdn.net/qq_40243295/article/details/111468616 中的1.3对象内存图

Method Area 方法区

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

静态变量、常量、类信息(构造方法、接口定义)、运行时常量池存在方法区中,但是实例变量存在堆内存(将地址传给方法区)中,和方法区无关。

static, final, Class,常量池修饰的都在方法区。

 方法区是所有线程共享的内存,在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制,在java8中移除了永久代的内容,方法区由元空间(Meta 

Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量

都转移到了Java堆中 。

三种JVM:

  • Sun公司 HotSpot Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
  • BEA JRockit
  • IBM J9VM

下面的学习内容都是针对于Sun公司 HotSpot 的JVM的。

 

十、堆

栈:先进后出,后进先出

队列:先进先出(FIFO:first input first output)

堆:堆内存,主管程序的运行,生命周期和线程同步;

线程结束,栈内存也就释放了,对于栈来说不存在垃圾回收问题,一旦线程结束,栈就Over了

1、堆里面存放什么

堆:8大基本类型 + 对象的引用 + 实例的方法。

堆内存中还要细分为三个区域:

  • 新生区(伊甸园区)young/new
  • 养老区 old
  • 永久区 perm

堆包括:新生区(伊甸园区、幸存区)、养老区、永久区

2、新生区

新生区:类诞生和成长的地方,甚至死亡;

新生区包括:伊甸园区和幸存区

新生区:

  • 伊甸园区:所有对象都是在伊甸园区new出来的
  • 幸存区(0、1/from、to)

伊甸园中经过轻量级之后还活着,移至幸存区(0、1),若伊甸园区和幸存区都满了,则进行一次重量级,如果此线程还活着则放入养老区。

GC垃圾回收,主要在伊甸园区养老区。

假设内存满了,OOM,堆内存不够!java.lang.OutOfMemoryError: Java heap space 

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

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

GC两种类型:轻GC(普通的GC),重GC(全局GC)

GC题目:

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

 

3、养老区

从新生区存活下的,即未死亡的的来到了养老区。

当新生代经历15次轻GC后还存在引用的,则被转移到养老区。

 

4、永久区

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

出现OOM的原因:

  • 启动类加载了大量的第三方jar包;
  • Tomcat部署了太多的应用;
  • 大量动态生成的反射类等 不断的被加载;

元空间:这个区域常驻内存的,用来存放jdk自身携带的class对象,interface元数据。

常量池有字符串常量池运行时常量池

  • jdk1.6之前:永久代,运行时常量池和字符串常量池在方法区。
  • jdk1.7 :永久代,但是慢慢退化,去永久代,字符串常量池在堆中。
  • jdk1.8之后:无永久代,运行时常量池在元空间,字符串常量池在堆中。
package jvm;

/**
 * 元空间逻辑上存在,物理上不存在
 */
public class Demo1 {
    public static void main(String[] args) {
        // 返回jvm试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        // 返回jvm的初始化内存
        long total = Runtime.getRuntime().totalMemory();

        System.out.println("max="+max+"字节\t"+(max/(1024*1024))+"MB");
        System.out.println("total="+total+"字节\t"+(total/(1024*1024))+"MB");

        //默认情况下,试图分配的最大内存是电脑内存的1/4,而初始化的内存是1/64
        // -Xms1024m -Xmx1024m -XX:+PrintGCDetails
    }
}

默认情况下,试图分配的最大内存是电脑内存的1/4,而初始化的内存是1/64。 

                         

修改堆内存分配过程:

输入: -Xms1024m -Xmx1024m -XX:+PrintGCDetails

 

将内存设置改变后,运行结果: 

新生区:305664k;养老区:699392k

内存相加:305664+699392=1,005,056k,换算单位:1,005,056/1024 = 981.5MB,大小等于JVM分配的最大内存,所以说元空间逻辑上存在,物理上不存在

VM options参数

-Xms 设置初始化内存分配大小,默认1/64

-Xmx 设置最大分配内存,默认1/4

-XX:+PrintGCDetails 打印GC垃圾回收信息

-XX:+HeapDumpOnOutOfMemoryError 生成oomDump文件

-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

 

十一、出现OOM(栈溢出)

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

    -Xms1024m -Xmx1024m -XX:+PrintGCDetails

  2. 若不行,分析内存,看一下是哪个地方出现了问题(专业工具)

    • 能够看到代码第几行出错:内存快照分析工具,MAT(eclipse),Jprofiler
    • Dubug,一行行分析代码!(不现实)

MAT,Jprofiler作用:

  • 分析Dump内存文件,快速定位内存泄漏
  • 获得堆中的数据
  • 获得大的对象
  • ……
import java.util.ArrayList;

public class JprofilerTest {
    byte[] array = new byte[1*1024*1024];

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

在idea中设置VM options:右键——>   

设置VM options -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError后,再次运行,控制台输出:

出现堆溢出: 

 

十二、GC的算法

1. 引用计数法

给对象的引用进行计数(统计),每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能在被使用的,垃圾收集器将回收该对象使用的内存。

客观的说,引用计数算法的实现简单,判定的效率很高,在大部分的情况下是一个不错的算法。

默认标记次数 = 15次,当标记 = 0,gc直接回收
对象被引用的话,就会 +1,然后放到 from或者to区,如果还是被频繁使用,那么会放入老年代中。

但引用计数法java中现在已经很少能用到。

 

2. 复制算法

为解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块(to)。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:没有内存的碎片。“复制”使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

它的主要缺点有两个:
        (1)效率问题:在对象存活率较高时,复制操作次数多,效率降低;
        (2)空间问题:浪费了内存空间,多了一半空间永远是空to,假设对象100%存活(极端情况);需要额外空间做分配担保(老年代)

复制算法最佳使用场景:对象存活度较低的时候(即在新生区时使用复制算法)。

from和to区是不停交换的,谁空谁是to。每次将一个幸存区的对象复制到另外一个幸存区,将一个幸存区腾空,命名为to。

 

3. 标记清除

3.1 标记清除

标记清除

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

 

3.2 标记压缩

对标记清除方法的再优化,对于标记清除的再压缩

标记压缩

缺点:较标记清除,又多了一个移动成本

 

3.3 标记清除压缩

先标记清除几次,然后再压缩。

 

4. 算法总结

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

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

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

思考:难道没有最优算法吗?

答案:没有,没有最好的算法,只有最合适的——>GC:分代收集算法

年轻代:

  • 存活率低
  • 复制算法

老年代:

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

还是要多看书《深入理解JVM》,花时间去深究,多看面试题。

 

十二、JMM

学习方法:

1.什么是JMM?(查百度百科)

JMM:Java Memory Model的缩写

2.它干嘛的?(博客)

作用:缓存一致性协议,用于定义数据读写的规则。

JMM定义了线程工作内存和主内存之间的抽象关系,线程之间的共享内存存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

解决共享对象可见性这个问题:voliate

3.它该如何学习?(学jmm的本质为了解决什么问题,理论落地需要什么?)

JMM:抽象的概念,理论

voliate等等(理论落地)

看面试题(总结思路)

本文地址:https://blog.csdn.net/qq_40243295/article/details/113953220