1.JVM内存区域
1.1jvm的执行方式
解释执行与编译执行相结合
解释执行就是边翻译为机器码边执行。
即时编译(编译执行)就是先将一个方法中的所有字节码全部编译成机器码之后再执行。
Hotspot 采用的是先解释执行,到了一定时机后热点代码(多次执行、循环等)再翻译成机器码 热点代码探测技术(通过执行计数器找到最有编译价值的代码,如果代码用得非常频繁,就会把这些代码编译成本地代码)。
1.2体系结构
- JavaSE,Java 平台标准版,为 JavaEE 和 JavaME 提供了基础。
- JDK:Java 开发工具包,JDK 是 JRE 的超集,包含 JRE 中的所有内容,以及开发程序所需的编译器和调试程序等工具。
- JRE:JavaSE 运行时环境 ,提供库、Java 虚拟机和其他组件来运行用 Java 编程语言编写的程序。主要类库,包括:程序部署发布、用 户界面工具类、继承库、其他基础库,语言和工具基础库
- JVM:java 虚拟机,负责 JavaSE 平台的硬件和操作系统无关性、编译执行代码(字节码)和平台安全性
1.3运行时数据区(重点)
1.3.1线程私有
- 程序计数器:当前线程执行的字节码的行号指示器
如果线程正在执行的是一个 Java 方法,则指明当前线程执行的代字节码行数 如果正在执行的是 Natvie 方法,这个计数器值则为空(Undefined) - 虚拟机栈:
每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个栈帧,存储了局部变量表,操作数栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。 栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss256k 。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分 配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表:顾名思义就是局部变量的表,用于存放我们的局部变量的。首先它是一个 32 位的长度,主要存放我们的 Java 的八大基础数据类型,一般 32 位就可以存放下,如果是 64 位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的 Object 对象,我们只需要存放它的一个引用 地址即可。(基本数据类型、对象引用、returnAddress 类型)
操作数据栈:存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类 型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法是会一直运行入栈/出栈的操作。
三步曲:恢复上层方法的局部变量表和操作数栈、 把返回值(如果有的话)压入调用者栈帧的操作数栈中、 调整 PC 计数器的值以指向方法调用指令后面的一条指令、 异常的话(通过异常处理器表<非栈帧中的>来确定) - 本地方法栈
各虚拟机自由实现,本地方法栈 native 方法调用 JNI 到了底层的 C/C++(c/c++可以触发汇编语言,然后驱动硬件)
1.3.2线程共享区域
- 堆
几乎所有对象都分配在这里,也是垃圾回收发生的主要区域,可用以下参数调整:
-Xms:堆的最小值;
-Xmx:堆的最大值
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值; 例如-Xmx256m - 运行时常量池(符号引用与字面量)
- 方法区/永久代
用于存储已经被虚拟机加载的类信息,常量("zdy","123"等),静态变量(static 变量)等数据,可用以下参数调整: jdk1.7 及以前:-XX:PermSize;-XX:MaxPermSize; jdk1.8 以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize jdk1.8 以后大小就只受本机总内存的限制 如:-XX:MaxMetaspaceSize=3M
虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算
法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。
2.JVM对象
2.1new一个对象
虚拟机遇到一条 new 指令时:根据 new 的参数是否能在常量池中定位到一个类的符号引用,如果没有,说明还未定义该类,抛出 ClassNotFoundException;
1)检查加载
先执行相应的类加载过程。如果没有,则进行类加载
2)分配内存
根据方法区的信息确定为该类分配的内存空间大小
new对象的并发安全问题:
解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用 CAS 配上失败重试的方式保证更新操作 的原子性;
分配缓冲
另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块私有内存,也就是本地线程 分配缓冲(ThreadLocalAllocationBuffer,TLAB),如果设置了虚拟机参数 -XX:+UseTLAB,在线程初始化时,同时也会申请一块指定大小 的内存,只给当前线程使用,这样每个线程都单独拥有一个 Buffer,如果需要分配内存,就在自己的 Buffer 上分配,这样就不存在竞 争的情况,可以大大提升分配效率,当 Buffer 容量不够的时候,再重新从 Eden 区域申请一块继续使用
3)内存空间初始化
(注意不是构造方法)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如 int 值为 0,boolean 值为 false 等等)。这 一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4)设置
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象的对象头之中。
5)对象初始化
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。所以,一般来说,执行 new 指令之后会接着把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完 全产生出来。
2.2对象的内存布局
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。 对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的 锁、偏向线程 ID、偏向时间戳等。
对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于 HotSpotVM 的自动内存管理系统要求对对 象的大小必须是 8 字节的整数倍。对象正好是 9 字节的整数,所以当对象其他数据部分(对象实例数据)没有对齐时,就需要通过对 齐填充来补全。
2.3堆内存分配策略
新生代
Eden区,survivor区(from to) 复制算法空间碎片问题
老年代
1)对象优先在eden区分配
虚拟机参数: -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
-XX:+PrintGCDetails 打印垃圾回收日志,程序退出时输出当前内存的分配情况
注意:新生代初始时就有大小
大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 MinorGC
2)大对象直接进入老年代
-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:PretenureSizeThreshold=4m -XX:+UseSerialGC
3)长期存活对象进入老年区
如果对象在 Eden 出生并经过第一次 MinorGC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor 区中每熬过一次 MinorGC,年龄就增加 1,当它的年龄增加到一定程度(默认为 15)_时,就会被晋升到老年代中。
4)对象年龄动态判定
如果在 Survivor 空间中相同年龄所有对象大小的综合大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
5)空间分配担保
在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 MinorGC 可以确保是安全 的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历 次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 MinorGC,尽管这次 MinorGC 是有风险的,如果担保失败则会进行一次 FullGC;如果小 于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 FullGC。
3.泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理 解呢? 顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说 在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、 泛型方法。 引入一个类型变量 T(其他大写字母都可以,不过常用的就是 T,E,K,V 等等),并且用<>括起来,并放在类名的后面。泛型类 是允许有多个类型变量的。
虚拟机是如何实现泛型的?
Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类 型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一 个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛 型。 将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型 出现之前的写法,泛型类型都变回了原生类型
4.垃圾回收
4.1什么是垃圾
引用计数法(循环引用)
可达性分析
当一个对象到 GCRoots 没有任何引用链相连时,则证明此对象是不可用的。 作为 GCRoots 的对象包括下面几种:
当前虚拟机栈中局部变量表中的引用的对象
当前本地方法栈中局部变量表中的引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
4.2引用类型
1. 强引用
一般的 Objectobj=newObject() ,就属于强引用。 (如果有 GCroots 的强引用)垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止
2.软引用
垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它
软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。
SoftRefence
3.弱引用
垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存.
只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。
(强制)在代码逻辑中使用完ThreadLocal,都要调用remove方法,及时清理
4.虚引用
幽灵引用,最弱,被垃圾回收的时候收到一个通知 。
虚引用主要用来跟踪对象被垃圾回收器回收的活动
4.3GC
-Xms 堆区内存初始内存分配的大小 -Xmx 堆区内存可被分配的最大上限 -XX:+PrintGCDetails 打印 GC 详情 -XX:+HeapDumpOnOutOfMemoryError 当堆内存空间溢出时输出堆的内存快照 新生代大小配置参数的优先级: 中间 -Xmn 限定大小
-XX:SurvivorRatio
GCoverheadlimitexceeded 超过 98%的时间用来做 GC 并且回收了不到 2%的堆内存时会抛出此异常
1.Minor GC
特点: 发生在新生代上,发生的较频繁,执行速度较快
触发条件:Eden 区空间不足\空间分配担保
2.Full GC
特点: 主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢
触发条件: 调用 System.gc() 老年代区域空间不足 空间分配担保失败
4.4GC算法
1.复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使 用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可, 实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。 注意:内存移动是必须实打实的移动(复制),不能使用指针玩。
2.标记清除
过程: 1. 首先标记所有需要回收的对象 2. 统一回收被标记的对象 缺点: 1.效率问题,标记和清除效率都不高 2.标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不 提前触发另一次垃圾收集动作
3.标记整理法
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端 边界以外的内存。
4.5垃圾收集器
分代收集
根据各个年代的特点选取不同的垃圾收集算法 新生代使用复制算法 老年代使用标记-整理或者标记-清除算法
4.6CMS
一般新生代使用 ParNew,老年代的用 CMS 。
整个过程分为 4 个步骤,包括:
初始标记:仅仅只是标记一下 GCRoots 能直接关联到的对象,速度很快,需要停顿(STW-Stoptheworld)。
并发标记:从 GCRoot 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。这个阶段的停顿时间一般 会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除:不需要停顿。
优点:
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用 户线程一起并发执行的。
缺点:
CPU 资源敏感:因为并发阶段多线程占据 CPU 资源,如果 CPU 资源不足,效率会明显降低。
浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法 在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。 由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。 在 1.6 的版本中老年代空间使用率阈值(92%) 如果预留的内存不够存放浮动垃圾,就会出现 ConcurrentModeFailure,这时虚拟机将临时启用 SerialOld 来替代 CMS。
会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片
4.7G1
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
算法:标记—整理 (humongous) 和复制回收算法(survivor)。
YoungGC 选定所有年轻代里的 Region。通过控制年轻代的 region 个数,即年轻代内存大小,来控制 youngGC 的时间开销。(复制回收算法)
MixedGC 选定所有年轻代里的 Region,外加根据 globalconcurrentmarking 统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内尽可能选择收 益高的老年代 Region。 MixedGC 不是 fullGC,它只能回收部分老年代的 Region。如果 mixedGC 实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 MixedGC,就 会使用 serialoldGC(fullGC)来收集整个 GCheap。所以我们可以知道,G1 是不提供 fullGC 的。
初始标记:仅仅只是标记一下 GCRoots 能直接关联到的对象,并且修改 TAMS(NestTopMarkStart)的值,让下一阶段用户程序并发运行时,能在正确 可以的 Region 中创建对象,此阶段需要停顿线程(STW),但耗时很短。
并发标记:从 GCRoot 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 RememberedSetLogs 里面,最终标记阶段需要把 RememberedSetLogs 的数据合并到 RememberedSet 中。这阶段需要停顿线程(STW),但是可并行执行。
筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起 并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
特点
空间整合:不会产生内存碎片
算法:标记—整理 (humongous) 和复制回收算法(survivor)。
可预测停顿:
G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆 积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region (这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
5.JVM执行子程序
5.1JVM栈的二次理解
1.局部变量表
局部变量表的容量以变量槽(VariableSlot,下称 Slot)为最小单位,虚拟机规范中导向性地说到每个 Slot 都应该能存放一个 boolean、byte、char、short、 int、float、double、long 8 种数据类型和 reference ,可以使用 32 位或更小的物理内存来存放。 对于 64 位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。 Java 语言中明确的(reference 类型则可能是 32 位也可能是 64 位) 64 位的数据类型只有 long 和 double 两种。
2.操作数栈
操作数栈(OperandStack)也常称为操作栈,它是一个先进后出(FirstInLastOut,FILO)栈。 同局部变量表一样, 操作数栈的每一个元素可以是任意的 Java 数据类型,包括 long 和 double。 32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2。 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/ 入栈操作。 例如,在做算术运算的时候是通过操作数栈来进行的,又或者在"调用其他方法的时候是通过操作数栈来进行参数传递的"。 java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。
3.动态连接
既然是执行方法,那么我们需要知道当前栈帧执行的是哪个方法,栈帧中会持有一个引用(符号引用),该引用指向某个具体方法。 符号引用是一个地址位置的代号,在编译的时候我们是不知道某个方法在运行的时候是放到哪里的,这时我用代号 com/enjoy/pojo/User.Say:()V 指代某个 类的方法,将来可以把符号引用转换成直接引用进行真实的调用。 用符号引用转化成直接引用的解析时机,把解析分为两大类 静态解析:符号引用在类加载阶段或者第一次使用的时候就直接转换成直接引用。 动态连接:符号引用在每次运行期间转换为直接引用,即每次运行都重新转换。
4.方法返回
方法退出方式有:正常退出与异常退出 理论上,执行完当前栈帧的方法,需要返回到当前方法被调用的位置,所以栈帧需要记录一些信息,用来恢复上层方法的执行状态。正常退出,上层方 法的 PC 计数器可以做为当前方法的返回地址,被保存在当前栈帧中。"异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部 分信息" 方法退出时会做的操作:恢复上次方法的局部变量表、操作数栈,把当前方法的返回值,压入调用者栈帧的操作数栈中,使用当前栈帧保存的返回地址 调整 PC 计数器的值,当前栈帧出栈,随后,执行 PC 计数器指向的指令。
6.类加载机制
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、 初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking)
1.初始化:
初始化阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的 时候,以及调用一个类的静态方法的时候。
2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类。
场景:
1.对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
2.数组形式的 new(而不是构造方法)不会触发类初始化
3.直接打印类的常量会不会触发类的初始化:(坑:项目中有可能常量改了,关联使用的类不重新编译就会还是原来的值) 常量 HELLOWORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值“helloworld”存储到了 NotInitialization 类的常量池中,以后 NotInitialization 对常量 ConstClass.HELLOWORLD 的引用实际都被转化为 NotInitialization 类对自身常量池的引用了
4.如果使用常量去引用另外一个常量,这个时候编译阶段无法进行优化,所以才会触发类的初始化。
2.加载阶段
虚拟机需要完成以下 3 件事情: 1)通过一个类的全限定名来获取定义此类的二进制字节流。 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
3.验证
是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体 上看,验证阶段大致上会完成下面 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
4.准备阶段
是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强 调一下,首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为: publicstaticintvalue=123; 那变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译后, 存放于类构造器<clinit>()方法之中,所以把 value 赋值为 123 的动作将在初始化阶段才会执行。假设上面类变量 value 的定义变为:publicstaticfinalint value=123; 编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 123。
5.解析阶段
是虚拟机将常量池内的符号引用替换为直接引用的过程。部分详细内容见解析
6.初始化
是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。 到了初始化阶段,才真正开始执行类中定义的 Java 程序代码在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程 序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方 法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序 所决定的。
初始化的单例模式(线程安全)
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类 的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就 可能造成多个进程阻塞。所以类的初始化是线程安全的,项目中可以利用这点。
7.类加载器
类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这 句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一 个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 这里所指的“相等”,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用 instanceof 关 键字做对象所属关系判定等情况。
8.双亲委派模型
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性.
从 Java 虚拟机的角度来讲,只存在两种不同的类加载器: 一种是启动类加载器(BootstrapClassLoader),这个类加载器使用 C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加 载器都由 Java 语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 启动类加载器(BootstrapClassLoader):这个类将器负责将存放在<JAVA_HOME>\lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是 虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用 null 代替即可。 扩展类加载器(Extension ClassLoader):这个加载器由 sun.misc.LauncherApp-ClassLoader 实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这 个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 我们的应用程序都是由这 3 种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance) 的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。 使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在 程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将 会变得一片混乱。
应用程序类加载器
ClassLoader 中的 loadClass 方法中的代码逻辑就是双亲委派模型: 在自定义 ClassLoader 的子类时候,我们常见的会有两种做法,一种是重写 loadClass 方法,另一种是重写 findClass 方法。其实这两种方法本质上差不多, 毕竟 loadClass 也会调用 findClass,但是从逻辑上讲我们最好不要直接修改 loadClass 的内部逻辑。我建议的做法是只在 findClass 里重写自定义类的加载方 法。 loadClass 这个方法是实现双亲委托模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委托模型框架内进行 小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写 loadClass 方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修 改这个方法始终是比较好的选择。
6.JVM性能调优
6.1内存溢出与内存泄漏
内存溢出:实实在在的内存空间不足导致;
内存泄漏:该释放的对象没有释放,常见于使用容器保存元素的情况下。 如何避免: 内存溢出:检查代码以及设置足够的空间 内存泄漏:一定是代码有问题 往往很多情况下,内存溢出往往是内存泄漏造成的
内存泄漏的几个原因:
1.长生命周期的对象持有短生命周期对象的引用
例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
2.连接资源未关闭
如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。
3.变量作用域不合理
例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为 null
4.内部类持有外部类
Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命 周期,程序很容易就产生内存泄漏 如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(你认为垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引 用,导致垃圾回收器不能正常工作) 解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部 类是否被回收;
5.Hash 值改变
在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露(有代码案例 Node 类)
6.2浅堆与深堆
浅堆 :(ShallowHeap)是指一个对象所消耗的内存。例如,在 32 位系统中,一个对象引用会占据 4 个字节,一个 int 类型会占据 4 个字节,long 型变量 会占据 8 个字节,每个对象头需要占用 8 个字节。
深堆 :这个对象被 GC 回收后,可以真实释放的内存大小,也就是只能通过对象被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象所 持有的对象的集合。深堆是指对象的保留集中所有的对象的浅堆大小之和。 举例:对象 A 引用了 C 和 D,对象 B 引用了 E。那么对象 A 的浅堆大小只是 A 本身,而如果 A 被回收,那么 C 和 D 都会被回收(可达性分析算法),所以 A 的深堆大小为 A+C+D 之和,同时由于对象 E 还可以通过对象 B 访问到,因此不在对象 A 的深堆范围内
6.3J常见工具
jps 查看java进程
jstat 监视工具
jinfo 修改虚拟机参数
GC常见参数
-XX:-HeapDumpOnOutOfMemoryError
在java.lang.OutOfMemoryError 异常出现时,输出一个 dump.core 文件,记录当时的堆内存快 照。
6.4JVM深入调优
调优的原则
1、 大多数的 java 应用不需要 GC 调优
2、 大部分需要 GC 调优的的,不是参数问题,是代码问题
3、 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多;
4、GC 调优是最后的手段
调优步骤
日志分析
1,监控 GC 的状态 使用各种 JVM 工具,查看当前日志,分析当前 JVM 参数设置,并且分析当前堆内存快照和 gc 日志,根据实际的各区域内存划分和 GC 执行时间,觉得是 否进行优化; 2,分析结果,判断是否需要优化 如果各项参数设置合理,系统没有超时日志出现,GC 频率不高,GC 耗时不高,那么没有必要进行 GC 优化;如果 GC 时间超过 1-3 秒,或者频繁 GC,则 必须优化; 3,调整 GC 类型和内存分配 如果内存分配过大或过小,或者采用的 GC 收集器比较慢,则应该优先调整这些参数,并且先找 1 台或几台机器进行 beta,然后比较优化过的机器和没有 优化的机器的性能对比,并有针对性的做出最后选择; 4,不断的分析和调整 通过不断的试验和试错,分析并找到最合适的参数 5,全面应用参数 如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。
逃逸分析:
是 JVM 所做的最激进的优化,最好不要调整相关的参数。 牵涉到的 JVM 参数: -XX:+DoEscapeAnalysis:启用逃逸分析(默认打开) -XX:+EliminateAllocations:标量替换(默认打开) -XX:+UseTLAB 本地线程分配缓冲(默认打开)
如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。 采用了逃逸分析--对象在栈上分配: