JVM探究(基于狂神学习)
请你谈谈你对JVM的理解?java8虚拟机和之前的变化更新?
什么事OOM,什么事栈溢出StackOverFlowError? 怎么分析?
JVM的常用调优参数有哪些?
内存快照如何抓走,怎么分析Dump文件?知道吗?
谈谈JVM中,类加载器你的认识?
1.JVM的位置
2.JVM的体系结构
3.类加载器ClassLoader
作用:加载Class文件
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
双亲委派机制
双亲委派机制:安全
APP–>EXC—B0OT(最终执行)
// B0OT
// EXC
// APP
1、类加载器收到类加载的请求 Application。
1、将这个请求向上委托给父类加载器去完成,一 直向上委托,直到启动类加载器 Boot。
3、启动加载器检查是否能够加载当前这个类,能加载就结束, 使用当前的加载器,否则, 抛出异常,通知子加载器进行加载。
4、重复步骤3。
5、Class Not Found异常就是这么来的。
6、Null:Java调用不到。Java早期的名字:C+± - Java = C++:去掉繁琐的东西,指针,内存管理。
Java语言保留了C的接口,这些方法就是用native(本地)修饰的,java通过native方法调用操作系统的方法。
4.沙箱安全机制
Java安全模型的核心就是java沙箱(sanddiox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的java程序运行都可以指定沙箱,可以定制安全策略。
在java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型
组成沙箱的基本组件
字节码校检器(bytecode verifier):确保java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校检,比如核心类
类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码
- 它守护了被信任的类库边界
- 它将代码归入保护域,确定了代码可以进行哪些操作。
- 虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,他们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用。
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
- 存取控制器(access controller)
存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。 - 安全管理器(security manager) : 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
- 安全软件包(security package) :
java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括: - 安全提供者 消息摘要 数字签名 加密 鉴别
Native
- 凡是带了native关键字的,说明java的作用范围打不打了,回去调用底层c语言的操作。
- 会进入本地方法栈调用本地方法本地接口, JNI
- 他在内存区域中专门开辟了一块标记区域:Native Method stack , 登记native方法
- 在最终指向的时候,加载本地方法库中的方法通过JNi。
- java程序驱动打印机,管理系统,掌握即可。在企业中应用较为少见。
5.PC寄存器
程序计数器:Programma Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
6.方法区
方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息多保存在该区域,在区域属于共享区间。
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
static, final , Class, 常量池
三种JVM
- Sun公司HotSpot Java HotSpot(TM)64-Bit Server VM (build 25.181-b13,
mixed mode) - BEA JRockit
- IBM J9 VM
我们现在学的是HotSpot
7.栈
-
栈:占内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收回顾
一旦线程结束,栈就是Over!
-
栈:8大基础类型+对象引用+实例方法
栈运行原理:栈桢
栈满了:StackOverflowError
8.堆
Heap,一个JVM只有一个堆内存,堆内存的大小可以调节的
类加载器读取了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量,保存我们所有引用类型的真实对象。
堆内存中还要细分三个区域:
- 新生区(伊甸园区)Young/New
- 养老区 old
- 永久区 Perm
GC:Garbage recycling
轻GC:轻量级垃圾回收,主要是在新生区
重GC(Full GC):重量级垃圾回收,主要是养老区,重GC就说明内存都要爆了
新生区、老年区、永久区
新生区:
- 类:诞生和成长的地方,甚至死亡
- 伊甸园,所有的对象都是在伊甸园区new出来的
- 幸存者区(0,1)
永久区
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息
这个区域不存在垃圾回收! 关闭VM虚拟就会释放这个区域的内存
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM;
JDk1.6之前:永久代,常量池是在方法区
JDK1.7 :永久代,但是慢慢的退化了,去永久代,常量池在堆中
JDK1.8之后:无永久代,常量池在元空间
伊甸园满了就触发轻GC,经过轻GC存活下来的就到了幸存者区,幸存者区满之后意味着新生区也满了,则触发重GC,经过重GC之后存活下来的就到了养老区。 真理:经过研究,70%-99%的对象都是临时对象!|
PermGen(永久区)和Metaspace(元空间)
的关键区别在于:PermGen是Java堆的一部分(通过-Xmx选项配置的最大大小),而Metaspace不是Heap(堆)的一部分。相反,Metaspace是本机内存(进程内存)的一部分,它只受主机操作系统的限制。
9.堆内存调优
jvm参数:
Xms1024m Xmx1024m -XX:+PrintGCDetails
默认情况下:
分配的总内存是电脑内存的1/4,而初始化的内存1/64
当堆内存过小时,容易爆OOM(堆内存溢出)错误 ,可以先扩大堆内存,进行调试。
MAT(eclipse), Jprofiler(idea)作用:
- 分析Dump内存文件,快速定位内存泄露
- 获得堆中的数据
- 获得大的对象
10.GC
JVM在进行GC回收时,并不是对三个区域一起回收,大部分的时候回收的都是新生代。
新生代、幸存区(from、to)、老年区
堆里面的分区有哪些?
Eden,from,to,老年区
GC的算法有哪些?
标记清除法,标记整理,复制算法,引用计数器
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
11.JMM
JMM:
Java内存模型(Java Memory Model, JMM)
就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步
内存交互操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态。
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中。
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令。
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中。
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
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操作之前,必须把此变量同步回主内存。