这里写自定义目录标题
第2章 内存管理机制
java内存模型
- | - |
---|---|
堆 | 共享,给实例、数组分配内存。新生代和老年代,分代垃圾回收算法 |
方法区 | 共享,存一些类信息、常量、静态变量 |
运行时常量池 | 编译期生成的各类字面量和符号引用 |
栈 | 虚拟机栈 本地方法栈 |
直接内存 | JDK1.4 NIO 用通道和缓冲区直接操作内存,避免在java堆和本地方法堆中来回复制数据 |
定义:
线程私有:各个线程独立的数据,互不影响。如PC
java虚拟机栈:包含1.局部变量表(存放了编译时可以确定的各种数据类型)
对象的创建
- 去常量池查看类信息,有没有加载 解析 初始化
- 已经加载过在堆中的,去Eden代去分配内存
1 如果内存是规整的直接移动指针 (指针碰撞)
2 如果内存有碎片 查看分配表 找到一块足够的区域分配给他(空闲列表) - 由于创建对象是一个非常频繁的事,可能A的指针还没来的及修改B由来修改指针,为了保证线程安全采用CAS和TLAB
- 初始化0值
- 设置对象头:虚拟机对对象设置,如哪个类实例,hash码,对象GC,元数据
- 执行程序员的INIT方法
对象的访问定位
- 句柄池 R-池-地址
- 直接指针 R-地址
第3章 垃圾回收
引用判断方法
- 引用计数法:循环引用问题
- 可达性分析法:栈中对象、方法区静态和常量
4种引用
- | - |
---|---|
强 | O o=newO()的 永远不会回收 |
软 | 在内存不够的时候才回收 |
弱 | 只要有垃圾回收就回收 |
虚 | 引用等于没有 |
垃圾回收算法
- 标记-整理:标记然后清除,有碎片
- 复制:把内存分成一半,每次回收把存活对象直接复制到另一半,然后把自己这半清除,用在新生代,因为统计98%的新生代对象都会被回收。80%Eden代,10% 10%surviver空间,回收时把Eden代的内容复制到10%空间中,如果空间不够使用老年代空间做担保。
- 标记-整理:让存活对象向一边移动,用在老年代,因为老年代对象的存活率很高。
- 分代收集法:分成新生代和老年代,分别用复制和标记整理算法。
发起GC:在安全点(主动中断,指令复用的地方如循环)或安全区(一段引用不会发生变化的区域)
垃圾收集器
- serial (OLD):单线程 简单速度快 ,会stop the world
- parnew:serial的多线程版本
只有上面两种可以与CMS配合使用
- parallel(+OLD 1.6):可控吞吐量
CMS(重点)concurrent Mark-sweep
- 初始标记: 标记GCROOTS,标记根节点 stop theworld
- 并发标记concurrent:从根节点遍历标记 耗时最长
- 重新标记:重新标记有变动的
- 并发清除:清除
- 缺点:
- 很吃CPU资源,尤其CPU只有1-2个核心时,要吃一半
- 清的过程中还产生垃圾,所以阈值要设68%,如果设置92%,在CMS运行期间 内存无法满足主程序运行需要(垃圾清的太慢了)就是清除失败CMF,只能采用 serial old这样停顿时间就很长了
- 标记清除有碎片
G1(重点)garbage-first1:
- 停顿时间可控,回收步骤类似CMS,宏观标记-整理,region之间标记-复制。
- 把所有堆分成多个region,维护个优先列表,回收价值最大的。
- region不是独立的,一个region的可能引用另一个region的对象,使用remembered set记录对象是否处于不同region中。
内存分配机制:
- 先在新生代分配,如果不够就执行一次minorGC把对象转移到survivor区,如果还不够,就通过担保机制转移到老年代。
- 大对象直接进入老年代。
- 长期存活的对象进入老年代:移动到survivor后年龄设置为1,每经过一次minorGC+1,默认15岁移动到老年代。or相同年龄所有对象的总和超过survivor的一半也移动,无需等到15。
类加载过程:
加载-连接(验证-准备-解析)-初始化
- 加载字节码文件,可以从各个地方加载(本地、网络等)
- 验证 字节码文件是否合法
准备 静态变量==的分配内存初始化值
解析 将符号引用替换为直接引用(字段、方法、接口) - 初始化 《clinit》由父到子执行程序员定义的静态方法
类加载器:放在JVM外部,可以由程序决定怎么加载字节码文件。即使是同一个类,用不同加载器加载,比较起来也不相等,因为比较只有在同一个加载器下才有意义。
双亲委派模型:
如果一个类加载器收到了加载请求,不会自己尝试去加载,委派给父加载器,一直到顶层,只有顶层反馈不能加载时,子加载器才会尝试去加载。
要求自定义类加载器要有自己的父加载器(启动类RT.JAR-扩展类EXT)
目的:保证java最基础的行为,如Object类是同一个类
并发
线程在操作系统中的实现
- 轻量级进程和用户线程混合
- 使用多个轻量管理多个用户线程 或 1个轻量管理1个或多个
好处:可以开很多线程,不受到内核数影响,不易使整个进程阻塞;切换消耗低,不要频繁系统调用。.
线程安全的实现方法:
- 互斥同步(阻塞同步)悲观
信号量,lock临界区unlock。
java中synchronized、reentrantlock实现同步。阻塞和唤醒一个线程需要操作系统来帮忙,由用户态转换到核心态中的时间有可能比用户执行代码的时间还长,是重量级的操作。 - 非阻塞同步
先操作,如果没有其他线程争用共享数据就操作成功;如果有那就不断地重试(冲突检测)。
指令集原子操作CAS(UNSAFE类):V原值,A预期值,B新值。如果V=A,那么用B更新V,返回旧V值,否则不执行更新。 - 无同步方案
线程本地存储,threadlocal,线程独享变量
锁优化
- 自旋锁和自适应自旋
互斥同步中阻塞引起线程挂起和恢复是要转入内核态完成,对性能影响很大。而自旋就是让等待的线程不放弃自己的执行时间,执行一个忙循环等待别人释放锁。适应性自旋就是如果上次等待成功可以就让他多等几次,如果失败就少等待几次,避免浪费资源。 - 锁消除
虚拟机检测到变量不会被别的线程访问到,就把他当做栈上私有的,消除锁。 - 锁粗化
同一个对象即使没有竞争也连续加锁解锁(如加锁在循环体内),很浪费性能,虚拟机自动扩大他的加锁范围(自动把锁粗话到循环外)。 - 轻量级锁
CAS操作在对象头标记,代表获得锁
先使用cas获取锁对象;如果存在两个以上线程争用同一个锁,则升级为重量级锁,阻塞线程。绝大数锁在同步周期内不存在竞争。 - 偏向锁
如果在第一个获取锁的线程在执行过程中不存在竞争,那就不同步了;否则转为轻量锁