目录
5.3程序计数器(Program Counter Register)
前言
参考书籍:深入理解java虚拟机--周志明著
目前主流的 Java 虚拟机主要还是 Oracle(前SUN) 的 jdk; linux开源的有 openjdk;weblogic以前有jrocket ;IBM 有自己的IBMJDK。不过如果不是有特殊要求,确保开发和应用使用相同的jdk。由于各JVM自己实现,所以可能略有不同。这里就以市面最常见的Oracle的jdk1.7(HotSpot虚拟机)进行探讨。
1999年4月27日,HotSpot虚拟机发布。HotSpot虚拟机发布时是作为JDK 1.2的附加程序提供的,后来它成为了JDK 1.3及之后所有版本的Sun JDK的默认虚拟机 。
1. 文章甜点
一下图片的内容是本篇文章主要进行梳理的内容如果和大家的理解的有分歧欢迎留言探讨,发现文章错误也欢迎指正!
2.虚拟内存与物理内存
操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能会出现很多问题:
- 因为物理内存是有限的,当有多个进程要执行的时候,都要给4G内存,很显然内存是不能够满足使用的,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的
- 由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是我们不想看到的
- 因为内存时随机分配的,所以程序运行的地址也是不正确的。
于是针对上面会出现的各种问题,虚拟内存就出来了
虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。
3. 虚拟机 类加载机制
3.1类加载机制的生命周期
类从被加载到虚拟内存中开始,到卸载内存为止,他的整个生命周期包含:
类的加载过程必须按照加载,验证,准备,初始化,和卸载的顺序开始,而
解析的阶段不一定,他在某些情况下可以在初始化阶段之后开始!
3.1.1加载:
- 获取二进制字节流
- 静态存储结构转化为方法区的运行时数据结构
- 在Java堆里面生成一个类对象,作为方法区的访问入口。
3.1.2验证
- 验证Class文件的标识:魔数Magic Number
- 验证Class文件的版本号
- 验证常量池(常量类型、常量类型数据结构是否正确、UTF8是否符合标准)
- Class文件的每个部分(字段表、方法表等)是否正确
- 元数据验证(父类验证、继承验证、final验证)
- 字节码验证(指令验证)
- 符号引用验证(通过符号引用是否能找到字段、方法、类)
在这个阶段报的常见错一般为下列几个
- IncompatibleClassChangeError ===》无法通过符号引用验证
- Unsupported major.minor version xx.x主要版本有问题
- IllegalAccessError
- NoSuchFieldError
- NoSuchMethodError
3.1.3准备
为类变量分配内存并且设置类变量的初始化阶段。
- 只对static类变量进行内存分配。(不懂的自行百度)
static int n = 2;
初始化值是0,而不是2。因为这个时候还没执行任何Java方法(<clinit>)。
static final int n = 2;
对应到常量池 ConstantValue,在准备阶段n必须被赋值成2。
类变量:一般称为静态变量。
实例变量:当对象被实例化的时候,实例变量就跟着确定。随着对象销毁而销毁。
3.1.4解析
对符号引用进行解析。
直接引用:指向目标的指针或者偏移量。
符号引号====>直接引用
主要涉及:类、接口、字段、方法(接口、类)等。
匹配规则:简单名字+描述符同时满足
- 字段的解析
- 类方法的解析
- 接口方法的解析
3.1.5初始化
初始化就是执行<clinit>()方法的过程。
<clinit>如果没有静态块,静态变量则没有<clinit>
<init>类的实例构造器
class A { static int i = 2; static { System.out.println(“”); } int n; }
<clinit>静态变量,静态块的初始化
<init>类的初始化
3.2双亲委任/双亲委派及破坏
目的:安全
- 父类加载的不给子类加载
- 一个加载一次
判断两个对象相对不相对,最重要的条件:是不是一个类加载器。
打破双亲委任机制:
当有人把自己的class打包在了JAVA_HOME/jre/lib/ext下或/lib下也会让相应的加载器加载到,(程序员写了不安全的程序JDK有责任不让他运行;程序安全是JDK的事,但是文件安全是系统的事)
- 打破双亲委派机制只需要要继承ClassLoader类,重写loadClass和findClass方法即可,如下例子:
public class Test { public Test(){ System.out.println(this.getClass().getClassLoader().toString()); } }
- 重新定义一个继承ClassLoader的TestClassLoader类,它除了重写findClass方法外还重写了loadClass()方法,默认的loadClass方法是实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时才由自己加载。这里为了破坏双亲委派机制必须重写loadClass方法,即这里先尝试交由System类加载器加载,加载失败才会由自己加载。它并没有优先交给父类加载器,这就打破了双亲委派机制。
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class TestClassLoader extends ClassLoader { private String name; public TestClassLoader(ClassLoader parent, String name) { super(parent); this.name = name; } @Override public String toString() { return this.name; } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> clazz = null; ClassLoader system = getSystemClassLoader(); try { clazz = system.loadClass(name); } catch (Exception e) { } if (clazz != null) return clazz; clazz = findClass(name); return clazz; } @Override public Class<?> findClass(String name) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { is = new FileInputStream(new File("D:/Test.class")); int c = 0; while (-1 != (c = is.read())) { baos.write(c); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (IOException e) { e.printStackTrace(); } } return this.defineClass(name, data, 0, data.length); } public static void main(String[] args) { TestClassLoader loader = new TestClassLoader( TestClassLoader.class.getClassLoader(), "TestLoaderN"); Class clazz; try { clazz = loader.loadClass("test.classloader.Test"); Object object = clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
4. Java虚拟机运行时数据区
(可以研究下java内存模型,这还是很有必要的)
运行时数据区所有线程共享: 堆 方法区==========>用来存储数据的
运行时数据区线程私有:虚拟机栈 本地方法栈 程序计数器=============>用来执行逻辑(流程上的)
程序:数据结构加算法
5.1堆(堆主要用来放对象实例)
对大对数应用来说,java堆(Heap)是java虚拟机所管理的内存中最大的一块
- java堆是被所有线程共享的一块区域,在虚拟机启动时创建
- 此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存
- OutOfMemoryError异常。如果在堆中没有内存完成实例分配,并且堆也无法在扩展时
5.2方法区
方法区(Method Area)与java堆一样,是各个线程共享的内存区域。用于存储:
- 已被虚拟机加载的类信息
- 常量
- 静态变量
- 即时编译器编译的代码
5.3程序计数器(Program Counter Register)
一块较小的内存空间,他的作用是当前线程所执行的字节码行号指示器,
唯一一个在JVN规范中没有规定任何OutOfMemoryError的区域
5.3栈
压栈push
出栈pop
main方法执行到栈底,每调用一个方法就会压入一个,当方法执行完执行虚拟机指令“rtn”也就是是出栈,
如果方法一直被调用不释放,就会报错 StackOverflowError内存溢出
public class TestJVM { private Long i = 1L; //main方法调用递归不停止,模拟出一直压栈不出栈的场景 public static void main(String[] args) { TestJVM tj = new TestJVM(); tj.test1(); } public void test1() { i++; System.out.println("=====>"+i); test1(); } }
=====>5668 =====>5669 =====>5670 =====>5671 =====>5672 Exception in thread "main" java.lang.StackOverflowError at sun.nio.cs.UTF_8$Encoder.encodeLoop(Unknown Source) at java.nio.charset.CharsetEncoder.encode(Unknown Source) at sun.nio.cs.StreamEncoder.implWrite(Unknown Source) at sun.nio.cs.StreamEncoder.write(Unknown Source) at java.io.OutputStreamWriter.write(Unknown Source) at java.io.BufferedWriter.flushBuffer(Unknown Source) at java.io.PrintStream.write(Unknown Source) at java.io.PrintStream.print(Unknown Source) at java.io.PrintStream.println(Unknown Source)
每当启动一个新线程的时候,java拟机都会为他分配一个java栈,java以栈帧为单位保存线程的运行状态。虚拟机只会对java栈执行两种操作:以栈帧为单位的入栈或者出栈
hotspot中 运行时数据区就叫做:栈
JVM执行流程
6.垃圾回收策略
1:首先JVM对什么区域进行垃圾回收?
- 堆
- 方法区
注意:栈是线程私有数据,所以不进行回收。
2:什么情况下回收?
一般我们说对这个对象不在引用的时候,进行回收?这种说法一定正确吗?
java中 对象引用关系
在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱
强引用:Object ob = new Object();像这样的常规引用,只要引用在,就永远不会回收对象。
软引用:在发生内存溢出之前(内存够用则不进行回收),进行回收,如果这次回收后还没有足够的内存,则报OOM
弱引用:生存到下一次垃圾回收之前,无论当前内存是否够用,都回收掉被弱引用关联的对象。
虚引用:不会对对象的生命周期有任何影响,也无法通过他得到对象的实例,唯一的作用也就是在对象被垃圾回收前收到一个系统通知
public class ReferenceObj { private static final int MB = 1024*1024; public Object instence= null; private byte[] size = new byte[2*MB]; public static void main(String[] args) { //强引用 ReferenceObj r1 = new ReferenceObj(); ReferenceObj r2 = new ReferenceObj(); //对象之间相互引用 r1.instence = r2; r2.instence = r1; r1 = null; r2 = null; System.gc(); } //软引用 public void softReference() { ReferenceObj r1 = new ReferenceObj(); SoftReference<ReferenceObj> sr = new SoftReference<>(r1); ReferenceObj rr1 = sr.get(); } //弱引用 public void weakReference() { ReferenceObj r1 = new ReferenceObj(); WeakReference<ReferenceObj> sr = new WeakReference<>(r1); System.gc(); ReferenceObj rr1 = sr.get();//null } //虚引用 public void phantomReference() { PhantomReference<String> pr = new PhantomReference<String>("hello", new ReferenceQueue<>()); System.out.println(pr.get());//null } }
虚引用的类为 java.lang.ref.PhantomReference
6.1对象可回收算法
6.1.1引用计数法
引用计数法是垃圾收集的早期策略,在这中方法中,堆中每个对象都有一个引用计数,每当有一个地方引用他时,引用计数值就+1,当引用失效时,引用计数值就-1,任何时刻引用计数值为0的对象就是可以被回收,当一个对象被垃圾收集时,被它引用 的对象引用计数值就-1,所以在这种方法中一个对象被垃圾收集会导致后续其他对象的垃圾收集行动。
优点:判定效率高;
缺点:不完全准确,当两个对象相互引用的时候就无法回收,导致内存泄漏。(上述代码中的对象相互引用的问题无法解决)
6.1.2可达性分析算法
GCRoots对象:
- 虚拟机栈(栈帧中的本地变量表)中的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
6.2.1垃圾回收算法之 标记-清除法
标记-清除法:分为“标记”和“清除”两个阶段:首先先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
缺点:
- 一个是效率问题,标记和清除的效率都不是很高
- 另一个就是空间问题,标记清楚之后会产生大量不连续的内存碎片(当内存碎片不足以存下一个大对象时候的就会抛出OOM)
6.2.2垃圾回收算法之 复制算法
复制算法:
- 将可用内存按容量划分为大小相等的两块,每次只是用其中一块,当这块的内存用完后,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。
- 缺点:内存缩小为原来的一半,内存利用率太低
6.2.3垃圾回收算法之 标记-整理(压缩)算法
标记-整理(Mark-Compact)算法,标记过程仍然与“标记-清楚”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存
优点:
自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。
6.2.3垃圾回收算法之 分代收集算法(Generational Collection)
目前商业虚拟机的垃圾收集都采用 “分代收集”算法,这种算法是根据对象存活周期的不同将内存划分为几块(java的对象大多都是朝生夕死)
“分代收集”算法,就是将上述三种算法整合了一下
根据各个年代的特点采取最适当的收集算法
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
- 老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
7.垃圾回收器
7.1SafePoint(安全点/安全区域)
这些特定的指令(安全点)位置主要在:
- 循环的末尾
- 方法临返回前 / 调用方法的call指令后
- 可能抛异常的位置
STW = stop the word (正常执行的用户线程全部停止)
找到“GC Roots”也是要花很长的时间,于是就通过采用一个OopMap的数据结构来记录系统中存活的“GC Roots”,在类加载完成的时候,虚拟机就把对象内什么偏移量上是什么类型的数据计算出来保存在OopMap,通过解释OopMap就可以找到堆中的对象,这些对象就是GC Roots。而不需要一个一个的去判断某个内存位置的值是不是引用。这种方式也叫准确式GC
[概念模糊]======
7.2 Serial收集器
Serial 垃圾回收器: serial回收器--串行的 jdk1.3时候使用,新生代和老年代都会STW。桌面环境不需要并发计算。
7.3ParNew(ParallelNew)收集器
只对新生代并行收集,对老年代还是单线程收集。收集过程中STW。
7.4 ParallelScavenge收集器
也是一个并发的收集器和ParallelNew类似, 它和ParNew的区别是,用户可以控制GC时用户线程停顿时间。
开启参数: -XX:UseParallelGC
ParallelScavenge的关注点在:可控的吞吐量
ParallelScavenge参数配置
-XX:MaxGCPauseMillis //最大垃圾收集停顿时间(大于0毫秒)
-XX:GCTimeRatio //吞吐量大小(大于0且小于100的整倍数,吞吐量百分比)
-XX:UseAdaptiveSizePolicy //内存调优委托给虚拟机管理
7.5 Serial Old 收集器
Serial Old是Serial收集器的老年代版本,他同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是给Clint模式下的虚拟机使用。
7.6 Parallel Old 收集器
7.8 CMS(Concurrent Mark Sweep收集器)
CMS收集器过程:
- 初始标记(仅只是标记一下GC Roots能直接关联到的对象,速度非常快),需要STW,
- 并发标记(和用户线程并发执行对对象进行标记)
- 重新标记(SWT,重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录)
- 并发清除(“标记--清除”算法实现的收集器,收集后容易产生内存碎片,可以配置在收集后触发整理操作)
默认:68% CMS默认空间
- -XX:CMSInitiatingOccupancyFraction 用来设置CMS空间参数
- MinorGC针对新生代。
- MajorGC=FullGC针对老年代。Major GC的时候会同时执行Minor GC
- FullGC = MajorGC+MinorGC
特点:低停顿
- -XX:+UseCMSCompactAtFullCollection //GC执行完后做一次整理操作
- -XX:+CMSFullGCsBeforeCompaction //执行多少次FullGC要做一次整理操作
7.9 G1收集器
仍然属于分代收集器,只不过把内存划分为多个Region
特定:空间整合。
算法:既属于标记-整理,也属于复制。
YoungGC:
- 扫描根GC Roots
- 更新RememberSet记录回收对象的数据结构
- 检测RememberSet哪些数据是要从年轻代到老年代
- 拷贝对象,要么往幸存代,要么往老年代
- 清理工作
7.10 垃圾回收总结
JDK1.7/1.8 默认的垃圾回收器:Parallel Scavenge(新生代)+Parallel Old
JDK1.9 默认垃圾回收器:G1(G1垃圾回收是在1.7提出18可以使用,1.9正式默认使用的垃圾回收器)
新生代
老年代
-XX:+UseSerialGC
SerialGC
Serial Old
-XX:+UseParallelGC
Parallel Scavenge
Parallel Old
-XX:+UseConcMarkSweepGC
ParNew
CMS GC,当出现Concurrent Mode Failure时采用串行GC
-XX:+UseParNewGC
ParNew
Serial Old
-XX:+UseParallelOldGC
Parallel Scavenge
ParallelOld
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
Serial GC
CMS GC,当出现Concurrent Mode Failure或promotion failed采用Serial Old GC
不支持的方式
-XX:+UseParNewGC -XX:+UseParallelOldGC
-XX:+UseParNewGC -XX:+UseSerialGC
使用这种会在启动的时候报错,
8.1JDK性能监测工具
jps
jstat -gc 进程号 1000(每毫秒数打印一次) 10(共打印多少次)
jinofo
jmap
jstack-堆栈跟踪工具
jconsole
后续会继续更新