Java进阶-jvm(二)

本文详细介绍了Java虚拟机(JVM)的类加载子系统,包括类加载的过程、类加载器的分类以及双亲委派机制。接着讲解了JVM运行时数据区,包括程序计数器、虚拟机栈、本地方法栈、Java堆内存和方法区的详细信息。文章还探讨了JVM的本地方法接口和执行引擎,包括解释器和JIT编译器的作用,解释了为什么Java是半编译半解释型语言。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM结构-类加载

类加载子系统

 

类加载器子系统负责从文件系统或者网络中加载class文件.classLoader只负责class文件的加载,由Execution Engine决定是否可以运行.加载的类信息存放于一块方法区称为(元空间)的内存空间.

类加载的角色

 

1.Class File存在于硬盘上,而最终这个模板再执行的时候要加载JVM当中来,根据这个模板实例化出n个一模一样的实例.

2.class file 加载到JVM中,被称为DNA元数据模板,放在方法区.

3.在.class-->JVM-->最终称为元数据模板,此过程就要有一个运输工具(类加载器Class Loader)扮演一个快递员的角色.

类加载的过程

 

加载

1.通过类名(地址)获取此类的二进制字节流.

2.将这个字节流锁代表的静态存储结构转换为方法区(元空间)的运行时结构.

3.在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的访问入口.

链接

验证:检验别加载的类是否有正确的内部结构,并和其他类协调一致;

验证文件格式是否一致:class文件在文件开头有特定的文件标识(字节码文件都以CA FE BA BE 标识开头);主,次版本号是否在当前Java虚拟机接受范围内.

原数据验证:对字节码描述的信心进行语义分析,以保证其描述的信息符合java语言规范的要求,例如这个类是否有父亲;是否继承浏览不允许被继承的类(final修饰的类)...

准备:准备阶段则负责为类的静态属性分配内存,并设置默认值;不包含final修饰的static常量,在编译时进行初始化.

类什么时候初始化

1.创建类的实例,new 一个对象.

2.访问某个类或接口的静态变量,或者对该静态变量赋值.

3.调用类的静态方法.

4.反射(Class.forName("")).

5.初始化一个类的子类(会首先初始化子类的父类).

类的初始化顺序

父类static->子类static->父类构造方法->子类构造方法.

对static修饰的变量或语句块进行赋值,如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序一次执行.

类加载器的分类

站在JVM角度看,类加载器可以分为两种:

1.引导类加载器(启动类加载器Bootstrap ClassLoader).不是用java语言写的.

2.其他所有类加载器.指java语言写的其他类加载器.

而站在java开发人员角度来看,类加载器分为:

1.引导类加载器:

这个类加载器使用c/c++语言实现,嵌套在JVM内部,它用来加载java核心类库,存放在<JAVA_HOME>\lib目录.并不继承java.lang.ClassLoader,没有父加载器.加载扩展类加载器和应用类加载器,并未他们指定父类加载器.

2.扩展类加载器

Java语言编写,派生与ClassLoader类由sun.misc.Launcher$ExtClassLoader实现.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext字目录下加载类库.如果用户创建的Jar放在此目录下,也会由此加载器加载.

3.应用程序类加载器

Java语言编写,派生于ClassLoader类由sun.misc.Launcher$AppClassLoader实现.加载我们自己定义的类,用于加载用户类路径上所有的类,该类加载器是程序中默认的类加载器.

ClassLoader类,他是一个抽象类,其后所有的类加载器都继承它.(不包括启动类加载器)

双亲委派机制

 

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行.若是父类还存在父类,继续向上委托,直到请求到达顶层的启动类加载器.

如果父类加载器可以完成,就成功返回,若父类加载器无法完成,子加载器才会自己去加载,如果最后到没加载成功,就会抛出ClassNotFoundException异常.

优点:

1.安全,可避免用户自己编写的类动态替换Java的核心类.

2.避免全限定命名的类重复加载(使用了 findLoadClass()判断当前类是否加载)

类的主动使用/被动使用:

主动使用:

1.通过new关键字导致类的初始化,是大家经常使用的初始化一个类的方式,new 类()肯定会导致类的加载并且初始化.

2.访问类的静态变量,包括读取和更新.

3.访问类的静态方法.

4.对某个类进行反射操作,会导致类的初始化.

5.初始化子类会导致父类的初始化.

6.执行该类的main函数.

被动使用:

1.引用该类的静态常量.final修饰

2.构造某个类的数组时不会导致该类的初始化.

JVM运行时数据区

java8虚拟机规范规定,Java虚拟机锁管理的内存将会包括一下几个运行时数据区域. 1.程序计数器:记录线程运行的位置(行号),线程需要切换执行,锁以需要记录执行的位置.

