JVM的原理

1 篇文章 0 订阅

JVM

·类加载

·类加载器

 

 

 

 

 

自定义类加载器是通过继承ClassLoader,而不是AppClassLoader

·双亲委派

 

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,下一层类加载器才会尝试自己去加载;

·JDK为什么要设计双亲委派模型,有什么好处?

1、确保安全,避免Java核心类库被修改;

2、避免重复加载;|

3、保证类的唯一性;

如果你写一个jaa.lang.String的类去运行,发现会抛出如下异常;

 

·可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?可以;

要打破这种模型,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双

亲委派即可;

·如何自定义自己的类加载器?

(1)继承ClassLoader

(2)覆盖findClass(String name)方法

findClass(String name)方法不会打破双亲委派;

loadClass()方法可以打破双亲委派;(tomcat就使用了这个方法)

 

·ClassLoader中的loadClass()、findClass()、defineClassO区别?

(1)loadClass()就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;

(2)findClass()根据名称或位置加载.class字节码;

(3)definclass()把字节码转化为Class;

·加载一个类采用Class.forName()和ClassLoader()有什么区别?

 

结果:

 

·使用Class.forName()会对类进行初始化。

·什么都没有,所以使用ClassLoader()不会对类进行初始化。

 

对于ClassLoader(),只有对类进行实例化时候,才会进行初始化

 

 

类什么时候加载?

 

 

只有调用类的成员,childClass才会加载

 

 

 

·你了解Tomcat的类加载机制吗?

可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和每个Web应用的类加载器+JSP类加载器;3个基础类加载器在conf/catalina.properties中进行配置。

Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派的机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类,我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,我们可以看到WebAppClassLoader类中重写了loadClass方法;

·为什么Tomcat要破坏双亲委派模型?

Tomcat是web容器,那么一个web容器可能需要部署多个应用程序;

1、部署在同一个Tomcat上的两个Web应用所使用的Java类库要相互隔离;

2、部署在同一个Tomcat上的两个Web应用所使用的Java类库要互相共享;

3、保证Tomcat服务器自身的安全不受部署的Web应用程序影响;

4、需要支持JSP页面的热部署和热加载;

·热加载和热部署,如何自己实现一个热加载?

热加载是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于Java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境;

热部署是指可以在不重启服务的情况下重新部署整个项目,比如Tomcat热部署就是在程序运行时,如果我们修改了War包中的内容,那么Tomcat就会删除之前的 War包解压的文件夹,重新解压新的War包生成新的文件夹;

1、热加载是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变;

2、热部署是在运行时重新部署整个项目,耗时相对较高;

·如何实现热加载呢?

在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重

新进行加载;

1、实现自己的类加载器;

2、从自己的类加载器中加载要热加载的类;

3、不断轮训要热加载的类class文件是否有更新,如果有更新,重新加载;

· Java代码到底是如何运行起来的?(3种方式)

 

 java.exe启动jvm

1、Test.java --javac--> Test.class --> java Mall (jvm进程,也就是一个jvm虚拟机)

2、Test.java --javac--> Test.class -->Test.jar --> java -jar Test.jar

3、Mall.java--javac---> Mall.class -->Mall.war -->Tomcat --> startup.sh 

-->org.apache.catalina.startup.Bootstrap (vm进程,也就是一个jvm虚拟机)

其实运行起来一个Java程序,都是通过D:\devJavaNjdk1.8.0_251\binyjava 启动一个JVM

虚拟机,在虚拟机里面运行Mall.class字节码文件;

 

·JVM整个运行原理图

 

Class file(Mall.class字节码文件)

(1)线程私有区域(不会发生线程安全问题,一个线程对应以下3份):

·虚拟机栈存储:方法,局部变量(int a=10,new出来一个user),运行数据(a=1,a+b=c)

·本地方法栈存储:Native方法(c++)

·程序计数器:执行下一行代码(代码行号)

(2)线程共享区域(可能会发生线程安全问题):

·堆存储:对象(user的值),数组

·元空间(方法区):存放Mall.class字节码文件或者虚拟机加载的字节码数据、静态变量、常量、运行时常量池

·从JVM角度剖析如下程序代码如何执行?

 

 

 

