原博
在早期的 Java 编程中,同一个项目在不同的操作系统运行开发需要不同的版本的 Java 代码,为了解决这一问题,开发了 JVM 即 Java 虚拟机,不同的平台有自己的虚拟机,因此只要将编译好的字节码文件加载到不同平台上的 JVM 虚拟机上,然后由JVM 负责将这些字节码文件翻译成特定平台下的机器码然后运行。
也就是说,只要在不同的平台安装对应的 JVM,就可以运行字节码文件,执行我们的 Java 代码,从而实现 Java 的跨平台特性。
JVM 的组成有类加载器、运行时内存、执行引擎、本地库接口。
可以通过执行 “java Main” 程序来看其组成及重要作用:
对于 OS 来说,执行这个指令只是开启了一个进程,这个指令它得先用 JVM 的类加载器将磁盘中的字节码文件加载 JVM 的运行时内存中,而字节码文件只是 JVM 的一套指令集规范,并不能交给底层的 OS 去执行,因此需要由特定的执行引擎将字节码文件翻译成特定平台下的底层系统指令,再交给操作系统的 CPU 去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。
JVM 在执行 Java 程序的时候,会把它所管理的运行时内存这一区域划分为若干个不同的数据区域。这些区域有各自的用途、以及创建和销毁的时间。有的区域会随着 JVM 启动而存在,有些区域则是依赖线程的启动和结束而创建和销毁。
JVM 规范并没有具体规定运行时内存的实现,以下用 HotSpot 举例,运行时内存 可划分为以下几个区域:
运行时内存中的数据主要有两个来源:
其中,程序计数器和栈是每个线程所私有的,堆、方法区和线程池的所有线程共享的。
浅拷贝:只是增加了一个引用指向已存在的内存地址;生成一个引用,引用指向和拷贝的目标都指向堆中同一个对象。
深拷贝:是增加了一个指针,并申请了一个新的内存,使这个增加的指针指向这个新的内存;先在堆中生成一个新的对象,这个对象和拷贝目标所指向的对象完全一致,再生成一个引用,用来指向这个对象
用一个例子来讲,现在有一个路由器类,这个类中,有一个属性为String类型的 name,又有一个电脑类,类中有一个属性为 路由器类型的route。
先使用代码,创建一个路由器对象a,它的属性name为”hello“,再创建一个电脑对象b,这个对象中的属性 route 指向之前创建的路由器对象a。
进行浅拷贝,新生成一个电脑对象b1,这个对象中的属性route,指向之前创建的路由器对象a,此时在JVM 内存中只有一个路由器对象,有两个引用b 和 b1 都指向它,这个过程就叫做浅拷贝。
进行深拷贝,还是之前的路由器对象a,电脑对象b,新生成一个路由器对象a1,它的属性name 为“world”,再生成一个电脑对象b1,它的属性route指向新生成的路由器对象a1。此时在 JVM 中有两个路由器对象a 和 a1,他们的 name 属性分别为“hello”“world”,两个电脑对象b 和 b1,这个过程就叫做深拷贝。
内存泄漏指的是不再被使用的对象或者变量一直被占据在内存中,导致该对象不能被回收。从理论上讲Java有GC 垃圾回收机制,也就是说不再使用的对象会被回收掉,从内存中清除。
但是,即使如此也还是会有内存泄漏的情况,比如:长生命周期的对象持有短生命周期对象的引用,即使短生命周期的对象已经不再需要,但是因为长生命周期对象的持有导致他不能被回收,这就是Java中内存泄漏发生的场景。
常见的内存泄漏:
1)单例造成的内存泄漏:单例模式属性的静态特征使得其生命周期与应用的声明周期一样长,如果一个对象已经不再需要使用了,而单例模式的类还持有该对象的引用,就会造成该对象的不正常回收。
2)非静态内部类创建静态外部类实例造成的内存泄漏:非静态内部类默认会持有外部类的引用,而该非静态内部类中又创建了一个静态的外部类实例,该实例的生命周期和应用的生命周期一样长,这就导致了该静态实例会一直持有Activity的引用,从而导致该内存资源不能被正常回收。
3)资源未关闭造成的内存泄漏。
JVM 中除了程序计数器外,其他各个内存都可能会抛出OOM。常见的有一下三种:
在 Java 中,程序员是不需要显示的释放一个对象的内存的,而是由虚拟机自行执行释放。在 JVM 中,有一个垃圾回收机制,它的优先级特别低,在正常情况下是不会被执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描并清除那些没有任何引用的对象。
本来 GC 的对象是JVM 的内存,但是程序计数器和栈,他们是线程出现的时候就需要这块内存,线程结束的时候,这块内存就可以回收了,所以它的分配回收时机非常明确,就不需要GC 花费大量精力,常量池和方法这两块内存占比比较小,且里面的数据很少失去作用,回收这块内存的性价比不高,因此也不是 GC 重点考虑的点,JVM 的运行时内存就只剩下堆了,所以 GC 重点考虑的就是如何管理堆上的内存,堆上存放的又基本是对象,所以GC 回收不再使用内存的问题,就转换成了,回收死去对象的问题!
有了垃圾回收机制可以让Java程序员在编写程序时不再考虑内存管理的问题,有效的防止了内存泄漏,可以有效的使用可使用的内存。
Java 中引用的本质作用是指向某个对象,但是在GC 中应用还能影响对象的生死,所以Java的引用类型,根据 GC 时引用指向的对象是否被回收划分了几个不同的类型:
没有引用指向的对象就可以被回收,怎么判断一个对象没有引用指向它?
常见的 GC Roots:栈上引用类型的局部变量指向的对象、常量池中引用指向的对象、静态属性中引用指向的对象
标记无用的对象,然后进行清除回收。它是一种常见的基本的垃圾回收算法,分为两个阶段:
优点:实现简单,不需要对象移动;
缺点:标记、清除过程的效率低,产生了大量不连续的内存碎片
为了解决标记-清除算法效率不高的问题,产生了复制算法。他把内存的空间划分为两个相等的区域,每次只使用其中一个区域。当发生垃圾回收时,遍历当前区域,把存活的对象复制到另外一个区域中,然后把使用的内存区域一次性清理掉。
优点:复制对象时按顺序分配内存即可,实现效率高、运行简单,不会产生内存碎片;
缺点:可用空间减小为原来的一半,对象存活率高时会发生频繁进行复制
为了解决标记-清除法中内存碎片的问题,产生了标记-整理法。在标记可回收对象之后,将所有存活对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,将已用和未用的内存都各自放在一边 —— 就是原地整理的意思。
先介绍一下一个普通对象可能的一生,分为新生代、老年代。且经过发现,大部分的对象,寿命都是非常短的,但对象的岁数一旦比较大,寿命就比较长了。即就是大量的对象可能都会在新生代被回收,只有少量的对象可能会存活到老年代。对象每经过一次 GC,它的年龄就会 +1。
所以我们根据对象的存活周期将内存划分为伊甸区、生存区、老年区。新生代的对象刚开始都在伊甸区,经过一次 GC 后,98%的对象已经死了,存活的对象都会进入到生存区,如果在生存区经过了 15 次 GC,对象还存活着,就会进入到老年区。大量的对象在伊甸区就会被回收掉,因此生存区中的对象就比较少,生存区中的对象在经过 GC 时,采用的算法是复制算法,而一旦进入到老年代,它的寿命比较长,存活率比较高,因此在GC 的时候,可以采用标记-整理法。
GC 不是每次都全区域 GC,根据 GC 的区域可以分为:
在分代收集算法中,GC 一般可以分为Minor GC和 Major GC:
其中,新生代垃圾回收器有Serial、PraNew、Parallel Scavenge,老年代的垃圾回收器有 Serial Old、Parallel Old、CMS,还有两个都可以用的 G1。
对象一般都分配在伊甸区。当伊甸区分配时没有足够空间时,虚拟机会调用一次 Minor GC,如果有空间了就分配,如果还是没有,就启用分配担保机制在老年代分配内存。
类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象,主要有三种类加载器:
public class PrintClassLoader{
public static void main(String[] args){
// 启动类加载器
// 打印 null,表示是由BootstrapClassLoader 加载
// 存在:jre.1.8.0_141\lib\rt.jar下的类库
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);
// 扩展类加载器
// 打印,ExtensionClassLoader
// 存在:jre.1.8.0_141\lib\ext\*.jar
ClassLoader classLoader2 = DNSNameService.class.getClassLoader();
System.out.println(classLoader2);
// 用户自定义类加载器
// 打印 ApplicationClassLoader
ClassLoader classLoader3 = PrintClassLoader.class.getClassLoader();
System.out.println(classLoader3);
}
用户自定义的类加载器是扩展类加载器,扩展类加载器的双亲是启动类加载器。
双亲委派原则就是如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类去完成,每一层的类加载过程都是如此,这一所有的类加载请求都会被传送到顶层的启动类加载器中,只有当父类加载器无法完成加载请求时,子类才会尝试去加载类。