目录
前言
一、什么是JAVA虚拟机(JVM)
二、内存结构
1.程序计数器
2.虚拟机栈
3.本地方法栈
4.堆
5.方法区(元数据区)
6、执行引擎
7、直接内存
三、垃圾回收
1.如何判断对象可以回收,
2.垃圾回收算法
3.分代垃圾回收
4.垃圾回收器
四、垃圾回收调优
五、类加载器子系统
1.类加载过程:
2.类加载器以及之间的关系
3.双亲委派机制
4.沙箱安全机制
六、Class文件格式(ClassFileFormat)
1.字节码文件的跨平台性
2.虚拟机基石
总结
前言
本文以HotSpot虚拟机为代表,重点是介绍一下java虚拟机内存结构,垃圾回收机制,类加载子系统。
一、什么是JAVA虚拟机(JVM)
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
简单来说JVM是用来解析和运行Java程序的。
二、内存结构
JVM的内部体系结构分为三部分,分别是:类装载器(ClassLoader)子系统,运行时数据区,和执行引擎
java虚拟机定义了若干的程序运行期间会使用到的运行时数据区,其中会有一些随着虚拟机启动而启动,随着虚拟机的退出而销毁,比如线程独享的存储区域。
线程共享:堆,堆外内存(元空间)
线程独享: 计数器,本地方法栈,虚拟机栈。
1.程序计数器
pc程序计数器:存储指向下一条指令的地址,由执行引擎读取下一条指令,线程私有,运行速度最快的存储区域,他是唯一一个没有一个内存溢出(out of memery)的区域
如果线程正在执行的是java虚拟机栈的方法时,程序计数器记录的是java虚拟机 正在执行字节码指令地址,如果执行的native方法,则记录的是Underfined。
2.虚拟机栈
虚拟机栈: 线程运行需要的内存空间,每个方法在执行时都会创建一个栈帧,栈帧:每个方法运行时需要的内存,一个栈内有多个栈帧。栈帧存储局部变量表,动态链接,操作数,方法出口等信息。一个方法从调用到结束就是一个栈帧从入栈到出栈的过程。
局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型,就是一些操作完以后的数据,它是一个数组结构。(returnAddress中保存的是return后要执行的字节码的指令地址。)
操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去,临时来存放数据。
动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
出口:出口正常的话就是return 不正常的话就是抛出异常落
1.垃圾回收是否涉及栈内存?
答: 不需要,这里是一定不能有垃圾的,如果这里有垃圾那么栈帧会弹不出去,整个虚拟机栈会卡死。
2.栈内存分配越大越好吗?
答: 不是,因为栈内存一定的情况下,分配越大,栈帧的数量就会减少
栈内存溢出StackOverFloatError: 方法的递归调用,栈帧过多,栈帧过大,第三方的库都可能会导致, 调优 -Xss
3.本地方法栈
java虚拟机调用本地方法时分配的内存,本地方法就是一些不是由java编写的代码比如一些由c/c++编写的方法,本地方法栈就是给本地方法运行提供内存空间。
4.堆
线程共享,这块是java虚拟机内存结构中最大的一块区域,也是垃圾收集的主要区域,线程共享,主要存放java创建的实例和数组,当堆中没有内存存放实例时会oom,内存溢出。
堆内存诊断:
jps:查看当前系统中有哪些java进程,命令行形式
jmap:查看堆内存的占用情况(某个时刻),命令行形式 jamp -heap 进程id
jconsole:多功能的检测工具,可以连续监测,图形界面
jvirsualvm:图形化界面工具,可以排查堆内存占用情况,堆dump。
相关VM参数
堆初始大小 : -Xms
堆最大大小:-Xmx
新生代大小: -Xmn
幸存区比例: -XX: SurvivorRatio=
GC详情:-XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC: -XX:+ScavengeBeforeFullGC
晋升阈值:-XX:MaxTenuringThreadhold=threashold
5.方法区(元数据区)
线程共享,方法区逻辑上是堆的一部分,方法存储了更类结构相关的一些信息,比如常量,类变量,类的构造器,方法的信息,成员方法和构造方法,编译器编译后的代码等等,方法区如果内存不足也会报内存溢出
在jdk1.8以前,称为永久代,在1.8以后,称为元空间MateSpace,不由具名管理它的内存结构,而是交给操作系统内存,元空间使用的系统内存,元空间的串池StringTable被移到了堆内存中。
代理技术在运行期间动态生成字节码,可能会出现内存溢出,场景: Spring ,mybatis。
常量池:存放编译期间生成字面量和符号的引用 ,虚拟机根据这张常量表找到要执行的方法名,类名,参数类型,字面量等信息。
运行时常量池: 当常量池是在*.class 文件中的,当类被加载,它的常量池信息就会放入运行时常量池中,并把里面的符号地址编号才能真实地址。
常量池(contanst pool)和串池(String Table)的关系:
public class a {
public static void main(String[] args){
String s1="a";
String s2="b"; //懒惰的
String s3="ab";
String s4 =s1+s2; //new StringBuilder().append().toString(),StringBuilder的
toString是new了一个新对象,相当于new String("ab");
String s5="a"+"b"; //javac在编译期间的优化,在编译就已经确定为ab
System.out.println( s3 == s4); //false s3是在串池中的,而s4是在堆里面的,地址不同
System.out.println(s3 == s5); //true 这里跟上面不同之处在于 "a"和"b"是常量,
而s1和s2是变量,常量在编译器就已经确定,不需要用StringBuilder去拼接
}
}
串池(StringTable)的特性:
常量池中的字符串仅仅是符号而已,第一次用到才变为对象;
利用StringTable的机制来避免重复创建字符串对象;
字符串变量拼接的原理是StringBuilder创建新对象;
字符串常量拼接原理是编译器优化;
可以使用intern方法,主动将串池中还没有字符串对象放入串池;
StringTable也会触发垃圾回收。
jdk1.8这个字符串对象尝试放入串池,如果有并不会放入,如果没有则放入串池,会把串池中的对象返回。
jdk1.6将这个字符串对象尝试放入串池,如果有并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回。
public class a {
//串池的字符["a","b","ab"]
public static void main(String[] args){
String s = "ab";
String s1= new String("a") + new String("b"); //new String("ab");
String s2 = s.intern(); //将这个字符串对象尝试放入串池中,如果有则不放入,没有就会把串池的对象返回
System.out.println(s2 == s1); //false
System.out.println(s2 == "ab");//true
}
}
串池的位置:
在jdk1.6串池StringTable是常量池中的一部分,随着常量池放在永久代(方法区)中
在jdk1.8串池StringTable是放在堆中,
永久代的内存回收效率很低,永久代得到fullGC时才会进行垃圾回收,而一个java对象中大量的字符串都放在StringTable中,StringTable用的非常频繁,如果放在堆中的话,只需要minGC既可以触发垃圾回收
6、执行引擎
从代码的执行角度来讲,执行引擎是非常重要的,是JVM核心结构之一。
JVM的主要任务是负责 装载字节码到内部,但是字节码并不能直接运行在操作系统之上,以因为字节码的指令并非等价于本地机器指令,它的内部包含的仅仅只是一些能够被JVM所识别的字节码指令,符号表。那么,想让一个Java程序运行起来,还需要执行引擎安静会字节码指令解释、编译成对应平台上的本地机器指令才可以。简单理解就是,一个翻译官。
java采用混合模式:混合使用解释器 + 热点代码编译,java的半解释半编译并不是先解释再编译的而是它可以解释也可以编译。
1.解释器(图中绿色部分)
将源代码翻译成字节码文件,然后在运行时通过解释器假尼姑字节码文件翻译成机器指令,时至今日,Java虚拟机其实不只是面对Java这门语言,因为任何class文件都能在Java虚拟机上运行。
2.及时编译器 Just In-Time intepreter(图中蓝色部分)
由于解释器在设计和实现上非常简单,除了java外还有很多语言也同样是基于解释器执行,基于解释器执行已经沦为低效的代名词,为了解决这一问题,JVM提供一种叫做及时编译器的技术。
优势:速度快,已经可以和C/C++一较高下了。
既然如此,为什么要保留解释器?
首先,解释器的优点是响应速度快,解释器可以马上发挥作用,省去编译时间,而不必等待即时编译器全部编译完成再去执行,这样可以省去很多不必要的编译时间,并且随之程序运行时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
热点代码检测:
多次被调用的方法(方法计数器:监测方法执行频率)
多次调用循环(循环计数器: 监测循环执行频率)
7、直接内存
直接内存并不是java虚拟机的一部分,不受JVM内存回收管理,但是会内存溢出
常用于NIO操作时,用在数据缓冲区
分配回收成本较高,但是读写性能高
java本身并不具备 磁盘 读写的能力,java要调用磁盘文件的内容需要调用本地方法,这里会牵扯到CPU的运行状态,由用户转态转换到内核转态,其次,当CPU切换到内核状态时内存这边也会划出一块系统缓冲区中,,而java代码是没办法直接运行的,所以会在java对内存中划分出一块java缓冲区,这时会有两块缓冲区,读取的时候必然会有不必要的复制,降低效率。
JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。java代码能直接访问这块区域,磁盘文件也能直接访问这块区域。所以就提高读写效率。
直接内存和堆内存的区别:
直接内存的读写效率 高,分配回收成本高
堆内存的读写效率 低 , 分配回收成本低
三、垃圾回收
1.如何判断对象可以回收,
1.1引用计数:给对象一个计数器,难以解决对象之间循环引用的问题,会造成内存泄漏。
1.2可达性分析,java虚拟机中的垃圾回收器采用的是这种算法,判断GC ROOT是否有相连的引用链,如果没有就回收。
1.3四种引用(强度递减)
强引用:只要沿着GCroot引用链,就不能被回收。
软引用,只要没有被强引用引用,就可能会被回收,当我垃圾回收时,内存不够就回收;软引用本身也是一个对象,当软引用对象被回收时,软引用会进入引用队列。
弱引用,只要没有被强引用引用,就可能会被回收,当我垃圾回收时,不管内存够不够都回收,同理,软引用也会进入引用队列。
虚引用,必须配合引用队列使用,当虚引用对象创建时,他会关联一个引用队列,主要配合
终结器引用,必须配合引用队列使用
2.垃圾回收算法
复制算法
特点:效率高,但是会浪费空间,复制算法主要用在新生代,因为大多数的对象都是朝生夕死的,没办法熬过第一次的GC,所以没有必要用标记算法。
垃圾清除算法
特点:会产生空间碎片, 用在老年代,老年代中对象存活率较高、没有额外的空间分配对它进行担保
垃圾整理算法
这是复制算法和标记清除算法的折中,内存利用率高,而且不会产生内存碎片,用在老年代。
3.分代垃圾回收
在谈论垃圾回收器之前我们得先了解分代模型
根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。可以用抓重点的思路来理解这个算法。
新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。新生代和老年代的比例是1:2,
新生代
在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
-
新生代的特点
- 所有的 new 操作分配内存都是非常廉价的
- TLAB thread-lcoal allocation buffer
- 死亡对象回收零代价
- 大部分对象用过即死(朝生夕死)
- Minor GC 所用时间远小于 Full GC
- 所有的 new 操作分配内存都是非常廉价的
-
新生代内存越大越好么?
- 不是
- 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
- 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
老年代
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记清除法或者标记整理算法进行回收。
Full GC Minor GC ,Major GC的概念
FullGC
清理的是整个堆空间包括新生代和老年代
SerialGC,ParallelGC,CMS,G1折四种垃圾收集器早新生代内存不足发生的垃圾收集都是-minor gc,Serial和ParallelGC在老年代内存不足发生的垃圾收集机制都是-full gc;
CMS和G1这两个垃圾收集在老年代有所不同,对于G1来说,新产生的垃圾跟不上回收的速度,这时候并发收集就失败了,这时候就会退化成FullGC,就会特别的慢,更长时间的Stop the world,响应速度变慢;对于CMS也一样,他们工作在并发的阶段,回收速度高于垃圾产生速度,GC日志是不会有FullGC的字样的。
MinorGC
Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。
MajorGC
Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。
4.垃圾回收器
所谓的垃圾回收,跟jdk版本有很大的关系,不同版本的垃圾回收器在内存的管理是不一样的,jvm的内存管理由垃圾回收器来决定。
随着内存变得越来越大,诞生了很多种垃圾回收器,jVM总共有十种垃圾回收器,这篇介绍的是前面的六种和G1
前面六种是分代模型,就是内存里面分成两代,分为新生代和老年代
G1是逻辑分代,物理不分代,
ZGC逻辑不分代,物理也不分代,
Shenando和ZGC他们很像
Epsilon,是jdk11新出的,特殊的一个,他什么事都没干
垃圾回收器的组合:
凡是图中有连线的都可以组合使用,但是常用的组合就三种
ParNew和CMS,Serial和SerialOld, ParallelScavenge和Parallelold
查看虚拟机用的是那种GC的命令
六种分代垃圾回收器
这几个垃圾收集器一般是配合使用
1.串行:单线程,适合堆内存小,垃圾回收线程工作时其他线程都得停止(Stop The World)
Serial,工作在新生代,复制算法
SerialOld,工作在老年代,标记-整理算法
2.吞吐量优先:多线程,适合堆内存比较大,多核CPU支持,适合服务器,让单位时间内Stop The World时间变短,垃圾回
ParallelScavenge:新生代,复制算法,垃圾回收线程跟CPU核数有关
ParallelOld:老年代,标记整理算法,这两种是JDK 1.8默认的垃圾回收机制
3.响应时间优先:多线程,适合堆内存比较大,多核CPU支持,适合服务器,尽可能让单次STW时间变短
ParNew:新生带,复制算法,serial的多线程版本
CMS:Concurrent Mark Sweep,老年代,用户线程和垃圾线程并行,stop The World时间短。
分为以下几个阶段
- 1、初始标记(CMS initial mark)会产生停顿,非常短,仅仅是扫描引用链
- 2、并发标记(CMS concurrent mark),从root开始找,一边找业务线程一边运行
- 3、重新标记(CMS remark),会产生停顿,当整个图谱屡清楚以后,再重新标记一边,它的STW时间其实不会很短,但是相比于前面几种回收器来讲时间会短很多。
- 4、并发清除(CMS concurrent sweep)清除垃圾,和用户线程一起工作
为什么CMS不启用标记压缩? 要保证用户线程还能够继续执行,不能改变用户线程对象的位置。
优点:并发收集,低延迟
弊端:会产生内存碎片,导致并发清除后,用户线程可用空间不足,无法分配大对象的情况下不得不触发Full GC。
CMS对CPU的资源非常敏感,在并发阶段,他虽然不会导致用户线程停止,但是会因为占用一大部分的线程导致应用程序变慢,总吞吐量会降低。
CMS收集器无法处理浮动垃圾。比如关联的对象突然变成垃圾,那么他在第一次就不会被清理掉,就是浮动垃圾,第二次CMS才会将他们删除。
另外,由于垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序有足够可用的内存可用。因此CMS收集器不能像其他收集器那样等老年代几乎完全填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始使用回收,以确保引用程序在CMS过程中依然有足够的空间支持程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次
"Councurrent Mode Failure" 失败,这时,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
-XX:+UseConcMarkSreepGC //手动使用ParNew + CMS
-XX:CMSInitiatingOccupanyFraction //设置堆内存使用率的阈值
-XX:+UseCMSCompactAtFullCollection //执行完full gc后进行压缩整理
-XX:+CMSFullGCsBeforeCompaction //设置执行多少次不压缩的Full GC后,来一次压缩整理
-XX:ParallelCMSThreads //cms线程数
5、G1垃圾回收器:
从JDK9就默认使用,使用场景同时注重吞吐量和低延迟,默认的暂停目标是200ms,G1跟cms一样都是并发,在小的堆内存场景下,他们的速度暂停速度都差不多,但是在堆内存大的情况下,G1收集器在暂停时间的优势就体现出来了,应用场景可以面向服务端应用。
设计思想:它将整个堆分成若干个预先设定的小区域块(1~32MB,大小可调),每个区域内部不在进行新旧分区,而是将
超大堆内存管理的思想,会划分为多个大小相等的区域,每个区域都可以作为伊甸园,老年区
整体是 标记整理算法,两个区域之间是复制算法。
C1回收器的垃圾回收阶段
YoungConnection,主要是对Eden区的GC,对象创建式分配到伊甸园区,Eden区的内存紧张了,对象多了,就会以复制的算法到复制到幸存区,再工作一段时间,幸存区的空间不够了,Eden区中的部分数据会之间晋升到老年代空间中,当最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
MixedConnection混合收集,在新生代和老年代都来进行规模较大的收集,当整个堆大小在具名堆栈空间中占比达到45%时,G1就会启动一次混合垃圾收集周期,同时也回收后台扫描标记的老年代分区
三色标记算法(Remark)
CMS和G1在并发标记时使用的是同一个算法:三色标记法,使用白灰黑三种颜色标记对象。白色是未标记;灰色自身被标记,引用的对象未标记;黑色自身与引用对象都已标记。垃圾回收时会根据对象的颜色去回收垃圾
漏标问题
两个条件发生漏标问题:
灰色对象指向的白色对象引用消失,黑色对象指向了白色对象。
解决办法:添加屏障
- 跟踪黑指向白的增加
incremental update:增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。CMS采用该方法,但是这个办法隐藏了严重的BUG,所以CMS的remark阶段,必须重头扫描一遍。 - 记录灰指向白的消失
SATB snapshot at the beginning:关注引用的删除,当灰–>白消失时,要把这个 引用 推到GC的堆栈,保证白还能被GC扫描到。G1采用该方法。
-XX:+UseG1GC //jdk9之前手动指定
-XX:G1HeapRegionSize //指定region大小,根据最小的堆内存划分成2048个区域
-XX:MaxGCPauseMillis //期望达到的最大gc停顿时间,过小单次gc的对象少,过大停顿时间长
-XX:InitiatingHeapOccupancyPercent //当整个Java堆的占用率达到参数值时,开始并发标记阶段
-XX:ParallelGCThread //并行时gc线程数,会stop-the-world
-XX:ConcGCThreads //并发时gc线程数
对象分配策略
java某些特定小的对象是可以在java虚拟机栈进行分配,栈里面通常会有方法的调用,一个方法对应一个栈帧,如果用栈帧来管理对象会有什么好处呢? 不需要GC参与垃圾回收,栈往外面一弹,整个对象分配结束,效率非常高。既然如此,那么为什么所有的对象都不往栈上分配?
1.栈的空间通常比较小;
2.如果一个栈帧里面有个一个引用,引用了另一个对象,这时如果这个对象被弹出去了,那么这个引用就会空指针错误。
如果这个对象够大,这个大是可以自己分配的,栈上分配不下,直接分配到老年代,如果这个对象不大不小的话就会分配到伊甸园区,不过前面还有一个线程本地分配缓冲区,不过这块区域本身就在伊甸园区,这块区域也能调优。
线程本地分配缓冲区,每个线程启动时,jVM下每个Eden去中为本地区分配缓冲区,这块区域是这个线程专属的,他的目的为了使对象尽可能快的而分配出来。如果独享在一个共享空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不需要进行任何同步,这样他们的效率又变高了。
不管TLAB空间中能不能分配的对象,对象都会在Eden空间中进行分配。如果Eden空间无法容纳该对象,就这能在老年代中进行空间分配。
四、垃圾回收调优
JVM垃圾回收调优主要就是调整下面两个指标
停顿时间: 垃圾收集器的垃圾回收线程中断应用执行的时间。-XX:MaxGCPauseMillils
吞吐量:花在垃圾收集的时间和花在应用时间的占比 -XX:GCTimeRatio=<n>
内存占用: Java堆中所占内存的大小
GC调优思路:
吞吐量大:降低gc频率,延长单次暂停时间来进行gc
暂停时间短优先:缩短单次暂停时间进行gc,提高工程频率,吞吐量下降
配置常见的四种GC:
1.SerialGC : -XX:+UseSerialGC #old和Young区都是用Serial
2.ParallelGC: -XX:+UseParallelGC
Young区使用Parallel scavenge回收算法
old区使用Parallel 回收算法,参数:-XX: UseParallelOldGC来控制
3.CMS: -XX:+UseConcMarkSweepGC
4.G1: -XX:+UseG1GC #没有young/old区
五、类加载器子系统
1.类加载过程:
程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
获取类的信息使他们分配到运行时区中,类加载器负责class文件的加载,至于它能否运行,由执行引擎决定,类加载子系统负责从文件或者网络中加载Class文件,Class文件打来有特定的文件标识
加载:
加载指的是将类的class文件读入到内存,class文件本来是一个个二进制字节,把他们装到内存中去,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来自定义自己的类加载器。
类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。
类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口。
链接: 验证--->准备--->解析
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。
准备:正式为类变量(static变量)分配内存并设置类变量默认值的阶段,这些内存都将在方法区中进行分配
解析:虚拟机常量池的符号引用替换为字节引用过程
初始化:为类的静态变量赋予初始值
2.类加载器以及之间的关系
jvm中所有的class都是被类加载器加载到内存的,一个class文件被加载到内存中,内存中其实创建了两个内容,第一块内容确实是这个class文件被加载到内存中,第二块内容是生成了一个Class类的对象Class,Class类的对象指向这个 class文件内容。
启动类加载器,由C++实现,没有父类,加载 lib 下或被 -Xbootclasspath 路径下的类。
拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null,加载扩展jar包 lib/ext 或者由 java.ext.dirs 系统变量所指定的路径下的类
系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader,加载classpath指定的内容
自定义类加载器,父类加载器为AppClassLoader。
3.双亲委派机制
java虚拟机对class文件采用按需加载,如果一个类加载器收到一个类加载请求,他会先委托给父类去加载,一步一步向上委托,直到启动类加载器,父类无法加载时子类才会尝试去加载
优点: 避免类的重复加载,保护程序的安全防止程序的api被加载。
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
4.沙箱安全机制
保证程序安全
保护Java原生JDK
Java安全模型的核心就是Java沙箱(sandbox)。什么是沙箱?沙箱就是一个限制程序运行的环境。沙箱机制就是将Java代码限制在JVM特定的运行范围中,防止对本地系统的破坏。
沙箱主要 限制系统资源的访问,系统资源包括CPU ,内存,文件系统,网络,不同级别的沙箱最这些资源的访问限制也不一样。
所有的Java运行环境都可以指定沙箱,可以指定沙箱策略。
六、Class文件格式(ClassFileFormat)
1.字节码文件的跨平台性
1.Java语言: 跨平台语言
当java源代码成功编译成字节码后,如果想在不同的平台上面运行,则无需再次编译,跨平台似乎已成为一门语言必选的特征
2.Java虚拟机:跨平台的语言
Java虚拟机不包括Java在内的任何语言绑定,他只和“Class”文件这种特定的二进制文件所关联,无论实用任何语言进行软件开发,只要是能将源文件编译成正确的Class文件,那么这种语言就可以在Java虚拟机上执行,可以说,统一额而强大的Class文件结构,就是Java虚拟机的即使和桥梁!
java语言每次在发布新版本时都会发布java规范和java虚拟机规范
3.如果想让Java虚拟机正确运行在JVM中,Java源码就必须编译成符合规范的字节码文件
Java的前端编译器的主要任务就是负责这个任务的
javac是一种能够将Java源码编译成字节码的前端编译器。
javac编译器编译经历四个步骤:词法解析,语法解析,语义解析,生成字节码
前端编译器并不是JVM中的一部分,在Hotspot 虚拟机中并没有强制要求只能使用javac编译器,处理javac之外,java虚拟机该有内置在Eclipse中的ECJ编译器,和javac全量式编译不同的是,ECJ使用的是增量式编译器。
前端编译器并不直接涉及到编译优化的技术,而是将具体的优化细节交给HotSpot的后盾编译器JIT编译器负责
2.虚拟机基石
字节码文件面是什么?
源代码经过前端编译器会生成字节码文件,字节码文件就是一种二进制的类文件,它的内容是JVM的指令,而不是像C/C++那样经过编译器直接生成机器码。
什么是字节指令?
Java虚拟机的指令是由一个字节长度,代表着某种特定操作含义的操作码,以及跟随其后的零至多个代表此操作所需参数的操作数所构成,虚拟机中许多指令并不包含操作数,是有一个操作码
如何解读字节码文件?
在idea中安装一个jclasslib插件就可以查看字节码文件
字节码文件结构
Class类的本质
任何class文件都对应着唯一的一个类或者接口的定义信息,但反过来说,Class文件实际上并不一定是一磁盘文件的形式存在,Class文件是一组以8位字节为基础的二进制流。
Class文件格式
class文件采用一种类似于c语言结构体的方式进行数据存储,这种结果中只有两种数据类型,符号和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由下表所示的数据项构成。
语法糖
所谓的语法糖 ,其实就是指 java 编译器把 *.java
源码编译为 *.class
字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利。
1.自动拆装箱,这个功能是jdk5加入的,将基本数据类型装换成包装类型就是装箱,将包装类型转换成基本数据类型就是拆箱,这些操作都是编译器在编译期间生成字节码,里面就包含拆装箱代码。
2.默认构造
在java文件当中进行编写一个不提供构造方法,在进行编译的时候会自动加上一个默认无参构造方法。
3.泛型集合取值
泛型也是jdk5以后添加的特性,但是在java编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码 文件之后就丢失了,实际上的类型都当做 Object来处理。简单来讲,就是泛型类型在编译成字节码文件后已经不区分是Intrger,String还是其他类型,统一当成是Object,泛型信息在这个时候丢掉了。
4.可变参数
也是jdk5引入的新特性,String ...args其实就是一个String [] args
5.foreach循环
还有一些重点不展开介绍
总结
看完课程再写完博客后感觉对JVM的理解变深了很多,虽然这篇文章的深度不够,有错误多点评,后面会修正。