JVM加载class文件的原理
JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。 Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。 类装载方式,有两种 (1)隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,利用反射即隐式加载可绕过一些权限检查机制。 (2)显式装载,通过class.forname()等方法,显式加载需要的类 ,隐式加载与显式加载的区别:两者本质是一样的。 java中类加载是动态的,并不会一次性把所有的类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
答:Java虚拟机是一个可以执行java字节码的虚拟机进程,java源文件被编译成Java虚拟机执行的字节码文件。 Java被设计成的应用程序可以运行在任何平台,不需要程序员的在不同的平台进行不同的重写和编译,因为它知道底层硬件平台的指令长和其他特性。
(1) 堆内存分配 JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小 于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、 -Xmx相等以避免在每次GC后调整堆的大小。
(2)非堆内存分配 JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
(3)VM最大内存 首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽 然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系 统下为2G-3G),而64bit以上的处理器就不会有限制了。
(4)下面是当前比较流行的几个不同公司不同版本JVM最大堆内存:
线程是比进程更轻量级的调度执行单位。线程可以把一个进程的资源分配和执行调度分开。一个进程里可以启动多条线程,各个线程可共享该进程的资源(内存地址,文件IO等),又可以独立调度。线程是CPU调度的基本单位。 主流OS都提供线程实现。Java语言提供对线程操作的同一API,每个已经执行start(),且还未结束的java.lang.Thread类的实例,代表了一个线程。 Thread类的关键方法,都声明为Native。这意味着这个方法无法或没有使用平台无关的手段来实现,也可能是为了执行效率。 Thread类的关键方法,都声明为Native。这意味着这个方法无法或没有使用平台无关的手段来实现,也可能是为了执行效率。
实现线程的方式
A.使用内核线程实现内核线程(Kernel-Level Thread, KLT)就是直接由操作系统内核支持的线程。 内核来完成线程切换 内核通过调度器Scheduler调度线程,并将线程的任务映射到各个CPU上 程序使用内核线程的高级接口,轻量级进程(Light Weight Process,LWP)
B.用户态和内核态切换消耗内核资源 使用用户线程实现 系统内核不能感知线程存在的实现 用户线程的建立、同步、销毁和调度完全在用户态中完成 所有线程操作需要用户程序自己处理,复杂度高 用户线程加轻量级进程混合实现 轻量级进程作为用户线程和内核线程之间的桥梁
拓展:
实际上操作系统会比上述的更复杂,这里只是个大概,我们可以发现一次切换经历了「用户态 -> 内核态 -> 用户态」。
java内存模型(Java Memory Model)JMM,JMM决定一个线程对共享变量的写入何时对另一个线程可见,从抽象的角度看,JMM定义了线程和主内存之间的抽象关系,线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读、写共享变量的副本。 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。其关系模型图如下图所示:
可达性分析:就是从GCROOT中看是否能能够到达某个对象,如果不能到达,则说明是不可达的。
即使在可达性分析算法中不可达的对象,也并非是“非回收不可”的,这时候它们暂时处于“等待”阶段,要真正宣告一个对象回收,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱回收的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中跳出回收——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
解释运行字节码程序消除平台相关性。 jvm将字节码解释为具体的平台指令,一般的高级语言如要在不同的平台执行,需要编译成不同的目标代码。而引入jvm之后,java语言在不同的平台上运行时不需要重新编译。Java语言使用模式java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
jvm虚拟机实现都采用了分代收集的思想,把整个堆区划分成新生代和老年代,新生代被划分成Eden空间,From Survivor 和 To Survivor三块区域这三块区域的容量比为8:1:1。
我们把Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出生, From Survivor 保存当前的幸存对象, To Survivor 为空。一次 gc 发生后: 1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ;2) 清空 Eden 和 From Survivor ; 3) 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。循环往复前边的3个过程。可以看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。
java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。
JNI
:Java Native Interface,一句话来说就是Java的本地接口,很多方法由Java本身来实现的,有的时候还需要本地方法来提供相关的功能。
区别:JNI对应的接口实际上就是我们看源码经常遇到的native修饰的方法,这样的方法也很独特,跟接口里面的方法一样,没有方法的具体实现,用native来修饰。
JNI的实现
:
实际上是由C或者C++语言来实现,为了和其它的方法实现做个对比,我先来一个JVM的图,Java方法栈针对的是Java的方法栈,而本地方法栈针对的就是针对本平台一些C实现的方法等的实现.
JNI的C语言方法加载分为两种:主动型和被动型。
主动型:JVM会根据native方法去找对应的C语言,JVM查找是根据一定命名规则来查找,毕竟Java和C是不同的语言,有不同的规范.
被动型:首先要有一个“主动”的C函数方法实现,被JVM加载,然后吊起的方法里面会有其他对应的native方法实现,于是这些方法没有按照规则命名,被动的加载进来。
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。因为双亲使用不同的类加载器得到最后的同样的object对象
。
垃圾回收算法: