【狂神JAVA笔记整理】JVM相关,会增加内容

JVM初探

面试问题

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

  1. JVM的位置
  2. JVM的体系结构
  3. 类加载器
  4. 双亲委派机制
  5. 沙箱安全机制
  6. Native
  7. PC寄存器
  8. 方法区
  9. 三种JVM
  10. 新生区、老年区
  11. 永久区
  12. 堆内存调优
  13. GC、常用算法
  14. JMM
  15. 总结

1、JVM的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-seiZLAhA-1615606391492)(第一版.assets/image-20210129163223110.png)]
JRE是java运行时环境,包含java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
JDK是java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。
JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具。

2、JVM体系结构

java文件编译成.class文件,再用类加载器加载,放到数据区中运行。数据区中有java栈和本地方法栈,不会有垃圾回收通过程序计数器和本地方法接口连接。另外还存在方法区和堆,要调整的地方基本在这里。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yj1BN2lH-1615606391498)(第一版.assets/image-20210131113845513.png)]
要调整的东西基本在堆和方法区,99%。

本地方法接口:JNI(Java Native Interface)

3、类加载器

作用:加载Class文件。

在这里插入图片描述
注意类是模板,抽象的

1、双亲委派

  • 1、虚拟机自带的加载器
  • 2、启动类(根)加载器【BOOT】
  • 3、扩展类加载器【EXT】
  • 4、应用程序/系统加载器【APP】

注:从4到1进行加载

双亲委派机制(安全):APP→EXT→BOOT【最终执行】

  1. 类加载器收到类加载的请求
  2. 将这个请求向上委托为父类加载器去完成,一直向上委托,直到启动类加载器
  3. 启动类加载器检查是否能够加载当前的这个类,能加载就结束,使用当前的加载器,否则抛出异常,通知子加载器进行加载
  4. 重复步骤 3

但是BOOT根加载器输出是null,这是java调用不到~ C,C++

Java = C++:去掉繁琐的东西(指针、内存管理等)

package com.draco.parents;

/**
 * 双亲委派机制
// APP-->EXC---B00T(最终执行
// B0OT
// EXC
// APP
 */
public class Car {
    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();

        //不同的实例
        System.out.println("car1 hashCode="+car1.hashCode());
        System.out.println("car2 hashCode="+car2.hashCode());
        System.out.println("car3 hashCode="+car3.hashCode());

        //同一个类模版
        Class<? extends Car> aClass1 = car1.getClass();
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();

        System.out.println("aClass1 hashCode="+aClass1.hashCode());
        System.out.println("aClass2 hashCode="+aClass2.hashCode());
        System.out.println("aClass3 hashCode="+aClass3.hashCode());

        //由于类模版都是一个,以下就选择一个进行测试
        ClassLoader classLoader = aClass1.getClassLoader();

        System.out.println(classLoader);  //AppClassLoader

        System.out.println(classLoader.getParent());  //ExtClassLoader  所在位置:\jre\lib\ext

        System.out.println(classLoader.getParent().getParent());  //null 1.不存在  2.java程序获取不到  所在位置:rt.jar

    }
}

运行测试结果:

类是模板,对象是具体的。不同对象有不同的hascode,统一对象只要一个hascode。

2、面试问题

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

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

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

  • 如何打破双亲委派?

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

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

    先递推到最根处,再从最根处找,没有就一步步往回退。

4、沙箱安全机制

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下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
    • 安全提供者
    • 消息摘要
    • 数字签名keytools
    • 加密
    • 鉴别

robot类可以操作键盘和鼠标,可以玩黑客技术,嘿嘿。安全认证

5、Native

在这里插入图片描述
编写一个多线程启动类

public static void main(String[] args) {
    new Thread(() -> {},"Thread1").start();
}

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

正常情况下一般不用native,和硬件打交道的时候采用,c比较多

Java化序劳动打中机,停理系统,肯是即可, 在企业级应用中较为少见!

调用其他接口:Socket.WebService
球球爱心网:→输入(PHP) → NodeJS →Socket→C++→游戏刷爱心

//点进去start方法
public synchronized void start() {

    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0(); // 调用了start0()方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {

        }
    }
}
// 凡是带了native关键字的,就说明Java的作用范围达不到了,会去调用底层C语言的库
private native void start0(); //start0()方法的定义,这个方法会调用底层C

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

在这里插入图片描述

  • JNI的作用:扩展Java的使用,融合不同的编程语言为Java所用,不过最初是想融合C,C++的,因为Java诞生的时候,C,C++横行,想要立足的话就要有能调用C的程序
  • 本地方法栈:具体做法是,在Native Method Stack中登记native方法,在执行引擎执行的时候加载Native Libraies【本地库】。

6、PC寄存器

程序计数器:Program Counter Register

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

7、方法区

在这里插入图片描述
需要会画会解释远离。先定义常量,然后在栈中有引用,栈又去堆中找数值

方法区:Method Area

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

​ 静态变量static、常量final、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。简单的来说就是:方法区static、final、Class、常量池

8、栈

  • 程序 = 数据结构 + 算法 【持续学习】

  • 大多数人程序 = 框架 + 业务逻辑【饭碗】

栈:先进后出,后进先出

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

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

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

1、栈里面存放什么

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

2、栈运行原理

栈帧
在这里插入图片描述
栈满了:StackOverflowError

