一.内存模型
堆 :
存放实例对象,堆区分为老年代和新生代;
栈:
虚拟机栈 是方法执行的内存模型
每个方法的从开始执行到执行完成,对应的都是一个栈帧从入栈到出栈的操作。栈帧是栈的基本单位,栈帧包括 局部变量区’、操作数栈、帧数据区。
局部变量表中存放的: 各种数据的基本类型,对象的引用,。
操作数栈中存放的是:临时的数据。
帧数据区中存放的是:一些数据,如常量池解析的支持。
本地方法栈:
虚拟机栈执行的java服务,而本地方法栈执行的是native服务。
方法区:
存放类的基本信息静态变量,常量,
运行时常量池,编译后的字节码。
程序计数器: 存放程序运行的位置。
二 堆区
堆区分为新生代和老年代,新生代又分为 Eden fromsurvivor toSurvivor,8:1:1,
每当新生去满了的时候,就会触发YGC, 先将存活的对象都放入to survivor中,然后把eden和另一个fromsurivior进行清空;等下一次进行垃圾清理的时候,就会使用另一个survvor,以此循环利用;
当做每次存放到to中的时候,age就会加1,等加到指定的阙值的时候就会放入到老年代,from 和to是相对的;(复制算法)
老年区中存放的是时间较久对象或者较大的对象,当jvm内存满了的时候。老年区也进行触发fullgc。
在清理的过程中如果存活对象太多或者太大的部分,survivor区存不下,就直接进入老年区。
三 对象创建的过程
1.当jvm 读取一个 new 的时候,会先去常量池中看下是否存在该类的符号引用,并检测是否已经被加载解析初始化,如果没有进行累的加载;
2.加载完之后,为其分配内存;在堆中分配有两种方法
①指针碰撞
当java堆区的内存是规整的,使用过的内存放在一边,空闲内存的放另一边,中间是指针分界点,只要把指针像空闲内存移动与对象大小相等的距离;
② 空闲列表
当java堆区的内存是不规整的,虚拟机把可用内存维护在一个表上,在表上找到一个大与对象小相等的内存空间分配,并更新表。
防止并发:
①采用cas,保证原子操作的原子性;
②把内存分配按照线程放在不同的空间,为每个线程在堆中提前分配一小块内存,称为本地线程缓存分配,Thread Local Allocation Buffer,TLAB;只要当TLAB的内存进行重新分配时才加锁。
3.在分配好内存之后,需要对内存进行初始化;
对象头:
将对象的类 哈希码 gc分代年龄等信息 设置到对象头中。
执行init方法。
内存布局:
对象头: 分为2部分,存储自身信息: 哈希码,gc年代分领等;另一部分 是类型指针,确定是属于哪个类;
实例对象: 存储对象中各个字段的内容
对齐填充: 可有可无,当实例对象没有的时候进行对齐填充。
4 对象的访问定位
对java堆中的对象操作,需要的是栈中的对象引用变量,reference。而具体的操作方式有两种:使用句柄和直接指针;
使用句柄:
在java堆中画出一块地方 叫做句柄,存放的是对象实例数据的地址,类型数据的地址。而reference 存储的是句柄的地址;
优点: 对象移动只需要修改句柄池中的数据,不需要修改reference;
直接指针 :
直接指向的是对象的地址。优点 就是快;
什么是GC:
gc就是当存在一些已经无用的对象的时候,gc垃圾回收机制就将他们回收,以此释放内存,无用对象的标准:
四 GC判定垃圾对象
1.根搜索法
以GCRoot对象为起点,所有可达的就是活对象,对于无法到达的则进行gc回收;
可以作为gcRoot的对象的有:
虚拟机栈中引用的对象;本地栈中引用对象;方法区中常量池引用的对象;方法区静态属性引用的对象;
但是当没某个对象无法到达时,不会立即回收 而是进行标记一次;然后看该对象是否该执行finalize();如果没有覆盖这个方法,或者已经执行了则直接进行gc回收。
如果需要,执行则把该对象放入到F-queue队列当中,此时虚拟机会触发低优先级的线程执行finalize()方法,执行完之后仍然存在队列中的对象进行二次标记,回收。
2.引用计数法
对每个对象设置一个引用计数器,每当被引用就+1,失效就减1,一直到0的时候,进行回收。
缺点是 :循环引用,A引用B,B引用A;
五. GC的收集方法
标记-清除算法:
原理: 先进行标记,然后清除。
缺点: 效率低,内存碎片化严重,当需要较大完整内存时,有可能因为内存碎片化无法提供,而提前fullgc
使用: 适用于老年代垃圾回收,CMS收集器就是采用该算法进行回收的。
标记-整理算法:
原理: 首先进行标记,然后让存活的对象统一移动到一边,然后清除端界以外的地方。
特点 :不产生碎片但是花费一定时间
使用: 适用与老年代的垃圾回收。parallel Old(针对parallel scanvange gc的) gc和Serial old收集器就
复制算法:
原理 :按容量划分内存,单独拿出一部分内存,当其他内存用完了,就将存活的对象放在该内存中,然后把使用过的内存进行清理掉,下一次在把当前内存存活对象存入另一边,反复循环使用。
特点: 牺牲一部分空间,移动堆顶指针
使用: 新生代,按照8:1:1的比例,将存回的对象放入其中一个sutvivor内存中,然后清除。缺点是,当survivor内存过小放不下时,一部分会直接进入老年代。
分代收集算法 :
将内存区域分为几个部分,新生代中的对象大都生命周期较短,所以采用复制算法,老年代中存活时间较长所以采用标记-整理的算法。
六 什么时候触发MinorGC,?什么时候触发FullGC?
新生代不够用的时候触发ygc,jvm不够用的时候触发FullGc;
老年代空间不足 发生fullgc。
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
方法区内存不足。
七.类加载过程
1.加载
①将根据类的权限名获取二进制字节流。
②将二进制字节流中的所代表的的静态存储结构转化方法区中运行时的数据结构。
③在堆区生成一个class对象,作为方法区这些数据的访问入口。
2.验证
确保class文件的字节流信息符合当前虚拟机的要求。
①文件格式验证
确保class文件能够正确的解析并存储到方法区中。经过这个验证,字节流才会进入内存的方法区中。
②元数据验证
对字节码描述的信息进行语义分析,保证符合java语言规范。
③字节码验证
是对校验类的方法的验证
④符号引用验证
这个过程发生在虚拟机将符号引用转化为直接引用的时候,确保符号引用的正确性。
3准备
为静态变量分配内存并且附上默认初始值。
4.解析
将常量池中的符号引用替换为直接引用。比如A类中的b方法,对于我们来说,b只是一个符号,因此调用b需要的是指针指向b的直接内存地址。这就是直接引用。
5.初始化
为静态变量附上正确的值。
八 类加载器
类加载器就是根据class的权限名获取二进制流文件。
类加载器:
启动加载器 :加载核心的java类库
扩展类加载器: java提供一个扩展类目录,加载目录下的类
系统类加载器: 加载具体的应用类
自定义加载器
双亲委派机制:
在系统类加载器进行加载的时候,会首先查看一下是否已经加载过了,如果没有加载过, 访问父类,看父类是否能够加载,父类在在继续委派父类的父类,如果父类无法加载,那么才会最开始的类去加载。
为何使用:
避免出现重复即同样的字节码,假如不使用双亲委派机制,各自使用各自的类加载器,无法保证类的唯一性。
如何自定义类加载器:
继承classLoader,重写findclass;因为在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass
函数。
如果没有指定, 那么就默认系统类加载器是其父类。
九GC策略
新生代策略:
串行GC: 单线程 serial_copying 优点:效率较高,不必考虑线程问题。
并行gc: 多线程 parallel scavenge 适用于多cpu上,对暂停时间要求较短。
并行gc : 结合老年代的cmsGc
老年代策略:
CMSGC:使用标记清除算法 ,牺牲吞吐量换区最短时间响应,对于服务器响应速度要求高的适用。
缺点:产生碎片,抢占cpu和应用线程,
优点: 只有第一次标记和重新标记的时候才会暂停整个应用,这对应用程序影响较小。
paraller mark sweep 标记压缩算法,把老年代分为若干个子区域,多个线程同时对每个子区域进行活跃对象标记,然后清除不活跃对象,把活跃对象整合在一起。
优点: 缩短了暂停时间,但是老年区的空间较大,标记花费时间较多。