JVM体系结构
类加载器及双亲委派机制
类加载器
JVM提供了三层类加载器:
- Bootstrap classLoader(启动类/根加载器):主要负责加载核心类库(如java.lang.*等),构造ExtClassLoader和AppClassLoader。
- ExtClassLoader(扩展类加载器):主要负责加载jre/lib/ext目录下的一些扩展类
- AppClassLoader(应用程序加载器):主要负责加载应用程序的主函数类
双亲委派机制
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
类加载器级别:App->Ext>Boot
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制的作用
1、防止重复加载同一个.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class
不能被篡改。通过委托方式,不会去篡改核心.clas
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
测试
沙箱安全机制
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络
。
Native
/**
* native:凡是带用native关键字的,代表Java作用范围达不到了,需要回去调用底层C语言的库
* 会进入本地方法栈 调用本地方法接口(JNI)
* JNI作用:扩展JAVA程序,融合不同的编程语言为Java所用 最终是为了融合C、C++
* Java诞生的时候 C、C++语言很流行,Java语言想要有立足之地的话就必须能够调用C、C++的程序
* 它在内存区域中专门开辟了一块标记区域:Native Method Stack 登记native方法
* 在最终执行的时候,通过JNI加载本地方法库中的方法
*
* 现在在开发中很少用到native了(适用场景:Java驱动打印机等)
*/
public native void start0();
PC寄存器
程序计数器:Program Counter Register
每个线程都有程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址)
方法区(Method Area)
方法区被所有线程共享。静态变量(static)、常量(final)、类模板信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中。(方法区中包含的都是在程序中永远的唯一的元素)
栈
程序=数据结构+算法
为什么main先执行,最后结束?
栈:栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放。
栈运行原理:栈帧
栈满了:StackOverError
栈中存放的是保存基本数据类型的对象和自定义对象的引用(不是对象)。
HosPot
三种JVM:
- Sun公司 Java HotSpot™
- BEA JRockit
- IBM J9VM
我们都是学习HotSpot
堆
一个JVM只有一个堆内存 堆内存大小是可以调节的
堆内存要分为三个区域:新生区、养老区、永久区
GC回收主要是在伊甸去和永久存储区
在JDK8以后,永久存储区 改名为元空间
元空间逻辑上存在 物理上不存在
新生区
新生区是类诞生和成长的地方,甚至死亡。
包含两个区域:
- 伊甸园区:所有的类都是在伊甸园new出来的
- 幸存者区(包含两个幸存者区)。
new出来的类先存在伊甸园去,当伊甸园满了之后,触发一次轻GC,存活的实例
则放在幸存者区。(经过研究,百分之99的对象都是临时对象,大多在新生区都被消灭了)
永久区
这个区域常驻内存。用来存放JDK自身携带的Class对象、interface元对象。这个区域不存在垃圾回收!关闭JVM虚拟机就会释放这个区域的内存。
该区一般不会出现OOM。
若出现OOM,可能原因有:
- 一个启动类加载了大量第三方jar包
- Tomcat部署很多的应用
jdk1.8之后,就被成为元空间了。
元空间逻辑上存在,物理上不存在。
内存测试
// 虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
// 虚拟机初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println(max + "字节 " + max/(double)1024/1024 + "M");
System.out.println(total + "字节 " + total/(double)1024/1024 + "M");
输出:(默认情况下 分配的总内存是电脑内存的是1/4,初始化的内存是1/64)
857735168字节 818.0M
58720256字节 56.0M
自定义JVM参数:
所以说永久区逻辑上存在,物理上不存在。
JVM调优
OOM异常排除
使用JProfile分析OOM原因
- 安装JProfile
- 下载JProfile客户端并安装
- 设置JVM参数
GC垃圾回收算法
JVM进行垃圾回收的区域:新生代(Eden、Survivor from、Survivor to)、老年代。大部分GC都在新生代中发生。
新生代发生的GC叫Major GC,老年代发生的GC叫Full GC,Full GC至少伴随着一次Major GC。
GC算法:引用计数法
不是很常用
GC算法 :复制算法
年轻代的GC主要使用复制算法(负责Survivor from、 to两区的复制)
好处:没有内存碎片
坏处:浪费了一块内存空间。(多了一块空间to区永远是空的)(如果在极端情况下,对象100%存活,这样就不好 因而新生代采用该算法)
标记清除算法
优点:不需要额外的空间
缺点:需要扫描两次,浪费时间,会产生内存碎片
标记整理算法
先标记清除几次,在压缩 这样比较节省性能
GC 算法总结
内存效率(时间复杂度):复制算法>标记清除算法>标记整理算法
内存整齐度:复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法
GC使用分代收集算法
年轻代:存活率低,所以采用复制算法
老年代:区域大,存活率高 使用标记清除算法+标记整理算法混合实现