JVM(Java虚拟机)
JVM探究
- 请你谈谈你对JVM的理解?Java8虚拟机和之前的变化更新?
- 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析
- JVM常见的调优参数有哪些?
- 内存快照如何抓取?怎么分析Dump文件?
- 谈谈JVM中,类加载器你的认识
JVM的位置
JVM即相当于一个软件,而我们的Java程序就相当于是软件里的功能,既然是软件,就必须要有操作系统支撑
JVM的体系结构
JVM调优其实本质上就是在对方法区和堆区进行调优,其中99%是在对栈区进行调优
类加载器
作用:加载Class文件~
public class ClassLoader{
public static void main(String[] args) {
ClassLoader loader = new ClassLoader();
java.lang.ClassLoader classLoader = loader.getClass().getClassLoader();
System.out.println(classLoader);//AppClassLoader 应用程序加载器
System.out.println(classLoader.getParent());//ExtClassLoader 扩展类加载器
System.out.println(classLoader.getParent().getParent());//null 是由C++编写的BootStrapClassLoader,对Java不可见,它是最顶层的类加载器
}
}
1,虚拟机自带的加载器
2,根加载器
3,扩展类加载器
4,应用程序加载器
双亲委派机制
什么是双亲委派机制?
双亲委派机制是Java为了运行环境的安全起见,做出的一种保护机制,比如说我们常用的java.lang.String这个类。我要是写一个同包同名的类会怎样?按照常理来说,我们创建我们所写的类对象,就按照类对象去执行,其实不然,话不多说,上代码:
package java.lang;
public class String {
@Override
public String toString() {
return "double";
}
public static void main(String[] args) {
System.out.println(new String().toString());
}
}
运行看结果:
我们的类明明存在main函数,但是idea告诉我们没有定义main函数?为什么?这就是双亲委派,一种安全机制,
程序再运行时会去找到类加载器,逐层的找
–AppClassLoader—ExtClassLoader–BootStrapClassLoader
我们所定义的String类处于AppClassLoader这一层,
而Java执行的是从rt.jar包的根加载器开始,平时我们写的普通类,
它加载时其实走了这么一段路,先从根加载器找,找不到再找扩展类加载器,再找不到就找应用程序加载器,最后运行
- 类加载器收到类加载的请求
- 将这个请求向上委托给父类加载器,一直向上委托,直到启动类加载器
- 启动类加载器检查是否能加载委托上来的这个类,能加载就使用当前加载器加载,否则抛出异常,通知子类加载器加载
- 重复 3
- 应用程序也加载不到就抛出ClassNotFoundException
沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在JVM虚拟机特定的运行范围中,并且严格限制代码对本地资源系统的访问,那系统资源包括什么?CPU,内存,文件系统,网络,不同级别的沙箱对这些资源访问的限制也可以不一样
所有的Java程序运行都可以指定沙箱,可以指定安全策略
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认为可信任的,而远程代码则被看做是不受信任的,对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码在早期的Java实现中,完全依赖于沙箱安全机制,如下图所示的JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统资源的时候,就无法实现,因此在后续Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限,如下图所示JDK1.1安全模型
在Java1.2版本中,再次改进了安全机制,增加了代码签名,不论是本地代码还是远程代码,都会按照用户的安全策略设定,由类加载器到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制,如下图所示JDK1.2安全模型
当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示 最新的安全模型(jdk 1.6)
以上提到的都是基本的Java 安全模型概念
,在应用开发中还有一些关于安全的复杂用法
,其中最常用到的 API 就是 doPrivileged。doPrivileged 方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源
。有时候这是非常必要的,可以应付一些特殊的应用场景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。
沙箱安全的基本组成
-
字节码校验器
(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。 -
类装载器
(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
-
存取控制器
(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。 -
安全管理器
(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。 -
安全软件包
(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
Native
什么是native?
通过阅读Thread类的源码.我们发现了这么一个方法
private native void start0();
线程启动时调用这个方法,native(本地) , 这个关键字其实是调用了底层C语言的库,因为Java虚拟机是运行在操作系统之上的, 本质上他不能直接操作硬件,而是通过native调用本地方法库, 凡是带了native这个关键字的额=,说明Java的作用范围达不到了,只能调用底层C语言的库,
这个方法运行时会进入本地方法栈,然后调用本地方法接口(JNI)
那它的作用是什么呢?
- 扩展Java的使用,融合不同编程语言为己所用,
- 最初在java诞生之时,C和C++时最流行的,要想有人使用,必须要调用C,C++得程序
- 于是Java得内存区域中,开辟了一块标记区域,Native Method Stack, 登记Native方法
- 在最后执行得时候,加载本地方法库中得方法,通过Java Native Interface
PC寄存器
+++
程序计数器:Program Counter Register
每一个线程都有一个程序计数器,是线程私有得,就是一个指针,指向方法区中得方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码), 再执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
方法区
Method Area 方法区
方法区就是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间
静态变量, 常量, 类信息 (构造方法, 接口定义 ), 运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
static final Class 常量池
栈
栈:数据结构
栈: 先进后出, 类似于一个桶,先进去都被压在最底下, 只有后面进去的出来了以后才能出来,
队列: 先进先出, 类似于一个管道, 这一端进,另一端出,
栈: 栈内存, 主管程序的运行, 声明周期和线程同步,
线程结束,栈内存就释放,对于栈来说, 不存在垃圾回收,一旦线程结束,栈就Over
栈存放的东西: 八大基本类型 + 对象引用 + 实例的方法
栈运行原理: 栈帧
栈满了就会StackOverFlowError
栈 + 堆 + 方法区: 交互关系
三种JVM
-
Sun公司 HotSpot
我们最常使用的
-
BEA的 JRockit
-
IBM的 J9VM
堆
堆(Heap), 一个JVM只有一个堆内存, 对内存的大小是可调控的
类加载器读取了类文件之后, 一般会把类, 方法, 常量,变量~ 保存所有引用类型的真实对象
堆内存还要细分为三个区域:
- 新生区(伊甸园区)
- 养老区
- 永久区
在新生区没被GC清理掉的,会进入幸存区
在幸存区经历了指定GC清理次数仍然没有被清理掉或者幸存区满了的,会进入老年区
永久去在1.8以后改名元空间
新生区
- 类: 诞生和成长的地方,在甚至死亡
- 伊甸园, 所有的对象都是在伊甸园区new 出来的
- 幸存者区, 在新生区没被轻GC清理掉的,会进入幸存区
- 幸存者1区
- 幸存者2区
老年区
永久区/元空间
这个区域常驻内存的,用来存放JDK自身携带的Class对象,interface元数据, 存储的是Java运行时的一些环境或类信息,这个区域不存在任何垃圾回收, 关闭虚拟机就会释放这个区域的内存,
一个启动类, 加载了大量的第三方jar包, Tomcat部署了太多的应用, 大量动态生成的反射类, 不断的被加载, 知道内存满,就会出现OOM溢出
- JDK1.6之前: 永久代, 常量池是在方法区
- JDK1.7 : 永久代, 但是慢慢退化了, 提出去除永久代的想法, 常量池在堆中
- JDK1.8之后: 无永久代, 常量池在元空间
逻辑上存在, 物理上不存在
查看虚拟机内存:
默认情况下: 分配的总内存是电脑内存的四分之一,初始化内存是六十四分之一
通过参数调节JVM的内存占用
调节虚拟机内存初始内存和最大内存为1G, 并答应GC垃圾回收信息-Xms1024m -Xmx1024m -XX:+PrintGCDetails
输出结果
相加新生区根老年区的占用空间:
发现所得结果刚好是虚拟机总内存,那么 元空间呢? 它不占用内存吗? 因此我们得出, 元空间只是逻辑上存在,物理上不存在
制作一个简单的OOM错误
public class MemoryTest {
public static void main(String[] args) {
String a = "测试OOM溢出";
while (true){
a+=new Random().nextInt(999999999)+new Random().nextInt(999999999);
a+=a.toString();
}
}
}
将虚拟机内存设置小一点,为8m
OOM这么严重,我们应该怎么对JVM进行调优呢???
堆内存调优
在一个项目中,突然出现了OOM故障,应该如何排除,找出出错的原因
- 能够看到代码第几行:内存快照分析工具。Jprofile,MAT
- Debug,一行一行分析代码!
内存调优工具的作用:
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中的数据
- 获得大的对象等
使用:
使用idea安装Jprofile插件
安装客户端:安装路径尽量没有空格没有中文
注册码网上一堆:
JProfiler11 序列号
L-J11-Everyone#speedzodiac-327a9wrs5dxvz#463a59
A-J11-Everyone#admin-3v7hg353d6idd5#9b4
JProfiler10 序列号
A-tfbyKUM9Gw-KhGMbpYhS1#14246
S-QCM1I25qH1-CkLfdYOFs2#1018
L-GG5oEVjKQX-xEJjkR3QBb#1847
L-idEVpl1jvU-Ww3AnQGBUY#4148
S-p8q09PhrZp-ioZmzCnXlT#18231
L-Vy82rebM6e-nLYfOEykeP#34152
A-r8m8UInymG-S382j9ujs5#3265
A-iWZjln8l5O-QAG2CyKTeC#26123
L-MTGPt84xpw-06dzulmNLY#301110
L-fuoED44azj-OyQMvOutje#22275
作者:FastCoder
链接:https://www.jianshu.com/p/e0c5488dfd3d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
idea配置
测试OOM
public class MemoryTest {
public static void main(String[] args) {
String a = "测试OOM溢出";
while (true){
a+=new Random().nextInt(999999999)+new Random().nextInt(999999999);
a+=a.toString();
}
}
}
添加调优参数 -Xms1m -Xmx3m -XX:+HeapDumpOnOutOfMemoryError
运行:
成功报错并且通过参数生成了Dump文件,打开Dump文件
使用我们刚刚安装的Jprofile客户端打开文件
通过线程精准定位错误位置
main线程第17行引起错误
也可以通过其他方法进行分析,大对象,快照等
使用到的参数
-Xms3m 设置初始内存大小,默认为计算机内存的1/64
-Xmx4m 设置最大内存分配,默认为计算机内存的1/4
-XX:+PrintGCDetails 打印GC垃圾回收信息
-XX:+HeapDumpOnOutOfMemoryError 在发生OutOfMemory错误时生成Dump文件
GC垃圾回收
JVM在进行垃圾回收时,并不是对这三个区域统一回收,大部分时候,回收都是在新生代
- 新生代
- 幸存区(form,to)这两个会不停交换
- 老年区
GC两种类型
- 轻量级GC
- 重GC,全局GC
堆里面的分区有,Eden,form,to,old
GC常用算法
标记清除,标记压缩,复制算法,引用计数器
- 引用技术法,就是在程序的对象身上做一个标记,使用一次计数器就+1,最终没被使用的就被清理
new()A因为没有被使用,直接被清理,引用技术法用得并不多,因为它需要为每个对象分配计数器,本身也存在资源消耗
-
复制算法
什么是复制算法?
- 每次GC都会将Eden区中或者的对象移到幸存区中,一旦Eden区经历过垃圾回收以后,Eden区就空了
- 幸存区分幸存01区和幸存02区,一个form一个to,如何区分谁是form谁是to呢?这两个区是必须要保证有一个是空的,谁是空的,谁就是to,这两个区是在不停的交换的,复制算法就是,假设幸存01区中的对象还活着,就把活着的对象复制到幸存02区,此时02区从to变成了from,而原来的01区对象复制到02区后,01区就空了,就从from变成了to,以此循环,这就是复制算法,年轻代主要使用复制算法
- 什么时候对象会从幸存区进入养老区呢?就是当对象在幸存区中默认经历了15次GC后,依然没有被清理掉,就会进入养老区,默认是15次,可以通过这个参数来控制
-XX:MaxTenuringThreshold=15
- 复制算法的好处是没有内存碎片,坏处是浪费了内存空间,因为GC过后Eden和to两个区都是空的,此算法的最佳应用场景,就是在对象存活度较低的时候使用,
清理垃圾过程
-
标记清除
回收时将对象进行标记,对活着的对象进行标记,清理时未被标记的对象将被清理掉
- 缺点:标记时扫描,清理时扫描,两次扫描,严重浪费时间,会产生内存碎片
- 优点:不需要格外的空间
-
标记压缩:
标记清除优化,防止内存碎片产生,再次扫描,向一段移动存活的对象,多了一个移动程本,解决了内存碎片的产生
效率比对:
内存效率:复制算法–>标记清除算法–>标记压缩算法(时间复杂度)
内存整齐度: 复制算法 = 标记压缩算法–> 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法–> 复制算法
就好像是鱼和熊掌不可兼得,想要什么就会失去什么,难道没有最优的算法吗?是的,没有~!**没有最好的,只有最合适的,**就跟谈恋爱一样,所以GC也被称之为分代收集算法。
年轻代,存活率低,使用复制算法最好
老年代,存活率高,区域大,使用标记清除压缩混合实现,