快速学习JVM
JVM体系结构
类加载器
- 作用:加载Class文件
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序(系统类)加载器
双亲委派机制:安全
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动类加载器检查是否能够加载到当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子类加载器进行加载
4.重复步骤3
Class Not Found~
null: java调不到~ c,c++
沙箱安全机制
Java安全机制的核心就是Java沙箱(sandbox)
沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,系统资源包括CPU,内存,文件系统,网络
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,远程代码则被看作是不受信任的,对于授信的本地代码,可以访问一切本地资源
当前最新的安全机制实现,则引入了域(domain)的概念。虚拟机会把所有代码加载到不同的系统域和作用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限。
组成沙箱的基本组件:
1.字节码校验器:保证Java类文件遵循Java语言规范。并不是所有的类文件都会经过字节码校验,如核心类,java.*
2.类加载器:
- 防止恶意代码去干涉善意的代码://双亲委派机制
- 守护了被信任的类库边界
- 将代码归入了保护域,确定了代码可以进行哪些操作
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被加载的类将由一个名字,这个命名空间是由Java虚拟机为每一个类加载器维护的,他们互相之间是不可见的
类加载器采用的是双亲委派模式
-
从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载而无法使用
-
由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类
-
存取控制器
-
安全管理器
-
安全软件包
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
Native
native:
- 凡是带了native的关键字的,说明Java的作用范围达不到了,会去调用底层c语言的库
- 会进入本地方法栈
- 调用本地方法本地接口JNI
- JNI作用:扩展Java的使用,融合不同的编程语言为Java所用
- 在内存区域中专门开辟了一块标记区域:Native Method Stack ,登记native方法
- 在最终执行的时候,加载本地方法库中的方法通过JNI
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码,(用来存储指向下一条指令的地址,也就是即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间
方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间
静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
栈
程序 = 数据结构+算法
栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题
- 方法索引
- 输入输出参数
- 本地变量
- Class File:引用
- 父帧
- 子帧
栈+堆+方法区
三种JVM
- Sun公司 HotSpot
Java HotSpot(TM) 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing)
- BEA
JRockit
- IBM
J9VM
堆
Heap 一个JVM只有一个堆内存,堆内存的大小是可以调节的
类加载器读取了类文件后,一般会把什么什么东西放到堆中,类,方法,常量,变量~,保存我们所有引用类型的真实对象
堆内存中还要细分为三个区域
- 新生代:存活的对象少,垃圾多
- Eden区
- Survivor0
- Survivor1
- 老年代:存活的对象多,垃圾少
- 永久区
GC垃圾回收,主要是在Eden区和老年代
假设内存满了,OOM(out of memory)
JDK8后,永久存储区改了名字,元空间
永久区
这个区域常驻内存,用来存放JDK自身携带的Class对象,interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭虚拟机就会释放这个区域的内存
一个启动类加载了大量的地反方jar包,Tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直达内存满,就会出现OOM
- jdk1.6以前:永久代,常量池是在方法区
- jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
- jdk1.8以后:无永久代,常量池在元空间
元空间:逻辑上存在,物理上不存在
long max = Runtime.getRuntime().maxMemory();//返回虚拟机试图使用的最大内存
long total = Runtime.getRuntime().totalMemory();//返回jvm的初始化总内存
//默认情况下:分配的总内存时电脑内存的1/4,而初始化内存是1/64
//修改 VM options
//-Xms1024m -Xmx1024m -XX:+PrintGCDetails
//-Xms 设置初始化内存分配大小
//-Xmx 设置最大分配内存
//-XX:+PrintGCDetails //打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError //OOM dump
OOM
- 尝试扩大堆内存看结果
- 分析内存,看一下哪个地方出现了问题(专业工具)
在一个项目中,突然出现了OOM故障,那么该如何排除,研究为什么出错
- 能够看到第几行出错:内存快照分析工具,MAT,Jprofiler
- Debug 一行行分析代码
MAT,Jprofiler
- 分析Dump文件,快速定位内存泄漏问题
- 获得堆中的数据
- 获得大的对象~
- ...
GC
JVM在进行GC时,并不是对这三个区域统一回收,大部分时候,回收都是新生代
- 新生代
- 幸存区(from,to)
- 老年代
GC两种类:轻GC(普通的GC),重GC(全局GC)
GC机制
MinorGC,Major GC vs Full GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC
Major GC 是清理永久代。Full GC 是清理整个堆空间—包括年轻代和永久代。
引用计数法
- 计数器本身也会消耗,空间成本
- 效率低
复制算法
- 每次GC都会将Eden活的对象移到幸存区:一旦Eden区被GC后,就会是空的
- 谁空谁是to
- 当一个对象经历了默认15次GC,都还没有死,转到老年代
- GC调优,修改默认次数
- -XX:MaxTenuringThreshold=5
- 通过这个参数设定进入老年代的时间
好处:没有内存碎片
坏处:浪费内存空间~:多了一半空间永远是空to
复制算法最佳使用情景:对象存活度低
标记清除算法
- 扫描对象:对对象进行标记
- 清除:对没有标记的对象,进行清除
缺点:两次扫描,严重浪费时间,会产生内存碎片
优点:不需要额外的空间
标记压缩
再优化:
- 压缩:防止内存碎片产生,再次扫描,向一端移动存活的对象,多了一个移动成本
标记清除压缩
- 先标记清除几次,再压缩
总结
- 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
- 内存整齐度:复制算法=标记压缩算法>标记清除算法
- 内存利用率:标记压缩算法=标记清除算法>复制算法
答案:没有最好的算法,只有最合适的算法——>GC:分代收集算法
年轻代:
- 存活率低
- 复制算法
老年代:
- 区域大:存活率高
- 标记清除(内存碎片不是太多)+标记压缩混合实现
CMS收集器
CMS:老年代收集器,以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现
过程:
- 初始标记:独占PUC,stop-the-world, 仅标记GCroots能直接关联的对象
- 并发标记:可以和用户线程并发执行,通过GCRoots Tracing 标记所有可达对象。
- 重新标记:独占CPU,stop-the-world, 对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新自我拯救那部分逃逸对象
- 并发清理:可以和用户线程并发执行,清理垃圾
优点:
- 并发,低停顿
- CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。
缺点:
- 对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢
- 法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾
- CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC
CMS出现FullGC的原因
- 年轻代晋升到老年代没有足够的连续空间,很有可能是内存碎片导致的,因此会触发FULL GC
- 在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC
G1收集器
初始标记-->并发标记---->最终标记---->筛选回收(根据用户期望的GC停顿时间回收)
特点:
- 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间
- 分代收集:分代概念在G1中依然得以保留
- 空间整合,没有内存碎片产生:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和来年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合
JMM
java内存模型
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,
作用:缓存一致性协议,用于定义数据读写的规则
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存
线程工作内存:每个线程都有自己的工作区域,是从主内存拷贝的
可见性问题,指令重排
- volatile
- synchronized