赞
踩
⚫ 面试的需要:入职 BATJ、TMD、PKQ 等一线大厂不光关注技术的广度,更关注
技术的深度,JVM 技术是大厂面试的必备技能,掌握越深越好
⚫ 中高级程序员、架构师必备技能:架构师每天都在思考如何让我的系统更快,
如何避免系统出现性能瓶颈。单纯的依靠物理机不足以解决问题,分析系统
性能、调优系统瓶颈离不了对 JVM 中内存、垃圾回收、字节码指令、性能监
控工具、调优参数的熟练掌握。
⚫ 精进技术、极客追求:JVM 是 Java 生态的核心价值的体现,垃圾回收算法、
JIT、底层原理值得每个程序员去探索。同时,JVM 作为跨语言的平台,对于
深入理解 Scala、Kotlin、JavaScript、Jython、Groovy 也很有帮助
Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。
具体来说:这两种架构之间的区别:
总结:
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
时至今日,尽管嵌入式平台已经不是Java程序的主流运行平台了(准确来说应该是HotSportVm的宿主环境已经不局限于嵌入式平台了),那么为什么架构更换为基于寄存器的架构呢?
栈:
跨平台性、指令集小、指令多;执行能力比寄存器差
Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行
虚拟机的退出
有如下的几种情况:
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在这行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。
简图
详细图
public class HelloLoader{
public static void main(String[] args){
System.out.println("谢谢ClassLoader加载我...");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
加载:
补充:加载.class文件的方式
链接:
验证(Verify):
准备(Prepare):
解析(Resolve):
初始化:
局部变量表也称为本地变量表,是存在栈中的,是线程私有的所以不存在安全问题
关于solt的理解
public class MianTest {
public void test(){
int a=1;
long b=2L;
double c=0;
}
}
1.非static方法默认0是this
2.double和long类型占两个槽,其他都只占1个槽
3.局部变量表所需的容量大小是在编译期确定下来的
4.局部变量表中的变量只能在当前方法调用中有效。当方法调用结束时,随着方法栈帧的销毁,局部变量表也会随之销毁。
启动类加载器(引导类加载器,Bootstrap ClassLoader)
优势
● 避免类的重复加载
● 保护程序安全,防止核心API被随意篡改
○ 自定义类:java.lang.String
○ 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是先把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
1.开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现 自己的类加载器,以满足一些特殊的需求
2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
防止恶意代码污染java源代码
比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染
概念:
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
作用:
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
● CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
也被称作局部变量数组,或者本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
● 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
● 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的LocalVariableTable数据项中。在方法运行期间是不会改变局部变量表的大小的。
● 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
● 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
说明:
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
每一个栈帧都对应着一个局部变量表,每个方法都是一个栈帧
public class DynamicLinkingTest {
int num;
String info;
public void test1(){
String a="";
info="JVM";
this.test2();
}
public void test2(){
num=2;
}
}
byte,short,char在存储前被转换位int,boolean也被转换位int(0表示false,非0表示true)。
long和double占据两个Slot
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
虚拟机中提供了以下几条方法调用指令:
关于invokedynamic指令
动态类型语言和静态类型语言:
堆是存储Java创建对象的地方,堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
jdk1.8的时候jvm有很大的改进:使用元空间(mate space)取代了永久代。虽然元空间逻辑上仍然可以视为方法区的一种实现,但是在jdk1.8的jvm里面却没有给予方法去单独的一块内存区域了(使用直接内存,不在由JVM分配空间)。
在jdk1.7的版本开始,已经开始了一部分去永久代的工作。比如字符串常量池迁移到堆内存中。而1.8则更进一步,把方法区的运行时常量池等信息全部迁移到了本地内存(native memory)之中
Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
● Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
● Tenure generation space 养老区 Old/Tenure
● Meta Space 元空间 Meta
IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
几乎所有的new 出来的对象都放在Eden区(除了大对象当Eden放不下的时候会直接放到Old区)
HotSpot垃圾收集器
jdk1.7和1.8默认采用ParallelGC,也就是Parallel Scavenge(新生代)+Parallel Old(老年代)GC。 jdk9采用G1垃圾收集器。
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
(这里说的都是针对HotSport虚拟机)
按照回收区域又分为两大种类型:
Minor GC 是俗称,新生代(新生代分为一个 Eden区和两个Survivor区)的垃圾收集叫做 Minor GC
触发条件:
当 Eden 区的空间耗尽了怎么办?这个时候 Java虚拟机便会触发一次 Minor GC来收集新生代的垃圾,存活下来的对象,则会被送到 Survivor区。
简单说就是当新生代的Eden区满的时候触发 Minor GC (Minor GC会回收Survivor区 s0区、s1区、Eden区,Survivor区 满了的时候不会触发GC,Survivor区是被动的回收)
Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
Major GC 是清理老年代。(只是老年代的垃圾收集)
目前只有CMS GC会有单独收集老年代的垃圾收集
注意:很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
触发条件:
● 老年代空间不足时,会先尝试触发Minor Gc。如果之后空间还不足,则触发Major GC。
● Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长 。
● 如果Major GC后,内存还不足,就报OOM了。
出现了Major GC 经常会伴随至少一次的Minor GC (但非绝对),较少发生,执行速度较慢(MajorGC 的速度一般会比 Minor GC 慢 10倍以上。)
收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式
触发条件:
按HotSpot VM的serial GC的实现来看,触发条件是:
不同对象的生命周期不同。70%-99%的对象是临时对象。
● 新生代:有Eden、两块大小相同的survivor(又称为from/to,s0/s1)构成,to总为空。
● 老年代:存放新生代中经历多次GC仍然存活的对象。
分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
总结:
如果对象在Eden出生并经过第 一次MinorGC后仍然存活,并且能被Survivor 容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在 Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中,对象晋升老年代的年龄阈值,可以通过选项-XX:MaxTenuringThreshold来设置
针对不同年龄段的对象分配原则如下所示:
● 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据。
●由于对象实例的创建过程在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
● 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题 同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称 之为快速分配策略。
据宋红康所说所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。