对狂神JVM探究的学习和总结
JVM探究
相关面试题:
- 请你谈谈你对JVM的理解?java8虚拟机和之前的有什么变化?
- 什么是 OOM,什么是栈溢出(StackOverFlowError)?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快照如何抓取,怎么分析Dump文件?知道吗?
- 谈谈JVM中,你对类加载器的认识?
知识点:
1.JVM位置
2.JVM的体系结构
3.类加载器
作用:加载Class文件
3.1.虚拟机自带的加载器
3.2.启动类(根)加载器
3.3.扩展类加载器
3.4应用程序加载器
4.双亲委派机制
双亲委派机制作用:保证安全
执行顺序为:应用程序加载器=>扩展类加载器=>根加载器(最终执行)
根加载器检查是否能够加载当前类,如果可以加载,就结束,不能就会抛出异常,通知子加载器进行加载
如果到了应用程序加载器还没找到,就会报错(Class Not Found)
5.沙箱安全机制
什么是沙箱?
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
??所有的Java程序运行都可以指定沙箱,可以定制安全策略。
java中的安全模型:
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示 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虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
??类装载器采用的机制是双亲委派机制。
1.从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
- 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
安全提供者
消息摘要
数字签名
加密
鉴别
文章具体转载自6.Native
凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
如图所示,会进入本地方法栈,然后调用本地方法本地接口JNI
JNI的作用:扩展java的使用,融合不同的编程语言为java所用!最初想融合:c,c++
Java在内存中专门开辟了一块标记区域:本地方法栈(Native Method Stack)登记native方法
在最终执行过程中加载本地方法库中的方法通过JNI7.PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计8.方法区
Method Area
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,比如构造函数,接口代码也在此定义,简单来说,所有定义的方法的信息都保存在该区域,此区域属于共享区间:(static)静态常量,(final)常量,(Class)类信息(构造方法,接口定义).运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
9.栈
栈:先进后出
队列:先进先出(FIFO)
形象的例子喝多了吐就是栈,吃多了拉就是队列
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就over
栈:栈的八大类型+对象引用+实例的方法
栈运行原理:栈帧
栈满了就会抛出StackOverFlowError错误
栈+堆+方法区:交互关系
蓝色为堆,绿色为栈,粉红色为方法区,深蓝色为常量池类实例化时内存中发生的变化
测试代码:
public class People{
String name; // 定义一个成员变量 name
int age; // 成员变量 age
Double height; // 成员变量 height
void sing(){
System.out.println("人的姓名:"+name);
System.out.println("人的年龄:"+age);
System.out.println("人的身高:"+height);
}
public static void main(String[] args) {
String name; // 定义一个局部变量 name
int age; // 局部变量 age
Double height; // 局部变量 height
People people = new People() ; //实例化对象people
people.name = "张三" ; //赋值
people.age = 18; //赋值
people.stuid = 180.0 ; //赋值
people.sing(); //调用方法sing
}
}
代码解析:
这段代码首先定义三个成员变量:String name、int age、Double height 这三个变量都是只声明了没有初始化,然后定义了一个成员方法 sing();
在 main()方法里同样定义了三个一样的变量,只不过这些是局部变量;
在main() 函数里实例化对象 people , 内存中在堆区内会给实例化对象 people 分配一片地址,紧接着我们对实例化对象 people 进行了赋值。people 调用成员方法 sing() 。mian()函数打印输入人的姓名,人的年龄和人的身高,系统执行完毕。
下面通过图解法展示实例化对象的过程中内存的变化:
在程序的执行过程中,首先类中的成员变量和方法体会进入到方法区,如图:
程序执行到 main() 方法时,main()函数方法体会进入栈区,这一过程叫做进栈(压栈),定义了一个用于指向 Person 实例的变量 person。如图:
程序执行到 Person person = new Person(); 就会在堆内存开辟一块内存区间,用于存放 Person 实例对象,然后将成员变量和成员方法放在 new 实例中都是取成员变量&成员方法的地址值 如图:
接下来对 person 对象进行赋值, person.name = “小二” ; perison.age = 13; person.height= 180.0;
先在栈区找到 person,然后根据地址值找到 new Person() 进行赋值操作。如图:
当程序走到 sing() 方法时,先到栈区找到 person这个引用变量,然后根据该地址值在堆内存中找到 new Person() 进行方法调用。
在方法体void sing()被调用完成后,就会立刻马上从栈内弹出(出栈)
最后,在main()函数完成后,main()函数也会出栈 如图:
10.三种JVM
目前市面上就只有三种主流的JVM:SUN、BEA和IBM。
Sun公司的HotSpot 是目前使用范围最广的Java虚拟机。
BEA公司的JRockit(原来的 Bea JRockit)电脑软件,系列产品是一个全面的Java运行时解决方案组合。
IBM公司的J9 VM 是一个高性能的企业级 Java 虚拟机。
11.堆
Heap:一个JVM只有一个堆内存,堆内存的大小是可以调节的.
类加载器读取了类文件后,一般会把类,常量,方法,变量放到堆中,保存我们所有引用类型的真实对象
堆内存中还要细分三个区域:
- 新生区
- 老年区
永久区
GC垃圾回收,主要是工作在养老区和伊甸区
内存满了就会出现OOM,堆内存不够!
在JDK8以后,永久存储区改了个名字(元空间).12.新生区.老年区
新生区:对象诞生和成长的地方,甚至死亡;
新生区可细分为- 伊甸园区.所有对象都是在伊甸园区new出来的
幸存者区(0,1)
老年区:新生区进行轻gc活下来的对象13.永久区
这个区域是常驻内存的,存放JDK自身携带的Class对象.interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭虚拟机就会释放这个区域的内存- jdk 1.6 之前:永久代,常量池是在方法区
- jdk 1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
jdk 1.8之后:无永久代,常量池在元空间(逻辑上存在,物理上不存在,因为元空间直接在内存,不受堆空间的限制)
14.堆内存调优
OOM解决措施:
1.尝试扩大堆内存看结果
2.分析内存,使用专业工具(JPofiler)看一下是否有垃圾代码或者无限循环的代码占用空间
-Xms 设置初始化内存分配大小 1/64
-Xmx 设置最大分配内存,默认1/4
-XX:+PrintGCDetails 打印GC垃圾回收信息
需要配置命令:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError //OOM dump
15.GC:垃圾回收机制
JVM在运行GC时并不是对新生代(伊甸区),幸存区,老年区统一回收,大部分时候,回收的都是新生代
GC两个种类:轻GC(普通GC),重GC(全局GC)
每次GC,都会将伊甸园的对象转移到幸存区:一旦伊甸区被GC,那么伊甸园区就会是空的
幸存区from和幸存区to的区别,记住一句话 :谁空谁是to
当一个对象经历了15次GC还没有死的情况下,就好进入老年区,因为对象头只留了四位设置GC最大次数(-XX:MaxTenuringThreshold),所以只能最大值设为15
-XX:MaxTenuringThreshold(垃圾最大年龄):如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.