JVM内存模型
计数器
对于一个处理器(如果是多核CPU那就是一核)再一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程的切换可以恢复到正确执行位置,每个线程都需要有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
虚拟机栈
每个方法被执行的时候都会创建一个栈帧用于存储内部变量表,操作栈,动态链接,方法出口信息等,每一个方法被调用的过程就对应一个栈帧再虚拟机栈中从入栈到出栈的过程
*存放java方法(8大计基本类型 对象引用 实例的方法)
动态链接:符号引用或直接引用在运行时进行解析和链接的过程
本地方法栈
本地方法栈则为虚拟机使用到的native方法服务
native:
- 凡是带了native关键字,说明java作用的范围已经达不了,会去调用C语言库
- 会进用本地方法栈
- 调用本地方法本地接口JNI
- JNI作用:扩展Java的使用,融合不同的编程语言为java所用,c c++
- 他在内存区域专门开辟了一块标记区域 Native Method Stack 标记Native方法
- 在最终运行的时候,加载本地方法库中的方法,用过JNI
再Thread类中就有很多native方法
堆
- 一个JVM只有一个堆内存,堆存在大小可以调节
- 堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。
方法区
用于存放已被虚拟机加载的类信息,常量,静态变量,如static修饰的变量加载类的时候就会被加载中方法区中。
JVM垃圾回收
三种JVM Hotsport JVM jockit javm
堆内存中分三个区域
- 新生代
- 养老区
- 永久区(JDK8之后,变成元空间)
GC垃圾回收主要在新生代和养老区
永久区(这个区域常驻内存)
- JDK1.6之前,永久代,常量池在方法区
- JDK1.7 永久代,去永久代,常量池在堆中
- JDK1.8 无永久代,元空间,常量池在元空间
用来存放JDK自身的Class对象,Interface元数据,存储的是Java运行时的一些环境或者类信息,不存在垃圾回收,关闭VM虚拟就会释放。
Hostport JVM把年轻代分为三部分
- 一个eden区(所有对象都在这里new出来)
- 两个Survivor区(一个from 一个to)
- 默认比例8:1
java
默认情况下分配的每次是电脑的1/4,而初始话内存为1/64
新生代的垃圾回收流程
- 一般情况下,新创建的对象都会被分配到Eden区(一些打对象特殊处理)
- 当Eden区存满后会触发一次Minor GC(轻GC)
- Minor GC先可达性算法分析出Eden区中存活的对象,将存活的对象复制到To区,并将Eden区的数据清除(复制算法)
- 在负责过程中,如果发现To区不足以存储存活对象,会直接将存活对象放到老年区再复制过程中
- 如果发现有对象较大,会直接将大对象存放到老年代,该值可以由-XX:PretenureSizeThreadold=2m进行设置,默认为0;
- 对象经历过一次MinorGC后年龄被标记+1,当年龄达到15时,会被直接送入老年代
- -XX:MaxTenuringThreshold 默认值为15
- 完成以上步骤后,会将From区和To区内容进行调转
- 下一次有新对象进来,照样存储在Eden区,Eden区再次存满后触发GC
- GC会对Eden区和From区进行垃圾标记,被标记为存活的对象复制到To区
- 然后清理Eden区和To区
什么时候发生重GC:
一个大的对象再Eden区放不下会放到老年代,诺老年代也放不下,则触发发重GC,清理老年代空间,若还不足,则触发OOM错误
可达性分析
回收前,要判断堆中的对象哪些还存活着,哪些已经死去(没有任何途径使用的对象)
- 这个算法的基本思路就是通过一系列成为GC ROOTS的对象做为起始点
- 从这些节点开始向下搜索
- 搜索所走过的路径成为引用链(Reference Chain)
- 当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。
什么可作为GC Roots?
- 虚拟机栈中引用的对象,例如方法的入参,局部变量,临时变量等
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象,如字符串常量池中的引用
- 本地方法栈JNI引用的对象
- JAVA虚拟机内部的引用,如基本数据对应的Class对象,常驻异常处理
- 被synchronized关键字持有的对象。
GC算法
引用计算法
复制算法
- 好处:没有内存的碎片,
- 坏处:浪费了内存空间(多了一般to永远为空)对象100%存活不适合
- 使用场景:对象存活度较低的区域(新生代)
标记清除压缩算法
- 标记清除:
- 好处:不需要额外的空间
- 坏处:两次扫描,严重浪费时间,会产生内存碎片
- 标记清除压缩
- 好处:防止产生内存碎片(一个系统中不可用的空间)
- 坏处:再次扫描,向一段移动存活的对象,多一个移动的成本
GC算法总结
内存效率: 复制算法 > 标记清除算法 > 标记清除压缩算法 (时间复杂度)
内存整齐度: 复制算法 = 标记清除压缩算法 > 标记清除算法
内存利用率:标记清除压缩算法 > 标记清除算法 > 复制算法
GC分代收集算法:
- 年轻代:存活率低,复制算法
- 老年代:区域大,存活率高,标记清除算法 + 标记清除压缩算法 实现
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载都应当有自己的父类加载器。
这里的类加载器之间的父子关系一般不会以继承的关系来实现。而是都使用组合关系来复用父加载器的代码
工作过程
- 如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个类请求委派给父类加载器去完成;
- 每个层次的类加载器都是一致的,因为所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载;
双亲委派模型解决的问题
- 每一个类只会被加载一次,避免了重复加载
- 每个类都会被尽可能的加载(从引导类加载器往下,每个加载器都额可以会根据优先次序尝试加载它)
- 有效避免了某些恶意类的加载(比如自己定义了java.lang.object类,一般而言再双双亲委派模型下会加载系统的Object类而不是自定义的Object类)
问:
能不能自己写个类,叫java.lang.String
答:
可以,但是无法运行,
- 因为最终都会由启动类加载器去加载,始终为加载jre.jar包里的类
- 是否可以用自己的类加载器去加载
- 即使自定义了自己的类加载器,强行用defineClass()方法去加载一个以java.lang开头的类也不会成功
- 如果尝试这样做,那么会收到一个由虚拟机自己抛出的java.lang.SecurityException:Probibited package name: java.lang 异常
如何排查内存溢出