www1350 / javaweb

http://www1350.github.io/
31 stars 5 forks source link

基础概念 #105

Open www1350 opened 7 years ago

www1350 commented 7 years ago

基本概念

JDK:包括编译器(javac.exe)、开发工具(javadoc.exe、jar.exe、keytool.exe、jconsole.exe)和更多的类库(如tools.jar)等。总结就是Java语言、Java虚拟机、Java API类库 JRE: 支持java运行的基本环境

image

JIT:just in time

运行时数据区: image

JVM的内存结构

程序计数器:

当前代码行号指示器。各个线程计数器互不影响,独立存储。如果是执行Java方法,是虚拟机字节码地址。如果是native方法,计数器为空。唯一一个没有OOM的区域。是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。

Java虚拟机栈:

描述Java方法执行的内存模型:每个执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法调用到执行完成过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。线程私有

本地方法栈:

为虚拟机使用的Native方法服务。一样会抛出StackOverflowError和OutOfMemoryError。线程私有

Java堆:

存放对象实例,被所有线程共享。所有对象实例以及数组都要在堆上分配,但随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致所有对象都在堆上分配也不是那么绝对。从内存分配看,可以划分出多个线程私有分配缓冲区。

方法区:

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(各个线程共享)。方法区是堆的一个逻辑部分,但是他有个别名非堆,为了和Java堆区区分。

直接内存:

JDK1.4引入的NIO,引入了一种基于通道与缓冲区的I/O方式,可以使用native函数库直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样避免Java堆和native堆中来回复制数据。(内存受到RAM、SWAP或分页大小以及处理器寻址空间的限制)

对象的创建:

new的时候:1.检查指令参数是否能在常量池中定位到一个类的符号引用2.检查这个符号引用代表的类是否已被加载、解析、初始化过3.有就执行相应类加载过程4.类加载检查通过后,虚拟机为新生对象分配内存(假设Java堆内存绝对规整,用过的一边没用过的一边,中间放着指针作为分界点的指示器,指针则挪动和对象大小的距离,这叫做“指针碰撞”;假如不规整,已使用和空闲交错,虚拟机就必须维护一个列表,记录哪些内存块可用,在分配的时候从列表找到一个足够大的空间划分给对象实例,并更新列表上的记录,这叫做“空闲列表”;使用Serial、ParNew等带Compact过程收集器,采用指针碰撞,使用CMS基于Mark-Sweep算法收集器,采用空闲列表)

并发情况下不是线程安全的,存在正在给A分配内存,指针还没来得及修改,对象B又同时使用原来指针来分配内存。有两种解决方法,一、对分配内存空间的动作进行同步处理——虚拟机采用CAS+失败重试保证更新原子性;二、按内存分配动作按线程划分不同空间进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(TLAB)。哪个线程要分配,就在TLAB上分配,用完才分配新的,才需要同步锁定。(-XX:+/-UseTLAB)

内存分配后,会初始化内存的零值。接下来,虚拟机对对象进行必要设置,存放信息(对象是哪个类实例、如何找类元信息、哈希码、GC分代年龄)在对象头。从虚拟机角度对象已经创建,从java角度对象才刚刚开始创建init还没开始执行,所有字段为0。执行new指令以后接着执行init(invokespecial) image

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头

对象头包括两部分信息:运行时数据和类型指针。

运行时数据

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

32bit中25bit存储对象哈希码,4bit存储对象分代年龄,2bit存储锁标志位,1bit固定为0 image

类型指针

对象头另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 (并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考对象的访问定位)

数据类型

这部分分配顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。

对齐填充

HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

对象访问定位

Java程序需要通过栈上的引用数据来操作堆上的具体对象。对象的访问方式取决于虚拟机实现,目前主流的访问方式有使用句柄和直接指针两种。

句柄

可以理解为指向指针的指针,维护指向对象的指针变化,而对象的句柄本身不发生变化;指针,指向对象,代表对象的内存地址。

image

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针

如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而引用中存储的直接就是对象地址。

image

优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。

垃圾回收算法

引用计数法

原理:给每个对象添加一个计数器,被引用计数器就加一;引用失效就减一;计数器为0就对象回收 缺陷:两对象之间循环引用则无法回收

可达性分析

原理:通过一系列“GC Roots”对象称为起始点,从这些结点开始向下搜索,搜索走过的链路称为引用链路,当GC不可达的时候会被判定可回收。

Java里,可以作为GC Roots对象的有:

1.虚拟机栈(栈帧本地变量)引用的对象

2.方法区中类静态属性引用的对象

3.方法区中常量引用的对象

4.本地方法栈(JNI)引用的对象

引用

垃圾回收算法

image

新生代和老年代的回收策略

Eden区满后触发minor GC,将所有存活对象复制到一个Survivor区,另一Survivor区存活的对象也复制到这个Survivor区中,始终保证有一个Survivor是空的。当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖Old区进行空间分配担保机制, 这部分内存直接进入Old区。

Young区Survivor满后触发minor GC后仍然存活的对象存到Old区,如果Survivor区放不下Eden区的对象或者Survivor区对象足够老了,直接放入Old区,如果Old区放不下则触发Full GC。

永久代-方法区回收

在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量无用的类。 回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:

但即使满足以上条件也未必一定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 因此在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项, 开启VM的类卸载功能, 以保证方法区不会溢出。

空间分配担保

在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).

然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).

枚举根结点 必须停顿所有Java线程(STW stop the world),采用OopMap数据结构检查所有上下文和全局的引用位置

安全点 每条指令都生成OopMap很耗费空间,实际上HotSpot没有为每条指令生成OopMap,而是只在特定位置记录,这些位置被称为“安全点”。(标准是 是否让程序长时间执行,如循环、方法调用等)。让GC在安全点停顿,HotSpot采用主动式中断(当GC需要中断线程的时候,只是标记,各个线程轮训这个标志,发现这个标志为真则挂起线程,和安全点重合则加上创建线程需要分配的位置)

新生代GC(Minor GC)

老年代GC (Major/Full GC)

新生代

老年代

与其他基于分代的收集器不同, G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合.

image

每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.