其中还有未标出来的:

堆:对象new Manager 内有private int a;

元空间:Application.class字节码文件,config.class字节码文件

·JVM运行时数据区程序计数器的特点及作用?

1、程序计数器是一块较小的内存空间,几乎可以忽略;

2、是当前线程所执行的字节码的行号指示器;

3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;

4、该区域是“线程私有”的内存,每个线程独立存储;

5、该区域不存在OutOfMemoryError;

6、无GC回收;

· JVM运行时数据区虚拟机栈的特点及作用

 

 

1、线程私有;

2、方法执行会创建栈帧,存储局部变量表等信息;栈帧(局部变量表,操作数栈,动态连接,返回地址)

3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)

4、栈深度大于虚拟机所允许StackOverflowError;(递归导致)

5、栈需扩展而无法申请空间OutOfMemoryError (比较少见,比如创建线程太多); hotspot虚拟机这种情况非常少;

6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(对象值等)都存放到堆上的;

7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置

默认为1M;

8、随线程而生,随线程而灭;

9、该区域不会有GC回收;

·JVM运行时数据区本地方法栈的特点及作用?

1、与虚拟机栈基本类似;

2、区别在于本地方法栈为Native方法(c++实现的方法)服务;

3、HotSpot虚拟机将虚拟机栈和本地方法栈合并;

4、有StackOverflowError和 OutOfMemoryError (比较少见) ;

5、随线程而生,随线程而灭;

6、GC不会回收该区域;

程序计数器虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;(私有区)

·JVM运行时数据区Java堆的特点及作用?

 

1、线程共享的一块区域;

2、虚拟机启动时创建;

3、虚拟机所管理的内存中最大的一块区域;

4、存放所有实例对象或数组;

5、GC垃圾收集器的主要管理区域;

6、可分为新生代、老年代;

7、新生代更细化可分为Eden、From Survivor:To Survivor:Eden:Survivor=8:1:1

8、可通过-Xmx、-Xms调节堆大小;(例如-Xmx1m、-Xms1m)

9、无法再扩展java.lang.OutOfMemoryError: Java heapspace(堆溢出,对象创建太多)

10、如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率;

 

即:把堆再划分为:公共去,私有区(缓冲区)

·JVM中对象如何在堆内存分配(3种方式)?

  1. 指针碰撞(Bump The Pointer):内存规整的情况下;           

 

·因此,当使用Serial、ParNew等带压缩整理过程(弄的很整齐)的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;

2、空闲列表(Free List) :内存不规整的情况下;

 

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否

带有空间压缩整理(Compact)的能力决定;

·而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲

列表来分配内存;

3、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)∶对象创建在虚拟机中频

繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存

的情况;

解决方案(2种):