2.虚拟机栈:运行java方法的区域,每一个方法生成一个栈帧.

3.本地方法栈:java经常需要调用一些本地方法(操作系统的方法hashCode(),read(),start(),arraycopy()).

4.:存放程序中产生的对象,也是虚拟机内存占比最大的一块.

5.方法区:存放类信息

堆,方法区:是线程所共享的.

程序计数器,虚拟机栈,本地方法栈:是线程私有的,线程独立的.

堆,方法区,虚拟机栈,本地方法栈:会出现内存溢出错误.

程序计数器

一块很小的内存空间,也是运行速度最快存储区域.主要用来记录每个线程中执行的执行位置,便于线程在切换执行时记录位置.

是线程私有的,生命周期与线程一样,运行速度快,不会出现内存溢出.

他是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器完成.

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令.

虚拟机栈

java虚拟机栈,早期也叫java栈,每个线程在创建时都会创建一个虚拟机栈,器内部保存一个个栈帧,对应一次方法的调用.

操作只有两个,调用方法,入栈,方法完成后,出栈,先进后出的结构.

运行速度非常快,仅此与程序计数器,当入栈的方法过多时,会出现栈溢出(内存溢出).

线程独立的,不用线程之间的方法不能相互调用.

栈帧的内部结构

局部变量表:

一组变量值存储空间,存放方法参数和方法内部定义的局部变量,对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用.

操作数栈:

就是用来对表达式求值,在一个线程执行方法的过程中,就是不断执行语句的过程,就是进行计算的过程,程序中所有的计算过程都是借助与操作数栈来完成.

动态连接(或指运行时常量池的方法引用):

调用的方法地址,字面量地址.

方法返回地址:

当一个方法执行完成之后,要返回之前掉用它的地方,栈帧中保存一个方法返回地址.

本地方法栈

java虚拟机栈管理Java方法的调用,而本地方法栈用于管理本地方法的调用.C语言实现的.

本地方法栈也是线程私有的.

允许被实现成固定或者是可动态扩展的内存大小,内存溢出方面也是相同的.如果线程请求分配的栈容量超过本地方法栈允许的最大容量抛出StackOverflowError.

它的具体做法是在Native Method Stack中登记Native方法.在Execution Engine执行时加载本地方法库.

Java堆内存

一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域.

Java堆区在JVM启动时被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间.但是大小可以通过参数调节.

《java虚拟机规范》规定,对可以处于物理上不连续的内存空间中,但逻辑上他应该被视为连续的.

所有的线程共享java堆,还可以划分线程私有的缓冲区.

《java虚拟机规范》对java堆的描述是:所有对象实例都应当在运行时分配在堆上.

在方法结束后,堆中的对象不会马上被删除,仅仅在垃圾收集的时候才会被删除.堆是垃圾回收的重点区域.

堆内存区域划分

java8之后堆内存分为:新生代+老年代

新生代分为Eden(伊甸园)和Survivor幸存者区(Survivor 0(From) Survivor 1(to))

 

为什么分区(代)?

将对象根据存活概率进行分类,对象存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率.针对分裂进行不同的垃圾回收算法,对算法扬长避短.

对象创建内存分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的实际者不仅需要考虑内存如何分配,在哪分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完成内存回收后是否会在内存中产生内存碎片.

1.new 的新对象先放到伊甸园区,大小有限制.

2.当伊甸园的空间填满时,程序又需要创建对象时,JVM的垃圾回收器对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被引用的对象进行销毁,在加载新的对象放到伊甸园区.

3.然后将伊甸园区中的剩余对象移动到幸存者0区.

4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的对象,如果没有回收,就会被放到幸存者1区,每次会保证有一个幸存者区是空的.

5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区.

6.默认是15次之后对象还没被销毁就从新生代转到老年区.在对象头中,它是由4位数据对GC年龄进行保护的,所以最大值为1111即为15.

7.在老年代,相对悠闲,当老年代内存不足时,在此触发Major GC进行老年代的内存清理.

8.若老年代执行了Major GC之后发现依然无法进行对象保存,就会产生OOM异常,Java.lang.OutOfMemoryError:Java heap space;

分代收集思想Minor GC(Yong GC),Major GC(Old GC),Full GC

JVM在进行GC时,并非每次都将新生代和老年代一起回收,一般回收的都是指新生代,针对HotSpot VM的实现,它里面的GC按照回收区域分为两大类型:一种是部分收集,一种是整堆收集.

新生代收集(Minor GC/Yong GC):只是新生代的垃圾收集.

老年代收集(Major GC/Old GC):只是老年代的垃圾收集.

