时时更新,假装很厉害(其实就是逼自己坚持下去···)我不生产程序 我是程序的搬运工
Part1 内存模型
1.运行时数据区域
- 程序计数器:空间小,线程私有,当前线程所执行的字节码的行号指示器。调用Native方法计数器的值为Undefined。唯一一个没有定义OutOfMemoryError情况的区域。
- Java虚拟机栈:线程私有,描述java方法执行的内存模型:每个方法执行创建栈帧,存储局部变量、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:本地方法即Native方法,
除次外与Java虚拟机栈几乎一一致,有些虚拟机(HotSpot, java8默认虚拟机)直接将两栈合二为一。 - java堆:线程共享,启动时创建,目的是存放对象实例,方便GC回收。
- 分代收集算法:分为新生代和老年代,再细分有Eden空间、Form Survivor空间、To Survivor空间等。
- 方法区:多线程共享,存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。java堆的一个逻辑部分,别名Non-Heap(非堆),目的与java堆分开。在HotSpot JVM中大家称为永久代,其他虚拟机不存在永久代概念,HotSpot也正在转变。
- 运行时常量池:方法区的一部分,用于存放编译期生成的各种字面量和符合引用。
- 直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。使用场景NIO,Native函数库直接分配堆外内存,然后存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了java堆与Native堆来回复制数据。
Part2对象创建
1.对象创建:
- new
- 检查指令参数是否可定位到类的符号引用
- 检查此类是否被加载、解析和初始化
- 虚拟机分配内存
- 若内存绝对规整,则采用指针碰撞(指针向非空闲区域挪动对象需要的大小)否则采用空闲列表(在已用空间的空闲区域中找到足够大的内存),规整与否由GC是否带压缩整理功能决定(Serial、ParNew等采用Mark-Compact算法带Compact,CMS基于Mark-Sweep算法采用后者)
- 保证原子性:
1.CAS+失败重试
2.TLAB(Thread Local Allocation Buffer)本地线程分配缓冲,保证更新操作的原子性。
- 初始化内存空间为零值,若TLAB则在TLAB分配时执行。
- 初始化对象实例(类元数据信息、对象哈希码、对象GC分代年龄等)
- 执行对象init方法(目测就是初始化变量值)
2.对象内存布局:
- 对象头(Header)
- Mark Word对象自身运行时数据(哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等)
- 类型指针,指向它的类元数据的指针。
- 实例数据(Instance Data)
- 对齐填充(Padding)
- 并不是必然存在,HostSpotVM对象地址必须是8字节的整数倍,所以需要对其。
3.对象访问定位:
-
句柄访问:
-
直接指针访问:
Part3 GC
1.对象的存活状态:
- 引用计数算法:主流的Java虚拟机中都未使用这一算法。原因:当A对象引用B,B对象引用A,AB就计数就一直不为0也就不会被回收。
- 可达性分析算法: Java、C#、Lisp都用这以算法。思路:从“GC Roots”作为起点,当一个对象没有路径与GC Roots相连则证明这个对象无引用。
2.引用:JDK1.2之后引用分为四种(由强到弱)
- 强引用String Reference
- 软引用Soft Reference
- 弱引用Weak Reference
- 虚引用Phantom Reference
3.死亡标记:对象死亡至少需要两次标记,一次是可达性分析算法的标记,放入F-Queue队列,然后等待队列调用finalize方法,执行后依旧没有引用则被再次标记,然后回收。finalize只会被调用一次。
4.回收方法区:主要回收内容废用常量(例如String),无用的类。常量判断方式与堆中差不多。无用类的判断有三点:
- 类的实例都被回收,堆中无任何实例。
- 该类的ClassLoader被回收。
- 类的类类型无处引用,即Object.class。
至于是否被回收还需要看启动参数等等因素。
5.垃圾收集算法:
- 标记-清除 Mark-Sweep:先标记,再统一回收,最基础的算法,两个问题,一个是效率第,一个是空间零碎。
- 复制算法Copying:将内存按容量划分大小相等的两块,每次使用其中一块,回收时将存活对象复制到另一半,然后全部回收,再将活着的对象复制回来。代价内存折半。实际在HotSpot中Eden和Survivor比例为8:1,可用内存在整个新生代中为90%,剩下的10%用来收集复制。对象存活率较高时不采用此方式。
- 标记-整理 Mark-Compact,与标记删除一样,标记整理是指标记后存活对象都向一端移动,然后清理掉边界以外的内存。
- 分代收集算法:大多数垃圾收集的算法,根据对象的存活周期不同将内存划分为几块,一般为新生代和老年代,新生代收集会有大批对象死去,用复制算法,老年代对象存活率高,用标记-清理或标记-整理。
6.垃圾收集器
- 复制算法:
- Serial收集器 需要stop the world,单个线程
- ParNew收集器 上一个的多线程版本
- Parallel Scavenge收集器 目标是控制吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)
- 标记整理算法:
- Serial Old收集器 老年代版本Serial收集器,与Parallel Scavenge收集器搭配使用,或是CMS收集器的后备方案
- Parallel Old收集器 老年代版本Parallel Scavenge收集器 Parallel Scavenge收集器的好搭档
- 标记清除算法
- CMS收集器 并发标记并发清除
- 复制算法+标记整理算法
- G1 Garbage-First最先进的收集器,不需要与其他收集器配合使用。可预测停顿,可整合空间。(-XX:+UseG1GC)
7.对象分配流程:
- 优先在Eden区中,空间不够进行gc然后再不够则将其他对象放至老年代。
- 大对象直接进入老年代。
- 长期存活对象进入老年代:给对象一个年龄,第一次Minor GC 仍然存活,则将其从Eden中移动到Survivor中,并且年龄加1,当年龄加到一定程度(默认15岁)晋升到老年代。
8.java虚拟机工具:
-
jps:虚拟机进程状况
-
jstat:虚拟机统计信息监控工具
(参数2147为pid)
-
jinfo:java配置信息工具
-
jmap:java内存映像工具
-
jhat:虚拟机堆转存快照分析工具
-
jstack:java堆栈追踪工具
-
jconsole:java监视与管理控制台
Part4实际分析
1.高性能机器部署:
问题:16G内存,java堆大小分配12G,一次GC高达14s,因为大对象比较多,大部分大对像都进入到了老年代,所以GC回收不掉这些大对象,造成十几分钟就十几秒的GC停顿。
解决方案:分配小一点内存,一台机器上部署多个服务。
2.集群间同步导致内存溢出:
问题:高并发读写操作,导致集群间数据疯狂同步,同步失败后还需要同步重试,服务器间通讯,消耗大量内存资源。
解决方案:书里没说,个人觉得这种高并发读写就不应该这种方式实现集群,生产者消费者的模式来减缓同步压力。
3.堆外内存导致的溢出错误:
问题:平台限制内存2G,虚拟机分配内存1.6G,当加大内存的时候内存溢出现象更加严重,jstat时候GC不频繁,个区域稳定。
解决:问题定位,由此可推算,直接内存区大小为0.4G,Direct Memory虽然也能被Full GC回收,但是它不能通知GC回收。大量NIO操作,需要用DM。分配DM空间-XX:MaxDirectMemorySize
4.外部命令导致系统缓慢:
问题:系统调用大量脚本,命令行等,方式来获取系统一些信息。
解决:大量的这种操作需要消耗很多资源,系统需要产生新的进程,建议使用java api的方式获取这些信息。
5.服务器JVM进程崩溃
问题:运行期间服务器频频出现自动关闭的现象,生产hs_err_pid.log,一个服务调用另一个服务,两个服务处理速度差距较大,导致多个线程等待,最终服务崩溃。
解决:使用生产者消费者模式,消息实现队列。
6.不恰当的数据格式导致内存占用过大。
7.由windows虚拟内存导致的长时间停顿。
(Class文件描述的各种信息略掉了。)
Part5虚拟机类加载机制
1.类生命周期:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段
2.五种类初始化时机:
new、getstatic、putstatic、invokestatic指令时类没有进行初则需要进行初始化。
使用反射调用的时候,类没进行初始化就要先初始化。
初始化一个类的时候,父类没调用就需要触发父类初始化。
虚拟机启动的时候,用户需要指定执行的主类,虚拟机会先初始化这个类。
当使用JDK1.7的动态语言的支持的时候,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,这个方法句柄对应的类没有进行初始化的时候。
3.加载阶段虚拟机动作:
- 通过类全名获取二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时的数据结构。
- 在内存中生成这个类的的java.lang.Class对象,作为方法区这个类的访问入口。
4.验证:
文件格式验证(魔数、版本、类型、引用、编码、md5)
元数据验证(是否由父类,是否继承了不允许被继承的类(final)
字节码验证(数据类型与指令代码序列都能配合工作,跳转指令不会跳到方法外,保证类型转换有效)
符号引用验证(引用能否找到对应的类,能否找到合法的方法和字段,字段访问性是否可被当前类访问)
5.准备(初始化类变量,类变量赋初值)
6.解析
- 类或接口解析
- 字段解析
- 类方法解析
- 接口方法解析
7.初始化
- 静态初始化块能赋值之后的静态变量,但无法访问。
8.同一个类加载器加载的类才是相等的,否则instanceof也返回false
9.类加载器:
- 启动类加载器(Bootstrap ClassLoader):C++实现,加载rt.jar等基本文件
- 扩展类加载器(Extension ClassLoader)负责加载<java_home>\lib\ext 目录中的依赖。
- 应用程序类加载器(Application ClassLoader):系统类加载器,加载classpath上的类库
Part6 虚拟机字节码执行引擎
1.栈帧:虚拟机运行时数据区中的虚拟机栈的栈元素。
2.栈帧需要分配多少内存不会受程序运行期变量数据的影响。
3.栈帧结构:
- 局部变量表:最小单位为槽(Variable Slot)每个槽能存储的类型有八种:(boolean、byte、char、short、int、float、reference、returnAddress)对于64位数据类型以高位对齐的方式分配两个连续的Slot。0位索引为this
- 操作数栈:LIFO,每个元素可以是任意的java类型,虚拟机都是基于栈的执行。
- 动态连接
- 方法返回地址
- 附加信息
4.方法重载Overload优先级
先输出 char,注掉char后输出int注掉int输出long注掉long输出Character 注掉Character输出Serializable注掉Serializable输出object
顺序是先转型,再装箱,再接口,再父类,最后可变长
Part7 内存模型与线程
1.评价服务性能:每秒事务处理数TPS(Transactions Per Second)
2.内存间交互操作
- lock
- unlock
- read
- load
- use
- assign
- store
- write
3.八种原则:
- read和load,store和write必须成对出现R->L S->W
- 最后一次assign后必须写回内存A->S->W
- 未assign操作的数据不允许同步到主内存中A->S->W
- 一个变量在同一时刻只允许一个线程lock,并且可以被lock多次,但必须unlock相同次数
- 如果对变量lock 会清空内存中此变量的值,在执行load或assign操作初始化变量的值。
- 没lock不许unlock,不许unlock其他线程lock的变量
- unlock前必须store,write U->S->W
- volatile
- 确保了数据存取的原子性(32位环境读取64位变量分两次,先读高32位后读低32位)
- 保证了数据的可见性
- 禁止了指令重排序
误区:- 自增操作(i++) 不具备原子性,即本操作是三步
- 从主存储中读取i的值到高速缓存
- 在高速缓存中对i进行算数运算
- 将结果从高速缓存写入主存储
- 不具有原子性的操作volatile无法保证其准确性
- 当a线程读取i并进行加法运算,此时b线程进行读取i操作,此时a并未将i写入主存储,所以b读到的还是旧的值。
- 自增操作(i++) 不具备原子性,即本操作是三步
- 想保证上一操作的原子性则需要在运算方法上加入synchronized关键字,或者使用Lock 对象锁的方式来保证同一时间仅有一个线程可以调用该方法。或是使用原子对象自增AtomicInteger.
5.天然的先行发生的关系:
- 程序次序规则:按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作(同一个锁)
- volatile变量规则:volatile变量的写操作优先发生于这个变量后面的读操作。
- 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作。
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
- 线程终端规则
- 对象终结原则
- 传递性
6.线程状态:
- 新建
- 运行
- 无限期等待
- 限期等待
- 阻塞
- 结束