jvm总体上是由类装载子系统(ClassLoader)、运行时数据区、执行引擎三个部分组成。
(jvm本质上就是一个java进程)
(1)jvm启动:通过一个引导类加载器创建一个初始类来完成,这个类由虚拟机具体实现指定
(2)jvm运行:执行java程序
(3)jvm退出:①程序执行结束或遇到异常②某线程调用System或Runtime类的exit方法或Runtime的halt方法
(1)sun classic VM:世界上第一款商用虚拟机(jdk1.4时被完全淘汰)只有解释器,hotspot内置此虚拟机
(2)exact VM:JDK1.2引入,可以知道内存中某个位置的数据具体是什么类型(eg:不能判断某个数据是数值还是地址,要通过handler句柄去找,很麻烦)
(3)hotspot VM:jdk1.3称为默认虚拟机。具备热点代码探测技术(引入方法区的概念)
(4)Jrockit:专注于服务器端应用。不关注启动速度,因此不包含解析器实现,所有代码都靠即时编译器编译后执行
(5)J9:ibm公司,广泛应用与ibm的各种java产品
加载class文件,加载的类信息存放于方法区(除类信息外方法区还有运行时常量池,字符串字面量和数字常量)
1.加载:通过类的全限定名获取类的二进制字节流(从磁盘或网路加载类到内存),在方法区生成一个代表这个类的class对象
2.链接:
(1)验证:确保class文件包含的信息符合当前虚拟机的需求
(2)准备:为类变量分配内存(方法区),并设置默认初始值,即0(注意:不包含final修饰的static,因为在编译阶段就会初始化)
(3)解析:将常量池内符号引用转化为直接引用(加载类中用到的其它类如System)
3.初始化
静态初始化——父类初始化——子类初始化
父类静态成员和static块-子类静态成员和static块
父类普通成员和非static块-父类构造函数
子类普通成员和非static块-子类构造函数
(1)启动类加载器:使用c/c++实现,加载java的核心库(jre的lib下rt.jat)
(2)扩展类加载器:java编写,父加载器为启动类加载器(从jre/lib/ext下加载类库)
(3)应用类加载器:程序中默认的类加载器(加载classpath下的类库)
1.机制
(1)一个类加载器收到类加载请求,不会自己加载,而是交给父类的加载器去加载
(2)父类加载器还存在父加载器,进一步向上委托,直到达到顶层的启动类加载器
(3)父类加载器可以完成类加载,就成功返回。如果不能子类加载器才尝试加载
2.好处
(1)避免类重复加载,父classloader加载后没必要子classloader再加载
(2)安全保证,java核心api定义的类型不会被随意替换
1.程序计数器作用:指向下一条指令的地址 线程私有(唯一一个再jvm中没有内存溢出的区域)
2.为什么需要?:cpu需要不停的切换各个线程,切换回来后需要知道从哪开始继续执行
1.概述
每个线程创建时都会创建一个虚拟机栈,内部保存着一个个栈帧,对应着方法的执行
2.设置栈内存大小
-Xss:设置线程最大的栈空间
3.栈帧
(1)局部变量表:主要用于存储方法参数和方法体内的局部变量(基本单位是slot(变量槽)32位以内占一个槽,64位占两个槽)
变量的分类:
成员变量:又分为类变量和实例变量(根据是否static修饰) 局部变量:方法内的变量
(2)操作数栈:在方法执行过程中,根据字节码指令往栈中写入或提取数据。即入栈/出栈
(3)动态链接:将符号引用转化为直接引用
每个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用(可以理解为栈中保存的方法的地址,真正的方法结构是在方法区的运行时常量池中)
(4)方法返回地址:调用程序计数器的值作为返回地址。即调用吓一条指令的地址
作用:用于管理本地方法(native)的调用
所有的对象和数组都应当在运行时分配在堆上 。
java8前,新生区(Eden和Survivor)+老年区+永久区
java8后,新生区(Eden和Survivor)+老年区+元空间
(1)
-Xms:表示堆区的起始内存
-Xmx:表示堆区的最大内存(超出OOM)
(通常两个参数值相等,避免了gc后频繁的调整堆内存大小)
(2)默认初始大小:电脑内存/64
,最大内存大小 电脑内存/4
(3)查看设置的方式
方式一:
jps (查看java进程)
jstat -gc 进程id (显示gc相关的堆信息)
方式二:
-XX:+PrintGCDetails
(1)参数配置
①默认-XX:NewRatio 新生代占1,老年代占2
②默认-XX:SurvivorRatio=8 Eden和两个survivor-1:8:8
1.new的对象先放到Eden,此区有大小限制
2.当Eden满时,会进行young gc(stop the world),将Eden死亡的对象销毁,加载新的对象进eden
3.将Eden剩余的对象放到survivor0
4.再次gc时,会将eden和survivor0存活的对象放到survivor1
5.当对象的年龄超过一定次数(默认15)进入老年区
(-XX:MaxTenuringThreshold=15)
1.对象过大,Eden区放不下,直接放到老年代 (老年代也放不下进行old gc)
2.从eden放survivor的对象放不下,直接放老年代
1.yong gc:只是新生代的垃圾收集(eden,s0,s1)(stw)
2.old gc:针对老年代的收集(stw)
(stw原因:避免垃圾回收的时候用户线程再产生垃圾)
3.full gc:针对整个堆和方法区的垃圾收集
full gc出发机制:①调用System.gc() ②老年代空间不足 ③方法区空间不足
和堆一样,线程共享。存储 类信息,常量,静态变量,编译后的代码。实现方式是永久代(1.8之前)和元空间(1.8之后)(非堆)
1.永久代使用jvm内存,元空间使用本地内存
2.运行时常量池从永久代放入堆中
1.8之前:
-XX:permsize 设置永久代初始空间
-xx:maxpermsize 永久代最大空间
1.8之后:
-xx:metaspacesize 默认元空间大小
-xx:maxmetaspacesize 最大元空间大小
字节码文件,内部包含了常量池
方法区,内部包含了运行时常量池
1.常量池
一个java源文件中的类,接口编译产生字节码文件。代码中可能用到system,print等结构。如果把他们所有的信息都存在这个class文件,那文件就会非常大。所以把他们标识存到常量池。所以它可以看作一个表,虚拟机指令根据这张表找到曜执行的类,方法等。
2.运行时常量池
类加载后会将常量池中的内容加载到方法区的运行时常量池中
jdk1.6:有永久代,静态变量存放在永久代
jdk1.7:有永久代,字符串常量池,静态变量保存在堆
jdk1.8:永久代变元空间。类信息,常量保存在元空间。字符串常量池,静态变量仍在堆
永久代回收效率很低,在full gc的时候才会触发。而full gc是老年代空间不足,永久代不足时才会触发。这就导致字符串常量池回收效率不高。开发过程我们会创建大量的字符串。所以放到堆里能及时回收
1.new
2.Class的newInstance()
3.Constructor的newInstance方法
4.clone() 不调用任何构造器,当前类实现cloneable接口,实现clone方法
5.使用反序列化
1.判断对象对应的类是否加载,连接,初始化
2.为对象分配内存
(1)如果内存规整,虚拟机采用指针碰撞来分配内存。所有用过的内存放在一边,空闲的内存放在另一边。中间放着一个指针作为分界点指示器,分配内存就仅仅把指针空闲那一边挪动一段与对象大小相等的距离
(2)如果内存不规整,虚拟机采用空闲列表法来分配内存。列表上记录哪些块是可用的,在分配的时候从列表找到一块足够大的空间分配给列表实例
3.处理并发安全问题。采用cas失败重试,区域加锁保证更新的原子性
4.初始化属性值
5.设置对象的对象头
6.执行init进行初始化(显示初始化)
字符串创建后就不可改变,即使对它进行拼接等操作也是在新的字符串的基础上做的
通过字面量给字符串赋值,此时字符串值声明在字符串常量池。(字符串常量池不会存储相同的字符串——底层使用map)
(1)String str1= “abc”; 在编译期,JVM会去常量池来查找是否存在“abc”,如果不存在,就在常量池中开辟一个空间来存储“abc”;如果存在,就不用新开辟空间。然后在栈内存中开辟一个名字为str1的空间,来存储“abc”在常量池中的地址值。
(2)String str2 = new String("abc") ;在编译阶段JVM先去常量池中查找是否存在“abc”,如果过不存在,则在常量池中开辟一个空间存储“abc”。在运行时期,通过String类的构造器在堆内存中new了一个空间,然后将String池中的“abc”复制一份存放到该堆空间中,在栈中开辟名字为str2的空间,存放堆中new出来的这个String对象的地址值。
1.常量与常量的拼接结果在常量池,原理是编译期优化
String s1 = "a" + "b" + "c"; String s2 = "abc" s1 == s2 //true
2.常量池中不会存在相同内容的常量
3.只要有一个是变量,结果就在堆中(相当于new String)。变量拼接的原理是stringBuilder
String s1 = "a"; String s2 ="b"; String s3 = "a" + s2; String s4 = s1 + s2; String s5 = "a" + "b" s3 == s4 //false s4 == s5 //false String s6 = s3.intern() s6 == s5 //true
4.拼接结果调用intern()方法,则主动将常量池中还没有的对象放入池中。并返回此对象地址(常量池中的地址)
(1) String s = ""; Random r = new Random(); for (int i = 0;i < 10;i++) { s = s + r.nextInt(1000) + " "; } (2) String s = ""; StringBuilder sb = new StringBuilder(); for(int i = 0; i < 10; i++){ sb.append(" "); }
变量的字符串拼接,底层调用的是stringBuilder的append方法。在for循环中这样拼接的话每次都创建一个stringBuilder对象
1.new string("ab");会创建几个对象?
两个,堆空间+字符串常量池
2.new String("a") + new String("b")创建几个对象?
(1)StringBuilder
(2)new string("a")
(3)常量池中的"a"
(4)new string("b")
(5)常量池中的"b"
将字节码指令解释/编译(注意区分和前面java的编译)为对应平台上的本地机器指令
1.解释器:对字节码采用逐行解释的方式执行,将字节码指令解释为本地本地机器指令
2.编译器:将Java编译为class代码
每个对象关联一个引用计数器属性,任何一个对象引用了A,引用计数器的值加1.当引用失效时,引用计数器就减1.当引用计数器的值为0时,表示对象不再被使用,可进行回收
1.需要单独的字段存储计数器,这样增加了存储空间的开销
2.每次赋值都要更新计数器值,增加了时间开销
3.存在循环引用的问题(所以jvm不用):比如一个引用p指向a,a依赖于b,b依赖于c,c依赖于a.当p置空时。a,b,c引用计数器的值仍为1.不会被回收
设立若干根对象,当任何一个根对象到某一个对象均不可达时,认为这个对象可以被回收
1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.本地方法栈中JNI(即一般说的Native方法)中引用的对象
3.方法区中的类静态属性引用的对象;
4.方法区中常量引用的对象;
(GC Root Object可以从Java堆的外部访问,也就是不受GC的自动回收管制。可以理解为有免死金牌的Java对象)
标记-清除算法:通过根节点,标记所有根节点开始的可达对象,清除未被标记对象(会产生内存碎片)
1.stop the world,gc的时候停止整个应用程序
2.产生碎片,需要维护一个空闲列表(记录垃圾所在的位置,新对象到时可以直接覆盖该区域)
3.效率不高(进行了两次O(N)的遍历)
将内存分为一块较大的Eden和两块较小的survivor,每次使用Eden和其中一块survivor。Gc时将标记的对象复制到另一块survivor
1.没有标记和清除的过程,运行高效
2.复制过去以后保证空间的连续性,不会有碎片产生
浪费内存空间,始终要有一个空闲的survivor
将标记的对象移动到内存的一端,清除边界外的所有空间(解决了碎片问题)
速度:复制>标记清除>标记整理
目前几乎所有的gc采用分代收集算法进行垃圾回收
年轻代:复制算法
老年代:标记清除与标记整理混合
新生代收集器:Serial、ParNew、Parallel;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
采用一条收集线程完成收集工作。stop the world(新生代复制 老年代标记整理)
-xx:+UseSerialGC
serial收集器的多线程版本
-xx:+UseparnewGC
类似parnew。也采用复制算法。不过他的目的是高吞吐量(吞吐量 = 用户代码运行时间/(垃圾收集时间+用户代码运行时间))。通过-XX:MaxGCPauseMillis设置gc最大暂停时间提高吞吐量(但可能导致垃圾收集发生的频繁一些)
对于老年代,提供了parallel old收集器。目的也是高吞吐量
1.概述
他关注的是尽可能缩短stw的时间(采用标记清除算法)
可以实现用户线程与垃圾收集线程同时执行(并发标记和清除阶段)
-xx:+UseConcMarkSweepGc
2.cms工作原理
(1)初始标记:标出gc root能直接关联到的对象(stw,但非常短)
(2)并发标记:从gc-root的直接关联对象遍历整个对象图的过程(耗时长但不stw)
(3)重新标记:标记并发标记期间用户线程产生的新的而未被标记的对象(stw)
(4)并发清除:清除未被标记的对象
3.cms为什么不使用标记-整理算法?
因为采用并发,并发清除时垃圾收集线程和用户线程都在执行,使用标记整理会改变对象的内存地址,用户线程无法正常使用对象
garbage first。对垃圾进行优先级划分。
它把堆内存划分为不同的region。来代标eden,s0,s1,老年代等。跟踪各个region的垃圾价值(垃圾的数量和垃圾收集的时间)。维护一个优先级列表,优先收集价值最大的region。避免了堆中全区域的垃圾收集