JVM体系结构
1.JVM的位置
2.JVM的体系结构
3.类加载器
含义
将class文件通过二进制读到内存,并且放在运行时数据区内的方法区内,然后在堆区创建class对象,用于存放数据。
分类
- 虚拟机自带加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
双亲委派模型
含义:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动加载器检查是否能够加载当前这个类,能就结束,不能就抛出异常,通知子加载进行加载。
好处:避免重复加载 + 避免核心类篡改
注意:我们一般不能自己写个类叫java.lang.System
|解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。|
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;
}
}
4.沙箱安全机制
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
*
5.native关键字*
凡是带了native关键字的,说明Java的作用范围达不到了,会去调用底层的c语言的库。,会进入本地方法栈,然后再调用本地接口(JNI),本地接口作用:扩展Java的使用,融合不同的编程语言为Java语言所用。
Java在内存区域专门开辟一块标记区域:native method stack,登记native方法。
public class TestThread {
public static void main(String[] args) {
new Thread(()->{
},"my thread name").start();
}
private native void start0();
// public void start();//这里报错,因为没有native
}
6.pc寄存器*
7.方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,
简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关I
方法区就存static,final,class,常量池这些。
下面就是对象在类加载时的内存分析:
8.栈
栈:先进后出,后进先出:桶
队列:先进先出(FIFO)
思考:为什么main方法先执行最后结束
栈:栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收
一旦线程结束,栈就结束
栈主要存储:8大基本类型,对象引用,实例方法
栈满了:stackovererror
栈+堆+方法区:的交互关系
对象在内存中的实例化过程:
9.堆
Heap,一个JVM只有一个堆内存,堆内存大小是可以调节的
类加载器读取了类文件后,一般会把类,方法,常量,变量放到堆中。保存我们的真实对象。
堆内存中还要细分为三个区:
- 新生区:Eden space(所有对象都是在这new出来的),幸存区
- 养老区:
- 永久区:用来接收JDK自身携带的class对象
- jdk1.6之前:永久代,常量池是在方法区
- jdk1.7:慢慢退货永久代,常量池在堆中
- jdk1.8:无永久代,常量池在元空间
假设内存满了抛出outofMemoryError
在jdk8以后,永久存储区改了名字为元空间
在一个项目中出现OOM故障,那么该如何排除,为什么出错 - 能看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
- Debug,一行行分析代码
- MAT,Jprofiler作用
1.分析Dump内存文件,快速定位内存泄漏,获取堆中数据,获取最大对象
.堆结构
分为新生代和老年代,JDK1.8中,永久代已经从java堆中移除;
所有新生成的对象首先都是放在年轻代的(除了部分大对象通过内存担保机制创建到Old区域,默认大对象都是能够存活较长时间的);
年轻代分三个区。一个Eden区,两个 Survivor区(一般而言),可以配置多个,他们之间的比例为8:1:1,也可调节;
当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区;
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中;
Eden:该区域的对象大部分都是短时间都会死亡的,故垃圾回收器针对该部分主要采用标记整理算法了回收该区域。
Survivor:复制算法
Old: 标记清除
GC:垃圾回收
JVM在进行GC时,并不是对这三个区域统一回收,大部分时候是对新生代的回收
- 新生代
- 幸存区(from ,to)
- 老年区
GC两种类型:轻GC,重GC
JVM的内存模型和分区?
堆里面的分区有哪些?他们的特点
堆里面分为新生代和老生代(java8取消了永久代,采用了Metaspace),新生代包含Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to,当经过一次或者多次GC之后,存活下来的对象会被移动到老年区,当JVM内存不够用的时候,会触发Full GC,清理JVM老年区
当新生区满了之后会触发YGC,先把存活的对象放到其中一个Survice
区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把Eden 进行完全的清理,然后整理内存。那么下次GC 的时候,就会使用下一个Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为JVM 认为,一般大对象的存活时间一般比较久远。
GC的算法有哪些和怎么用
标记清除算法:先标记需要清除的对象,标记完之后清除对象;
好处:不需要移动对象,效率高,在对象存活率较高的情况下效率非常高;
缺点:可用的内存并不是连续的,而是断断续续,造成大量的内存碎片。 存储对象时要求内存空间时连续的,所以虚拟机在给新的内存较大的对象分配空间时,有可能找不到足够大的连续的空闲的空间来存放,从而引发一次垃圾回收动作;
标记整理算法:先标记需要清除的对象,然后将所有存活的对象向一端移动;
好处:内存空间连续性大;
缺点:需要移动对象。造成时间、cpu的浪费 ;
复制算法:将内存分为两块大小一样的区域,每次是使用其中的一块。当这块内存块用完了,就将这块内存中还存活的对象复制到另一块内存中,然后清空这块内存;
好处:在对象存活率低时效率很高(例如新生代,因为新生代的对象基本上都是朝生夕死的,存活下来的对象约占10%左右),在垃圾回收的过程也不会出现内存碎片的情况,不需要移动对象,只需要移动指针即可,实现简单,所以运行效率很高;
缺点:每次只能使用一半的内存;
虚拟机如何判断哪些对象需要进行GC处理?
java虚拟机是通过可达性分析算法来判定对象是否存活。当一个对象到GC Roots没有任何引用链相连,或者说从GC Roots到这个对象不可达时,这个对象将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
轻GC和重GC分别在什么时候发生
年青代的eden区满了会触发younggc
老年代old满了会触发oldgc
Full GC清理整个heap区,包括Yong区和old区
引用计数器:
复制算法;
好处:没有内存碎片
坏处:浪费内存空间,只能用一半
最佳使用场景:对象存活率较低的新生区
标记清除算法:
好处:不需要额外的空间
缺点:两次扫描,严重浪费空间
标记压缩算法:(对上面优化)
内存效率:复制算法>标记清除>标记压缩
内存整齐度:复制算法=标记压缩>标记清楚
内存利用率:标记压缩算法=标记清除算法>复制算法
年轻代:
- 存活低
- 复制算法
老年代: - 区域大:存活率
- 标记清除(内存碎片不是很多时)+标记压缩
JMM(Java内存模型)
JMM:(Java Memory Model的缩写)//Java内存模型的缩写
作用:
缓存一致性协议,用于定义数据读写规则
JMM定义线程工作内存和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存
解决共享对象可见性的这个问题:volitile
volatile:保证可见性和有序性,不保证原子性
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操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对yolatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安
全的了。但是这些规则实在复杂,很难在实践中直接分析.所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。