(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;

(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的

缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;

·JVM堆内存中的对象布局?

在HotSpot虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)实例数据

(Instance Data)对齐填充(Padding);

对象头(Header):包含两部分,

第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,32位虚拟机占32 bit,64位虚拟机占64 bit,官方称为‘'Mark Word’;

 

第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过Java 对象元数据确定大小,而数组对象不可以;

实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot 虚拟机,任何对象的大小都是8字节的整数倍;

·JVM什么情况下会发生堆内存溢出?

Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出;

MAT工具分析xxx.hprof 文件,排查溢出的原因;

·堆溢出

 

·堆没有溢出

 

·JVM 如何判断对象可以被回收?

在JVM堆里面存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还“存活”着,哪些已经“死去”;

Java通过可达性分析(Reachability Analysis)算法来判定对象是否存活的;

该算法的基本思路:通过一系列称为“GCRoots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收;

 

对象object 5、object 6、object7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象;

·哪些对象可以作为GC Roots呢?

1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;

2、方法区/元空间中的类静态属性引用的对象;

3、方法区/元空间中的常量引用的对象;

4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象;

5、Java 虚拟机内部的引用,如基本数据类型对应的Class 对象,一些常驻的异常对象(比如

NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;

6、所有被同步锁(synchronized关键字)持有的对象;

7、反映Java 虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;

8、其他可能临时性加入的对象;

·谈谈Java 中不同的引用类型?

Java里有不同的引用类型,分别是强引用、软引用、弱引用和虚引用;

强引用:Object object = new Object() ;

软引用: SoftReference内存充足时不回收,内存不足时则回收;

弱引用: WeakReference不管内存是否充足,只要GC一运行就会回收该引用对象;

 

虚引用: PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有

引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;

·JVM堆内存分代模型?

JVM堆内存的分代模型:年轻代、老年代;

大部分对象朝生夕死,少数对象长期存活;

 

JVM里垃圾回收针对的是新生代,老年代,还有元空间/方法区(永久代),

不会针对方法的栈帧进行回收,方法一旦执行完毕r栈帧出栈,里面的局部变量直接就从内存

里清理掉,也就是虚拟机栈不存在垃圾回收;

代码里创建出来的对象,一般就是两种:

1、一种是短期存活的,分配在Java 堆内存之后,迅速使用完就会被垃圾回收;

2、一种是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用;

第一种短期存活的对象,是在Java堆内存的新生代里分配;

第二种长期存活的对象,通过在新生代SO区和S1区来回被垃圾回收15次后,进入Java堆内存的老年代中,这里的15次,我们也称为对象的年龄,即对象的年龄为15岁;

回收过程:

  1. 新创建的对象会放到Eden区,直到Eden区满之后,;
  2. 触发Minor GC回收,并且把还有引用的对象放入S0区,其他的全部回收;Minor GC回收一次该存活对象年龄+1
  3. 然后又进行(1),把在Eden区,S0区中还有引用的对象放入S1区,其他的在Eden区,S0区没有引用的对象全部回收。
  4. 然后又进行(1),把在Eden区,S1区中还有引用的对象放入S0区,其他的在Eden区,S1区没有引用的对象全部回收。
  5. 无限进行(3),(4)循环。(存活的对象在S0,S1之间换来换去)
  6. 如果,一个对象在S0,S1之间进行了15次Minor GC回收(一般情况15),则把这个对象放入老年区。

·JVM对象动态年龄判断

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升老年代;

结论-->动态年龄判断: Survivor区的对象年龄从小到大进行累加,当累加到X年龄时的总和大于50%(可以使用-XX:TargetSurvivorRatio=?来设置保留多少空闲空间,默认值是50),那么比×大的都会晋升到老年代;

即:第一次在S0,S1内对象总大小恰好达到空间的50+%,此时还不触发进入老年区的,第二次在

S0,S1内达到50%之后的老年人就进入老年区;

 

 

 ·老年代空间分配担保机制

存活对象太多,要挤到老年区才能保证的情况下:

 

新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?

  1. 执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;

新生代所有对象的总大小比较老年代可用内存空间

2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;

3、如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC";所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC;

4、如果FullGC之后,老年代还是没有足够的空间存放 Minor GC过后的剩余存活对象,么此时就会导致“OOM”内存溢出;

·什么情况下对象会进入老年代?

1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;

2、动态对象年龄判断;

3、老年代空间担保机制;

4、大对象直接进入老年代;

大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合;

大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们而复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;

我们可以通过JVM参数“-XX:PretenureSizeThreshold”设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放老年代,而不会经过新生代;-XX:PretenureSizeThreshold参数只对Serial和 ParNew两款新生代收集器有效,其新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑 ParNew+CMS 的收集器组合;

·JVM运行时数据区元空间

  1. 在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;

2、元空间与Java 堆类似,是线程共享的内存区域;

3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;

4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;

-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;

6、元空间内存不足时,将抛出OutOfMemoryError;

·JVM本机直接内存

1、直接内存(Direct Memory) 不属于JVM运行时数据区,是本机直接物理内存; .

2、像在JDK 1.4中新加入了NIO (New Input/Output)类,一-种基 于通道(Channel) 与

缓冲区(Buffer) 的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过-个

存储在Java堆中的DirectByteBuffer 对象作为这块内存的弓|用进行操作,这样能在一-些场景

中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;

3、可能导致OutOfMemoryError异常出现;

·在准备阶段:

 

·解析阶段

把符号引用翻译为直接引用

·初始化

初始化:当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化.....那么这些都会触发类的初始化;

 

 

 

·卸载

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值