Java SE体系架构
-
JAVA SE:java平台标准版,是JAVA EE和JAVA ME提供了基础。
-
JDK:JAVA开发工具包包,包含了JRE,及开发程序所需的编译器和调试等工具。
-
JRE:JAVA SE的运行环境,提供库,java虚拟机和其他组件来运行java编写的程序。
-
JVM:java虚拟机,屏蔽了JAVA SE 平台硬件和操作系统的方法差异,编译字节码文件。
JVM内存模型(运行时数据区)
共分为5大块:java虚拟机栈、本地方法栈、程序计数器、堆、方法区
-
java虚拟机栈:描述java方法运行过程的模型。由多个栈帧组成,每个栈帧会存储每个方法在运行时所需要的一些信息,这些信息包括:局部变量表、操作数栈、动态链接,方法出口信息及其他信息;
- 局部变量表:是一组变量存储空间,用于存放方法参数和方法局部变量,在方法被编译后就已经在方法的code属性max_locals确定了该方法所需的最大局部变量的容量。
- 局部变量的容量以变量槽为单位
- 32位虚拟机中的一个槽可以存一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)
- slot对对象的引用会影响到GC;
- 系统不会为局部变量赋初始值
- 栈的默认大小为1MB,可通过-Xss指定
- 特点:
- 局部变量随着栈帧的创建而创建,但是他的大小在编译期已经确定,运行时候不会改变;
- StackOverFlowError和OutOfMemoryError异常
- StackOverFlowError:线程请求栈的深度超过当前java虚拟机栈的最大深度会出现该异常;
- OutOfMemoryError:当线程请求栈时内存不够用,且无法动态扩展,会出现该异常;
- java虚拟机栈为线程私有的,其他线程不能访问,随着线程的创建而创建,线程的死亡而死亡;
- java虚拟机栈中的数据为私有的(包括基础类型及引用类型),其他栈不能访问;
- 栈中的数据在线程内部是共享的,这样可以节省空间,如int a=2;int b=2;这样他会只分配一个空间存储2;但是这个不同于两个对象的引用指向同一个对象,前者a的改变不会影响到b;
-
本地方法栈:本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法栈是本地方法运行的内存模型。区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机用到的Native方法服务
- 特点:
- 也会有StackOverFlowError和OutOfMemoryError异常;
- 线程私有;
- 特点:
-
程序计数器:程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址,如果当前线程执行的是一个本地方法,那么此时程序计数器为空。
- 特点:
- 线程私有;
- 是唯一不会出现OOM的内存区域;
- 生命周期随着线程的创建而创建,随着线程的死亡而死亡;
- 所占内存非常小
- 作用:
- 字节码解释器通过改变程序计数器依次读取指令,实现代码的流程控制;
- 多线程下,当线程切换到其他线程,其他线程可以继续上次的流程操作;
- 特点:
-
方法区:存储类信息、静态变量,即存储类的元数据,一旦类被使用,虚拟机就会对其进行装载、连接(验证
、准备、解析)和初始化,使得.class转变成方法区中一段特定的数据结构。
- 特点:
- 线程共享;
- 存储类信息、常量、静态变量;
- 有运行时常量池:存各种字面量和符号引用
- 因为方法区存储的信息共享,因此在用的过程中需要设计成线程安全的。
- 特点:
-
堆:存放对象实例
-
特点
-
java虚拟中占内存最大的一块区域,内存分配可以使用-Xms(初始值)和-Xmx(最大值)控制
-
线程共享;
-
会触发垃圾回收
-
有新生代(执行MinorGC)和老年代(执行MajorGC)
-
新生代和老年代的比例为1:2;
-
新生代分为eden区、s0、s1区,内存分配比为8:1:1,可以通过-XX:SurvivorRation来调整eden和Survivor Space的大小;
-
无论什么时候新生代实际的可用空间只会使用90%,因为总有一个Survivor为空;
-
新生代默认为1MB,可通过-XX:NewSize和-XX:maxNewSize、或者-Xmn进行设置;
-
新生代采用标记-复制算法,老年代采用标记-清除算法
-
新生代和老年代进行回收时都会触发stop-the-world事件,挂起所有任务,执行GC,执行完成GC后才会恢复执行任务,多数情况GC性能调优就是降低stop-the-world时GC的执行时长;
-
-
-
JVM中的对象
-
对象的分配:虚拟机遇到new时,会在常量池中查找类的符号引用,如果没有报ClassNotFoundException;
- 检查加载
- 分配内存:根据方法区存的类信息确定给为该类分配的内存大小。
- 如果内存规整则使用“指针碰撞”分配方式,否则会使用“空闲列表”方式,可以看出对象的内存分配方式和内存是否规整有关(即内存是否连续),而内存是否规整又与堆得垃圾收集器是否带有整理功能决定。
- 需要考虑线程安全问题,有两种方式:1.CAS机制、2.分配缓存
- 内存空间初始化:内存分配完成后需要将分配到内存空间都初始化为零值(0或false),保证了对象实例字段在不赋值就可以直接用。
- 设置:对对象进行必要的设置,例如对象属于哪个类,对象的哈希码,GC的分代年龄信息,这些信息存放在对象的对象头(对象的内存布局章节会讲)之中。
- 对象初始化:上一步一个新的对象已经产生了,只不过是jvm的默认值初始化,执行到这一步也就是这些对象按照程序员的意愿进行初始化。
-
对象的内存布局:hotSpot虚拟机中,对象在内存的布局分为3个区域:对象头(Header)、实例数据((Instance Data)和对齐填充(Padding).
-
对象头通常包括两部分信息(如果是数组的话还有一个Length):
- 第一部分(Mark Word)存储对象自身的运行时数据,如:哈希码(HashCode)、GC 标志、对象分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 第二部分(Class Pointer)是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
-
实例数据:即对象的实际数据,包括了所有的成员变量。其大小由各个成员变量大小决定,如下表
-
对齐填充:java对象是8字节对齐的,即所有java对象所占的bytes数都是8的倍数,如有两个变量int和byte类型,他们的大小就是4+1。这时就需要对齐填充。
-
对象访问定位:对象的建立是为了使用,java程序是通过栈的reference数据操作堆上的具体对象,主流的访问方式有两种:句柄和直接指针。
- 句柄:句柄是在堆中划出的一块区域句柄池来存储句柄,栈reference中存储的就是句柄的地址,而句柄包含了对象的实例数据和各类型数据具体的地址信息。优点是稳定,当对象被移动时只会改变句柄中实例数据的指针即可,不需要改变reference(因为句柄存储的是稳定的句柄地址)。
- 直接指针(Sun HotSpot使用这种):栈reference直接存储的是对象的具体地址。优点是速度快,他节省了一次指针定位的开销,通过栈直接定位到对象。
-
堆内存分配策略:常用参数如下
-Xms20m 堆空间初始20m
-Xmx20m 堆空间最大20m
-Xmn10m 新生代空间10m-XX:+PrintGCDetails 打印垃圾回收日志,程序退出时输出当前内存的分配情况
-XX:PretenureSizeThreshold=4m 超过多少大小的对象直接进入老年代,此参数只对Serial 和ParNew 两款收集器有效。最典型的大对象是那种很长的字符串以及数组。这样做的目的:1.避免大量内存复制,2.避免提前进行垃圾回收,明明内存有空间进行分配。
-XX:+UseSerialGC
-XX:MaxTenuringThreshold=15
-XX:+PrintTenuringDistribution
-XX:SurvivorRatio=8
- 新生代:新生代初始时就有大小
- eden:对象优先分配区,当eden区没有内存会触发一次minorGC.常用参数如下
- Survivor(from)区:
- Survivor(to)区:
- 老年代:长期存活的对象进入老年代(如果对象在Eden 出生并经过第一次Minor GC 后仍然存活,并且能被Survivor 容纳的话,将被移动到Survivor 空间中,并将对象年龄设为1,对象在Survivor
区中每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定程度(默认为15)_时,就会被晋升到老年代中)
对象年龄动态判定:
官方解释:虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
这里需要说明一点:这里的累加其实计算的年龄从小到大的累加是超过TargetSurvivorRatio(50%),则会把 这个累加过程中最后一个年龄段以上的进行晋升至老年代。
空间分配担保(HotSpot默认开启):在发生minorGC之前,虚拟机先检查老年代最大可用空间是否大于新生代所有对象总空间大小,如果大于,则进行一次minorGC可以确保是安全的,如果不大于,则看虚拟机是否设置允许担保失败(HandlePromotionFailure),如果允许,则会再检查老年的的可用空间大小是否大于历次晋级到老年代的对象平均大小,如果大于进行一次minorGC,如果担保失败进行一次fullGC;如果小于,或者不允许担保冒险,那这时也执行一次fullGC
- 新生代:新生代初始时就有大小