目录
一、Jdk源码解析过程
二、java虚拟机运行时数据区
1、Java虚拟机的五大分区
三、OutOfMemory异常实践(OOM)
1、Java堆溢出
2、虚拟机栈和本地方法栈溢出
3、方法区和运行时常量池溢出
4、本机直接内存溢出
四、垃圾回收
(1)堆的回收
(2)方法区的回收
(3)垃圾回收算法
(4)如何回收
(5)垃圾收集器(七种)
(6)内存分配回收策略
五、类(class)文件结构
1、Class文件的结构
六、java虚拟机类加载机制
1、虚拟机的启动
2、类加载的时机(类加载的过程)
3、类加载的过程
4、类加载器
七、早期优化(编译器的优化)
1、解析与填充符号表
2、注解处理器:对注解处理,JDK1.5之后。
3、语义分析和字节码生成
4、语法糖的应用
八、晚期优化(运行期的优化)
1、Hotspot的即时编译器
2、编译对象和触发条件
3、编译过程
九、Java内存模型
1、主内存和工作内存
2、Java与线程
3、Java线程调度
4、Java线程状态转化
十、线程安全与锁优化
1、Java语言中的线程安全
2、线程安全的实现方式
3、锁优化
首先来看一个图:
Java虚拟机的五大分区为:
下面来一一介绍
程序计数器(PC寄存器):
可以理解为当前线程所执行的字节码的指示器。
是线程私有的,每条jvm线程都有自己的程序计数器,各条线程互不影响,独立存储,是“线程私有”内存。
Java虚拟机栈
线程私有的,每个jvm线程都有自己的java虚拟机栈,与线程同时创建,生命周期和线程相同。
虚拟机描述的是java方法执行的内存模型:
每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈:
和虚拟机栈作用相似,虚拟机栈为虚拟机执行的java方法服务,本地方法栈为虚拟机用到的Native方法服务。
线程私有的
HotSpot直接把java虚拟机栈和本地方法栈合二为一。
Java堆
是虚拟机内存中最大的一部分
用来存储对象实例,所有对象技术组都要在这里分配内存
线程共享
虚拟机创建的时候创建
由于这块区域是线程共享的,里面存的数据不能随线程消亡而删除,所以这里的存储的对象实例以及数组要在这里被自动管理,也就是垃圾回收(GC)(Garbage Collector)。
方法区
方法区也是一块被各个线程共享的区域。
存储被虚拟机加载的类信息,常量,静态变量。
在虚拟机启动的时候被创建
运行时常量池
是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
类似于一个缓存区,避免了在Java堆和Native堆中来回复制数据。
当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
内存泄露:是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
(1)单线程:
①如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
②如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
(2)如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程了。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。
运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后文件很小,而程序中有直接或简介使用了NIO,那就可以考虑一下是不是这方面的原因。
由于程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,栈帧随方法进行入栈出栈操作。所以的这些区域的内存分配和回收具有确定性,不考虑垃圾回收问题。内存会随线程的结束而自动回收。
堆和方法区不一样,我们只有在程序运行期间才知道会创建哪些对象,这部分的内存分配和回收是动态的,垃圾回收关注的是这部分内存。
- 死掉的对象所占内存需要回收
- 判断对象是否死掉
1)引用计数法
- 每当有一个地方需要使用这个对象时,计数器加一,当引用失效的时候,计数器减一,任何时刻,计数器为0的对象都不会再被使用。
- Java中没有使用引用计数法,因为很难解决对象之间循环引用的问题。
2)可达性分析算法或者根搜索算法
- 即判定对象是否存活,即从“GC Roots”对象作为起始点,从这些节点向下搜索,搜索走过的路径叫做“引用链”,当一个对象到GC Roots 没有任何引用链,证明此对象是不可用的。
- 在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
3)对象的引用
- JDK1.2之前,认为如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用,这种太过狭隘。
- 在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
- 强引用:类似于“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:软引用用来描述一些还有用,但并非必需的对象。表示在java堆里面没有数据,但是在栈和方法区中有数据,比如:Object object= null;
- 弱引用:弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用的对象无论内存是否足够,都会被回收。例如:局部变量,返回值,参数。
- 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。比如:反射获取的对象,注解。
用来回收新生代,伊甸园区和两个幸存者区是8:1:1的关系。
枚举根节点:一次枚举根节点,数量庞大,逐个检查,会消耗很多时间。
安全点:由于GC必须停止进程,安全点就是,可以进行GC的点
安全区域:可以随时进行GC的区域
- Serial收集器(在GC日志中新生代的名称是DefNew)
- ParNew收集器(在GC日志中新生代的名称是ParNew)
- Parallel Scavenge收集器(在GC日志中新生代的名称是PSYongGen)
- Serial Old(MSC)收集器
- Parallel Old收集器
- CMS收集器
- G1收集器
- Magic
- 固定值,确定这个文件是否为一个能被虚拟机所接受的 Class 文件。
- minor_version、major_version
- 不同版本的java虚拟机支持的版本不一样。
- 高版本jvm向下兼容低版本的class文件。
- 常量池计数器
- 有几个常量池,就是几。
- constant_pool 表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是有效的。
- 使用索引 0 来表示“不引用任何一个常量池项”的意思。
- constant_pool[ ]
- 常量池:它包含 Class 文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。
- access_flags
- 用于表示某个类或者接口的访问权限及基础属性
- this_class
- 表示这个 Class 文件所定义的类或接口
- super_class
- 表示这个class文件的父类或父接口
- interfaces_count
- 表示当前类或接口的直接父接口数量。
- interfaces[]
- 表示这个类实现的接口(顺序:从左到右)。
- fields_count
- 类或接口的成员的个数
- fields[]
- 字段表,fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。
- methods_count
- 方法计数器,方法的个数。
- methods[]
- 方法表,methods[]数组,只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
- attributes_count
- 属性计数器,attributes_count 的值表示当前 Class 文件 attributes 表的成员个数。
- attributes[]
- 属性表
虚拟机的类加载机制,虚拟机吧描述类的文件从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被java虚拟机直接使用的java类。
说白了,就是把编译好的class文件加载到方法区中的各个部分,并且进行校验。
Java虚拟机的启动是有一个引导类加载器创建一个初始类来完成。
(1)类从被加载到虚拟机内存中到卸载出内存,整个生命周期:加载、连接、初始化、使用、卸载。其中连接分为验证、准备、解析。解析的顺序不一定,有可能按照上述顺序,也有可能在初始化阶段之后才开始,这是为了支持Java的运行时绑定(动态绑定)。如下图:
(2)对类进行初始化的时机:
(1)创建和加载
①Java 虚拟机支持两种类加载器:Java 虚拟机提供的引导类加载器(Bootstrap Class Loader)和用户自定义类加载器(User-Defined Class Loader)。
②加载需要完成:
1、通过类的完全限定名获取类的二进制字节流文件
也就是根据类名,获取class文件
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
也就是将class文件转化为方法区中jvm可以识别的数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
也就是说,生成了一个类的访问接口,class对象,记录了类成员,接口等信息,那么可以推测,是不是可以通过反射机制获取类信息了。
(2)连接和验证
①验证保证类或接口的二进制表示结构上是正确的。
②验证过程可能会导致某些额外的类和接口被加载进来(§5.3),但不应该会导致它们也需要验证或准备。
③验证阶段大体会进行如下阶段的检查:
- 文件格式验证:class文件是否符合规范
- 元数据验证:是否存在不符合java语言规范的元数据信息
- 字节码验证:保证验证类不会做出危害虚拟机安全的事件
- 符号引用验证:确保后续解析阶段符号引用正确
(3)连接和准备
(4)连接和解析
符号引用是指用符号来描述引用目标,符号可以是任何字面量。
直接引用,可以理解为引用他的内存地址
- 类与接口解析:jvm解析一个类
- 字段解析:jvm解析一个类的字段
- 类方法解析:jvm解析一个类方法:抛出异常:java.lang.IncompatibleClassChangeError,java.lang.AbstractMethodError,java.lang.NoSuchMethodError.的时机。
- 接口方法解析:
(5)初始化:初始一个类。
(6)使用
(7)退出
编译器的优化是在生成class文件的编译过程中的优化,发生在编译器上,生成class文件的时候。
Javac编译器的编译过程,基本可以按照下图来表示。
(1)词法,语法分析
(2)填充符号表
Java程序最初是解释执行的,后来,发现某个方法使用的非常频繁,就会认为这段代码是热点代码,运行时,会把这些代码编译成平台相关的代码,并进行各种层次的优化,完成这个任务的编译器为即时编译器(JIT)。
(1)hotspot同时存在解释器和编译器,如下图所示。
(2)HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器–Client 和C2编译器– Server(也叫Opto编译器)
(1)编译对象是“热点代码”
(2)判断一段代码是不是“热点代码”的方法:
Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存差异,来实现Java程序在各种平台下都能达到一致的内存访问效果。即:一套完整的java内存使用规则,内存的规则。
保证了:环境一致 内存一致 访问方式一致
(1)这里所讲的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分。
主内存主要对应java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分数据。从更低层次上说,主内存就直接对应于物理硬件的内存。
为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。
(2)内存之间的交互操作
①即,将一个数据在jvm中和硬盘中存入,取出的过程。见下图:
②分析
- lock(锁定):作用于主内存的变量,它把一个变量标志为一条线程独占的状态。
- unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存中的变量,它把store操作从主内存中得到的变量值放入主内存的变量中。
③volatile(关键字)型变量的特殊规则
(1)内核线程
(2)用户线程
(3)用户线程和轻量级线程混合使用
(4)Java线程的实现
Java线程在JDK 1.2之前是基于用户线程实现的,而JDK 1.2中,线程模型替换为基于操作系统原生线程来实现。后来变为混合实现。
抢占式调度,但是可以设置优先级,因为Java线程是通过映射到原生线程上来实现的,所以线程调度最终还是取决于操作系统。
- 新建(New):创建后尚未启动的线程处于这种状态。
- 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是说处于此种状态的线程可能正在执行,也可能正在等待CPU为它分配执行时间。
- 无限期等待(Waiting):处于这种状态下的线程不会被分配CPU执行时间,他们要等待被其他线程显示唤醒。
- 限期等待(Timed Waiting):处于这种状态下的线程也不会被分配CPU执行时间,不过无须等待被其他线程显示唤醒,在一定时间之后它们由系统自动唤醒。
- 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生。
- 结束(Terminate):已经终止的线程的线程状态,线程已经结束执行。
阻塞还在占用资源,等待不占用资源。
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
(1)不可变
(2)绝对线程安全
(3)相对线程安全
(4)线程兼容
(5)线程对立
*我们一般说的线程安全指的是:相对线程安全和线程兼容。
(1)互斥同步
(2)非阻塞同步
(3)无同步方案
(1)自旋锁和自适应自旋
(2)锁消除
(3)锁粗化
(4)轻量级锁
(5)偏向锁
总结:轻量级锁和偏向锁,并不是总是对程序运行有利的,当在经常出现冲突的程序中,反而增加了CAS操作和偏向操作,降低了运行效率。
参考资料:https://me.csdn.net/sinat_38259539(敬业的小码哥)
真正的平静,不是避开车马喧嚣,而是在心中修篱种菊。 ——白落梅 《你若安好 便是晴天》