3、栈堆方法区的交互关系

在这里插入图片描述

9、三种JVM

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

10、堆

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

1、堆里面存放什么

类加载器读取了类文件后,一般会把什么东西放在堆中? 类,方法,常量,变量,保存我们所有引用类型的真实对象

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

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

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

在这里插入图片描述
假设内存满了,OOM,堆内存不够!java.lang.OutOfMemoryError: Java heap space

package com.draco.heapOverflow;
import java.util.Random;

/**
 * 堆溢出
 */
public class HeapOver {
    public static void main(String[] args) {
        String name = "adfjkllkjfdsa";
        while(true){
            name += name + new Random().nextInt(888888888)+new Random().nextInt(999999999);
        }
    }
}

在这里插入图片描述
在JDK8以后,永久存储区改了个名字叫:元空间

2、新生区

  • 新生区:类诞生和成长的地方,甚至死亡;
    • 伊甸园区,所有对象都是在伊甸园区new出来的
    • 幸存者区(0、1)

在这里插入图片描述
真理:经过研究,99%的对象都是临时对象!

3、养老区

同时,很大的文件在养老区生成

新生区没干掉,没杀死的来到了养老区~

很多在幸存一区就没了,所以一般没有oom的情况

4、永久区

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

一个启动类加载了大量的第三方jar包;Tomcat部署了太多的应用;大量动态生成的反射类等 不断的被加载,直到内存满,就会出现OOM。

  • jdk1.6 之前:永久代,常量池是在方法区中;
  • jdk1.7 :永久代,但是慢慢退化了,去永久代,常量池在堆中
  • jdk1.8 之后:无永久代,常量池在元空间

在这里插入图片描述
持久带也叫元空间

但是,元空间:逻辑上存在,物理上不存在

package com.draco.heapOverflow;

/**
 * 元空间逻辑上存在,物理上不存在
 */
public class SanQu {
    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
    }
}

运行结果:
在这里插入图片描述
当修改了VM选项后:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
输出结果:
在这里插入图片描述

让我们来算一笔账,

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

加在一起:1,005,056k,除以1024后 = 981.5MB,等于jvm试图分配的最大内存,所以说元空间逻辑上存在,物理上不存在。

5、出现OOM

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

    -Xms1024m -Xmx1024m -XX:+PrintGCDetails

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

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

幸存区1区和幸存区0区,每次都会互换位置。

MAT,Jprofiler作用:

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

在这里插入图片描述
先轻GC,后重GC,知道重GC也无法处理,都满了。

运行出现堆溢出:

package com.draco.heapOverflow;

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();
        }
    }
}

设置VM options -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError后,再次运行,控制台输出:
在这里插入图片描述
找到HPROF快照
在这里插入图片描述
双击打开

在这里插入图片描述
修改代码,把try catch中的Exception改成Error后运行:

在这里插入图片描述

6、VM options参数

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

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

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

-XX:+HeapDumpOnOutOfMemoryError 生成oomDump文件

-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

11、GC垃圾回收

1、GC的作用区域

在这里插入图片描述
JVM在进行GC时,并不是对这三个区域统一回收,大部分时候,回收都是新生代

  • 新生代
  • 幸存区(form to)【会交换的,不是一成不变的】
  • 老年区

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

2、GC相关题目

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

3、GC算法

引用计数法

在这里插入图片描述
计数为0的时候可以淘汰,这个方法不够高效

哪个对象的引用数为0,就会回收哪个对象

复制算法

在这里插入图片描述
谁空谁是to,跟沙漏有点像,上下会变化。必须得保证有一个空的,不空的就复制到另外一个区域。

一般新生代(伊甸园区、幸存区)会使用复制算法,生成新的to区

在这里插入图片描述

本质就是交换了空间,eden先去的from区,然后在将需要保存长期使用的放在养老区

  • 好处:没有内存的碎片
  • 坏处:浪费了内存空间,多了一半空间永远是空to。假设对象100%存活(极端情况)

复制算法最佳使用场景:对象存活度较低的时候,也就是新生区

标记清除算法

在这里插入图片描述

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

最优的方案,永远只是时间或者空间的权衡,而现在因为不缺空间,所以都是优先复制算法。

标记压缩算法

对于标记清除的再压缩
在这里插入图片描述
但是又多了一个移动成本,压缩相当于整理

标记清除压缩算法

先标记清除几次(比如5次),然后再压缩

总结

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

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

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

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

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

年轻代:

  • 存活率低
  • 复制算法

老年代:

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

jvm调优就是不断调整这个问题

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

JMM

  1. 什么是JMM?

    JMM:Java Memory Model的缩写。java内存模型

  2. 它干嘛的?

    作用:缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Uzdui72-1615606391550)(第一版.assets/image-20210202211809212.png)]

这样会形成一个工作台

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

  1. 它该如何学习?

    JMM:抽象的概念,理论

    voliate等等

JMM对这八种指令的使用,制定了如下规则:

    • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
    • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
    • 不允许一个线程将没有assign的数据从工作内存同步回主内存
    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
    • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
    • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
    • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
    • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

    JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

jmm面试题答案

最后:拥抱开源时代,享受技术给我们带来的新体验

面试:
3/10== pass,面经= 10,分析这10个?触类旁通:百度面试题?
通过大量的面试总结,得出一套解题思路? 3-4.|

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值