导图社区 JVM-Java虚拟机
JVM深度知识点。介绍了JVM内存模型、JVM类型加载机制、垃圾收集机制、执行子系统、JDK各版本的区别等方面的内容。
编辑于2022-04-17 16:51:10JVM
JVM内存模型
运行时区域
程序计数器
线程私有
执行java方法时,存的是语句地址
执行native方法时,为null
没有任何oom异常情况
(java栈)虚拟机栈
线程私有
为虚拟机执行java方法服务
栈帧
栈描述的是Java方法执行的内存模型。 每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出】 栈帧: 是用来存储数据和部分过程结果的数据结构。 栈帧的位置: 内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> here[在这里] 栈帧大小确定时间: 编译期确定,不受运行期数据影响。
局部变量表
一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。 局部变量表所需要的内存空间在编译期完成分配。
操作数栈
动态链接
方法出口信息
两种异常:stackOverflowError,outOfMemoryError
本地方法栈
为虚拟机执行native方法服务
功能与虚拟机栈类似
同样有虚拟机栈的两个异常
java堆
java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。 即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序) 逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。
java内存中最大的一块
所有线程共享
虚拟机启动时创建
唯一目的是存放对象实例
垃圾收集器管理的主要区域
方法区
存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
所有线程共享
运行时常量池
是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。
在老版jdk,方法区也被称为永久代
因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。
jdk8真正开始废弃永久代,而使用元空间(Metaspace)
直接内存
并非虚拟机运行时数据区的一部分
通诺Native函数分配的内存、通过DirectByteBuffer引用操作
会抛出oom异常
hotspot虚拟机对象
对象的创建
1.执行new
检查
1.检查这个指令参数是否能在常量池中定位到一个类的符号引用。 2.检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,则必须先执行类的加载过程。
分配内存
所需内存在类加载完成后便可完全确定
指针碰撞
若Java堆的内存是绝对规整的,即用过的内存放一边,空闲的内存放另一边,中间用指针作为分界点隔开,那么内存分配时仅仅是把中间指针向空闲空间那边移动一段与对象大小相等的距离即可,这种内存分配方式称为“指针碰撞”(Bump the pointer)
空闲列表
若Java堆的内存并不规整,用过的内存和空闲的内存相互交错,那么虚拟机必须维护一个列表来记录哪些内存区是可用的,在分配时找一块足够大的区域给对象实例,并更新列表,这种分配方式称为“空闲列表”(Free List)
并发处理
对分配内存空间的动作进行同步处理
把内存分配的动作按照线程划分在不同的空间中进行
2.执行init方法
对象的内存布局
对象头(Header)
对象头又包括两部分信息,第一部分包括自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、自身持有的锁、偏向线程ID、偏向时间戳等,另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例
实例数据(Instance Data)
实例数据部分是对象真正存储的有效信息,也是程序中定义的各种类型的字段内容。存放对象程序中各种类型的字段类型,不管是从父类中继承下来的还是在子类中定义的。 分配策略:相同宽度的字段总是放在一起,比如double和long
对齐填充(Padding)
对齐填充仅仅起占位符的作用,不是必然存在的。HotSpot中要求对象的起始地址必须是8字节的整数倍。
对象的访问定位
句柄
简单来说就是java堆划出一块内存作为句柄池,引用中存储对象的句柄地址,句柄中包含对象实例数据、类型数据的地址信息。 优点:引用中存储的是稳定的句柄地址,在对象被移动【垃圾收集时移动对象是常态】只需改变句柄中实例数据的指针,不需要改动引用【ref】本身。
直接指针
与句柄访问不同的是,ref中直接存储的就是对象的实例数据,但是类型数据跟句柄访问方式一样。 优点:优势很明显,就是速度快,相比于句柄访问少了一次指针定位的开销时间。【可能是出于Java中对象的访问时十分频繁的,平时我们常用的JVM HotSpot采用此种方式】
OutOfMemoryError异常
Java堆溢出
虚拟机栈和本地方法栈溢出
方法区和运行时常量池溢出
本地直接内存溢出
JVM类加载机制
虚拟机将描述类额数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机使用的Java类型。
7步生命周期
加载
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据
在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
验证
连接阶段第一步,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全
会抛出java.lang.verifyError异常或其子异常
文件格式验证
验证字节流是否符合Class文件格式规范,并且能不当前的虚拟机处理。
元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。
字节码验证
最复杂,通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的。
符号引用验证
对类自身以外(常量池的中各种符号引用)的信息进行匹配性验证。
准备
正式为类变量分配内存并设置类变量初始化值
解析
顺序可在初始化之后
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
类或接口的解析
字段解析
类方法解析
接口方法解析
初始化
遇到new getstatic putstatic和invokestatic指令时会触发初始化
使用java.lang.reflect包对类进行反射调用时,触发初始化
初始化类时,首先初始化其父类
虚拟机启动时,初始化主类及包含main方法的类
<clinit>
<clinit>方法是收集类变量赋值动作与静态语句块合并产生的
<clinit>方法会先与子类的方法执行
<clinit>对类和接口不是必须,接口不能使用静态语句块
<clinit>虚拟机会确保该方法在多线程环境中被正确的加载和同步
使用
卸载
类加载器
四种类加载器
启动类加载器:Bootstrap ClassLoader
负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。 由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 有JVM用c++实现,因而获取System.class.getClassLoader时返回null。
扩展类加载器:Extension ClassLoader
是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。 它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。 开发者可以直接使用标准扩展类加载器。
应用程序类加载器:Application ClassLoader
是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。 它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。 由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器。
自定义类加载器:User ClassLoader
双亲委派模型
该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。 首先委派父类加载器去加载某个类。 只有当父类加载器找不到时,子类才自己去加载。
工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归。 因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
好处
使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类 java.lang.Object,它存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。 相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。
Java类随着它的类加载器一起具备了一种带有优先级的层次关系
防止内存中出现多份同样的字节码
3次大打破
双亲委派模型出现前,即JDK1.2之前
java.lang.ClassLoader添加了一个新的proceted方法findClass()。 在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。 JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
模型自身缺陷导致
双亲委派模型让越基础的类由越上层的加载器进行加载,但如果基础类又要调用用户的代码,那该怎么办呢。 例如JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办? 为了解决这个困境,Java设计团队只好引入线程上下文件类加载器(Thread Context ClassLoader)。 这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。 线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型。 Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
用户对程序动态性的追求导致
例如OSGi的出现,在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
OSGI对类加载器的使用
1)将以java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的Class Path,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类查找失败。
垃圾收集机制
判定对象是否存活
引用计数算法
给对象中添加一个引用计数器,每当有地方引用它时,引用计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象是不可能在使用的对象。
可达性分析算法
这个算法的基本思路就是通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
垃圾收集算法
标记-清除法
1.效率问题,标记清除两个过程的效率并不高;
2.问题,标记清除后会产生大量的内存碎片;
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。 商业的虚拟机都采用复制算法来回收新生代。因为新生代中的对象容易死亡,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。每次使用 Eden 和其中的一块 Survivor。 当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。Hotspot 虚拟机默认 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存是会被“浪费”的。
1.简单高效,不会有内存碎片
2.耗内存
标记-整理算法
标记过程仍然与“标记-清除”算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
将对象分为新生代和老年代,新生代采用复制算法,老年代采用标记-清除算法或标记-整理算法。
分代策略
新生代:1/3
Eden:8
From Survivor:1
To Survivor:1
老年代:2/3
永久代:Java8后取消,被元数据去取代
Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用 本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。
垃圾收集器
Serial收集器:单线程,需停掉其他收集器
最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。 虽然Serial看起来很坑,需停掉别的线程以完成自己的gc工作,但是也不是完全没用的,比如说Serial在运行在Client模式下优于其它收集器[简单高效,不过一般都是用Server模式,64bit的jvm甚至没Client模式] 优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】 缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。
ParNew收集器:多线程
可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。 优点: 1.支持多线程,多核CPU下可以充分的利用CPU资源 2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】 缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。
Parallel Scavenge收集器:复制算法、多线程、吞吐量优先
采用复制算法的收集器,和ParNew一样支持多线程。 但是该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】 被称为吞吐量优先收集器
新生代收集器
Serial Old收集器:单线程、标记整理算法
和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。 如果是Server模式有两大用途 1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。 2.作为CMS收集器的后备。
Parallel Old收集器:多线程、标记整理算法、吞吐量优先
支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"【老年代的收集器大都采用此算法】 Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。
cms收集器:标记清除算法、重视响应、对cpu敏感、无法收集浮动垃圾
CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】 总体上CMS是款优秀的收集器,但是它也有些缺点。 1.cms堆cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。 2.cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC 3.由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)
老年代收集器
G1收集器:标记整理/复制算法、注重响应、用于替代cms
G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。 用到的算法为标记-清理、复制算法
内存分配与回收策略
对象优先在eden分配
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
空间分配担保
JAVA引用类型
强引用
在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即 使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之 一。
软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它 不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存
虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚 引用的主要作用是跟踪对象被垃圾回收的状态。
GC概念
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
Major GC
Major GC 是清理永久代
Full GC
Full GC 是清理整个堆空间—包括年轻代和永久代
触发情况
System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc()。
老年代空间不足
让对象在Minor GC阶段被回收; 让对象在新生代多存活一段时间及不要创建过大的对象及数组。
方法区空间不足
class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,方法区可能会被占满
采用CMS GC
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。 promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的; concurrent mode failure是在执行CMS GC的同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC); 对应解决办法为:增大survivor space、老年代空间或调低触发并发GC的比率。
增大方法区空间
执行子系统
类文件结构
class文件的结构
魔数与class文件版本
常量池
访问标志
类索引、父类索引与接口索引集合
字段表集合
方法表集合
属性表集合
字节码指令简介
字节码与数据类型
加载和存储指令
运算指令
类型转换指令
对象创建于访问指令
操作数栈管理指令
控制转移指令
方法调用和返回指令
异常处理指令
同步指令
公有设计和私有实现
虚拟机字节码执行引擎
运行时栈帧结构
定义
存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息
每个方法从开始至执行完成的过程,都对应一个栈帧在虚拟机里面从入栈到出栈的过程
局部变量表
操作数栈
动态连接
方法返回地址
附加信息
方法调用
解析
分派
动态语言支持
基于栈的字节码解释执行引擎
JDK各版本的区别
常量池的存放
jdk1.6和1.7,常量池存放于堆中的方法区
jdk1.8,常量池存放于元数据区
jdk5
自动装箱与拆箱
枚举
静态导入
可变参数
泛型
For-Each循环
变长参数
int sum(int ...intlist)
jdk6
Desktop类和SystemTray类
使用JAXB2来实现对象与XML之间的映射
使用Compiler API
轻量级Http Server API
插入式注解处理API(Pluggable Annotation Processing API)
jdk7
多语言支持
Java7的虚拟机对多种动态程序语言增加了支持,比如:Rubby、 Python等等。
switch中可以使用字符串
"<>"这个玩意儿的运用List<String> tempList = new ArrayList<>(); 即泛型实例化类型自动推断。
新增一些取环境信息的工具方法
安全的加减乘除
jdk8
引入HashMap中的红黑树:HashMap中链长度大于8时采取红黑树的结构存储。
ConcurrentHashMap改进:底层采用node数组+链表+红黑树的存储结构,通过CAS算法(乐观锁机制)+synchronized来保证并发安全的实现。
JDK1.7:ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment实际继承自可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,每个Segment里包含一个HashEntry数组,我们称之为table,每个HashEntry是一个链表结构的元素。初始化有三个参数:initialCapacity:初始容量大小 ,默认16。loadFactor, 扩容因子,默认0.75,当一个Segment存储的元素数量大于initialCapacity* loadFactor时,该Segment会进行一次扩容。concurrencyLevel 并发度,默认16。 JDK1.8:已摒弃Segment的概念,直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,是优化过且线程安全的HashMap。在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。 JDK1.8取消了segment数组,直接用table保存数据,锁的粒度更小,减少并发冲突的概率; JDK1.8存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大; JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点); JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,已使用synchronized来进行同步,由于粒度的降低,实现的复杂度增加; JDK1.8使用红黑树来优化链表,红黑树的遍历效率是很快; JDK1.8使用内置锁synchronized来代替重入锁ReentrantLock。
接口中可以有默认方法与静态方法,也就是接口中可以有实现方法
Lambda 表达式
首先看看在老版本的Java中是如何排列字符串的: List<String> names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator<String>() { @Override public int compare(String a, String b) { return b.compareTo(a); } }); 只需要给静态方法 Collections.sort 传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。 在Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8提供了更简洁的语法,lambda表达式: Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); 看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短: Collections.sort(names, (String a, String b) -> b.compareTo(a)); 对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字,但是你还可以写得更短点: Collections.sort(names, (a, b) -> b.compareTo(a));
方法与构造函数引用
jdk9
JShell : 交互式 Java REPL
Linking
集合工厂方法
List<String> strings = List.of( "first","second");
G1是Java 9中的默认GC
模块化系统:模块化是一个很通用的概念。
jdk10
局部变量类型推断:局部变量类型推断将引入"var"关键字,也就是你可以随意定义变量而不必指定变量的类型.
原本需要这样: List <String> list = new ArrayList <String>(); Stream <String> stream = getStream(); 现在你可以这样: var list = new ArrayList <String>(); var stream = getStream(); 说到类型推断,从JDK 5引进泛型,到JDK 7的"<>"操作符允许不绑定类型而初始化List,再到JDK 8的Lambda表达式,再到现在JDK 10的局部变量类型推断,Java类型推断正大刀阔斧的向前发展。
GC改进和内存管理
JDK 10中有2个JEP专门用于改进当前的垃圾收集元素。 第一个垃圾收集器接口是(JEP 304),它将引入一个纯净的垃圾收集器接口,以帮助改进不同垃圾收集器的源代码隔离。 预定用于Java 10的第二个JEP是针对G1的并行完全GC(JEP 307),其重点在于通过完全GC并行来改善G1最坏情况的等待时间。G1是Java 9中的默认GC,并且此JEP的目标是使G1平行。
线程本地握手
JDK 10将引入一种在线程上执行回调的新方法,因此这将会很方便能停止单个线程而不是停止全部线程或者一个都不停。
备用内存设备上的堆分配
允许HotSpot VM在备用内存设备上分配Java对象堆内存,该内存设备将由用户指定。
其他Unicode语言 - 标记扩展(JEP 314)
目标是增强java.util.Locale及其相关的API,以便实现语言标记语法的其他Unicode扩展(BCP 47)。
基于Java的实验性JIT编译器
Oracle希望将其Java JIT编译器Graal用作Linux / x64平台上的实验性JIT编译器。
.删除工具javah
性能调优
常用参数
-XX:+PrintGCDetails(打印GC的详细信息)
-XX:+PrintGCTimeStamps(打印GC发生的时间戳)
-Xloggc:log/ge.log(指定GC.log的位置,以文件形式输出)
-XX:+PrintHeapAtGC(每一次GC后都打印出堆信息)
-Xmx(最大堆的空间)
-Xms(最小堆的空间)
-Xmn (设置新生代的大小)
-XX:NewRatio(设置新生代和老年代的比值,如果设置为4则表示(eden+from(或者叫s0)+to(或者叫s1)): 老年代 =1:4),即年轻代占堆的五分之一
-XX:SurvivorRatio(设置两个Survivor(幸存区from和to或者叫s0或者s1区)和eden区的比),8表示两个Survivor:eden=2:8,即Survivor区占年轻代的五分之一
-Xss设置栈
-XX:PretenureSizeThreshold 设置对象超过多大时直接在旧生代分配,默认值是0。
-XX:MaxTenuringThreshold设置垃圾最大年龄。
-XX:HandlePromotionFailure设置值是否允许担保失败
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间, 如果大于,则此次Minor GC是安全的 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
JVM调优工具
jdk的命令行工具
jps:虚拟机进程状况工具
Java VirtualMachine Process Status Tool
jps -v:输出jvm参数
jps –l:输出主类或者jar的完全路径名
jps –q :仅仅显示java进程号
jstat:虚拟机统计信息监视工具:持续观察虚拟机内存中各个分区的使用率以及GC的统计数据
Java Virtual Machine statistics monitoring tool
:jstat -gc pid,显示gc的信息
S0C:第一个幸存区的大小 S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小 EC:伊甸园区的大小 EU:伊甸园区的使用大小 OC:老年代大小 OU:老年代使用大小 MC:方法区大小 MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间
jstat -gccapacity:可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小
NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量 S0C:第一个幸存区大小 S1C:第二个幸存区的大小 EC:伊甸园区的大小 OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:当前老年代大小 OC:当前老年代大小 MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小 CCSMN:最小压缩类空间大小 CCSMX:最大压缩类空间大小 CCSC:当前压缩类空间大小 YGC:年轻代gc次数 FGC:老年代GC次数
jstat -gcnew pid:新生代回收统计
jstat -gcnewcapicity pid:新生代回收统计
jstat -gcold pid:老年代回收统计
jstat -gcmetacapacity pid:元数据空间统计
jinfo:java信息配置工具
jmap:java内存映像工具,查看堆内存使用情况
Java Virtual Machine Memory Map
jmap -dump:live,format=b,file=myjmapfile.txt 19570:将内存使用的详细情况输出到文件,MAT分析工具,如jhat命令,eclipse的mat插件
用jhat命令可以参看 jhat -port 5000 heapDump 在浏览器中访问:http://localhost:5000/ 查看详细信息
jmap -finalizerinfo 3772:打印正等候回收的对象的信息
jmap -histo:live pid>a.log:以观察heap中所有对象的情况(heap中所有生存的对象的情况)
jmap -histo:live 这个命令执行,JVM会先触发gc,然后再统计信息。
jhat:虚拟机堆转储快照分析工具
jstack:虚拟机堆栈跟踪工具:查看Java进程内的线程堆栈信息
hsdis:jit生成代码反汇编
jdk的可视化工具
jconsole:java监视与管理控制台
visualvm:多合一故障处理工具
阿里巴巴调优工具Arthas
GC日志
GCEasy日志分析工具使用
GCViewer日志分析工具使用
Java内存模型
主内存和工作内存
定义程序中各个变量的访问规则,即虚拟机将变量存入内存和从内存中取出的底层细节
所有变量都存储在主内存中
每条线程还有自己的工作内存,线程对变量的所有操作都在工作内存中进行
内存间交互操作
操作
lock(锁定)
unlock(解锁)
read(读取)
load(载入)
use(使用)
assign(赋值)
store(存储)
write(写入)
规则
1、不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
4、一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
5、一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7、如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
volatile关键字
可见性
不满足此规则时需加synchronized或concurrent关键字
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
变量不需要与其他的状态变量共同参与不变约束
对long和double型变量的特殊规则
原子性、可见性和有序性
基本数据类型的访问是原子性的,long和double例外
volatile、synchronized、final可实现可见性
volatile与synchronized可实现有序性
先行发生原则
Java内存模型的先天有序性。
程序次序规则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
管程锁定规则
一个unLock操作先行发生于后面对同一个锁的lock操作
volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作
线程启动规则
Thread对象的start()方法先行发生于此线程的每个一个动作
线程终止规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
对象终结规则
个对象的初始化完成先行发生于他的finalize()方法的开始
传递性
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C