整堆收集(Full GC):收集整个java堆和方法区的垃圾收集.

当主动调用System.gc(),老年代空间不足,方法区空间不足.时会出现整堆收集的情况.(开发期尽量避免整堆收集).

字符串常量池

JDK7之后的版本中将字符串常量池放到了堆空间中,因为方法区的回收效率低,在Full GC 的时候才会执行垃圾回收,而一般垃圾回收回收的是新生代的收集,Full GC垃圾回收频率低,导致方法区的回收效率低.而会导致永久代内存不足,放到堆中可以及时回收.

方法区

方法区,是一个被线程共享的内存区域,其中主要存储加载的类字节码,class/method/field等元数据,,static final常量,static变量,即使编译器编译后的代码等数据,方法区还包含了一个特殊的区域"运行时常量池".尽管所有的方法区在逻辑上属于堆的一部分,但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开.

方法区在JVM启动时被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的,可以选择固定大小或者可扩展大小.

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样也会抛出内存溢出的错误

关闭JVM就会释放这个区域的内存.

方法区,栈,堆的交互关系

 

 

java栈存储基本类型的值,而引用类型存储对象的引用.

java堆存储的是代码产生的对象.

方法区存储已被虚拟机加载的类型信息,常量,静态变量,即使编译后的代码缓存,运行常量池.

方法区的垃圾回收

方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型.

下面也称作卸载

判定一个常量是否"废弃",还是相对简单,而要判定一个类是否属于"不再被使用的类"的条件就比较苛刻了,需要同时满足下面三个条件:

1.该类的所有的实例都已被回收,也就是Java堆中不存在该类及其任何派生子类的实例.

2.加载该类的类加载器已经被收回,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi,JSP的重加载等,通常时很难达成的.

3.该类对应的java.lang.class对象没有在任何地方引用,无法在任何地方通过反射访问该类的方法.

本地方法接口

简单来说,一个Native Method就是一个java调用的非java代码的接口,一个Native method是这样一个java方法,该方法的底层实现由非java语言实现,这个特征并非java特有,很多其他的编程语言都有这一机制在定义一个native method时,并不提供实现体,因为其实现体是由非java语言在外面实现的.关键字native可以与其他所有的java标识符连用,但是abstract除外.

为什么要使用Native Method

1.与java环境外交互

有时java应用需要与java外面的环境交互,这是本地方法存在的主要原因.因为可以想想java需要一些底层系统,如某些硬件交换信息时的情况.本地方法正式这样的一种交流机制,它为我们提供了一个非常简洁的接口,而且我们无需区了解java应用之外的繁琐细节.

2.Sun's java

Sun的解释器是用C实现的,这使得他能像一些普通的C一样与外部交互.jre大部分是用Java实现的,它也通过一些本地方法与外界交互.例如:类java.lang.Thread的setPriority()方法使用Java实现的,但是它实现调用的事该类里的本地方法setPriority0().

执行引擎

执行引擎是Java虚拟机核心组成部分之一.

JVM的主要任务时负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅是一些能够被JVM所识别的字节码指令,符号表,以及其他辅助信息.

那么如果想要让一个java程序运行起来,执行引擎的任务就是将字节码指令解释/编译为对应平台上的本地及其指令才可以,简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者.

1.前端编译:从java程序员-字节码文件的这个过程叫前端编译.

2.执行引擎这里有两种行为:一种是解释执行,一种是编译执行(这里是后端编译).

什么是解释器?什么是JIT编译器

解释器:当java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容"翻译"为对应平台的本地机器指令执行.

JIT(Just In Time Compiler)编译器:就是虚拟机将源代码一次性直接编译成和本地机器相关的机器语言,但并不会马上执行

为什么Java是半编译半解释型语言?

起初将Java语言定位为"解释执行"还是比较准确的.在后来,Java业发展出可以直接生成本地代码的编译器.现在JVM执行代码时,通常将解释执行和编译执行二者结合起来进行.

原因

JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台性,因此避免采用静态编译的方式由高级语言直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法.

解释器真正意义上所承担的角色就是一个运行时"翻译者",将字节码文件中的内容"翻译"为对应平台的本地机器指令执行,执行效率低.

JIT编译器将字节码翻译成本地代码后,就可以做一个缓存操作,存储在方法区的JIT代码缓存中(执行效率更高了)

是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定.

JIT编译器在运行时会针对那些频繁被调用的"热点代码"做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能.

一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为"热点代码".

目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测.

JIT编译器执行效率高为什么还需要解释器?

1.当程序启动后,解释器可以马上发挥作用,响应速度快,省去编译的时间,立即执行.

2.编译器想要发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高.就需要采用解释器与即时编译器并存的架构来换取一个平衡点.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值