java虚拟机学习常见问题如下:
一、Java引用的四种状态:
强引用:
用的最广。我们平时写代码时,new一个Object存放在堆内存,然后用一个引用指向它,这就是强引用。
如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
软引用:
如果一个对象只具有软引用,则内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。(备注:如果内存不足,随时有可能被回收。)
只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
弱引用:
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
每次执行GC的时候,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
虚引用:
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。
注:关于各种引用的详解,可以参考这篇博客:
http://zhangjunhd.blog.51cto.com/113473/53092
二、Java中的内存划分:
Java程序在运行时,需要在内存中的分配空间。为了提高运算效率,就对数据进行了不同空间的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。
上面这张图就是jvm运行时的状态。具体划分为如下5个内存空间:(非常重要)
- 程序计数器:保证线程切换后能恢复到原来的执行位置
- 虚拟机栈:(栈内存)为虚拟机执行java方法服务:方法被调用时创建栈帧-->局部变量表->局部变量、对象引用
- 本地方法栈:为虚拟机执使用到的Native方法服务
- 堆内存:存放所有new出来的东西
- 方法区:存储被虚拟机加载的类信息、常量、静态常量、静态方法等。
- 运行时常量池(方法区的一部分)
GC对它们的回收:
内存区域中的程序计数器、虚拟机栈、本地方法栈这3个区域随着线程而生,线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作,每个栈帧中分配多少内存基本是在类结构确定下来时就已知的。在这几个区域不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。
GC回收的主要对象:而Java堆和方法区则不同,一个接口中的多个实现类需要的内存可能不同,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC关注的也是这部分内存,后面的文章中如果涉及到“内存”分配与回收也仅指着一部分内存。
1、程序计数器:(线程私有)
每个线程拥有一个程序计数器,在线程创建时创建,
指向下一条指令的地址
执行本地方法时,其值为undefined
说的通俗一点,我们知道,Java是支持多线程的,程序先去执行A线程,执行到一半,然后就去执行B线程,然后又跑回来接着执行A线程,那程序是怎么记住A线程已经执行到哪里了呢?这就需要程序计数器了。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器,这块儿属于“线程私有”的内存。
2、Java虚拟机栈:(线程私有)
每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。局部变量表存放的是:编译期可知的基本数据类型、对象引用类型。
每个方法被调用直到执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
在Java虚拟机规范中,对这个区域规定了两种异常情况:
(1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
(2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM
3、本地方法栈:
(1)本地方法栈与java虚拟机栈作用非常类似,其区别是:java虚拟机栈是为虚拟机执行java方法服务的,而本地方法栈则为虚拟机执使用到的Native方法服务。
(2)Java虚拟机没有对本地方法栈的使用和数据结构做强制规定,Sun HotSpot虚拟机就把java虚拟机栈和本地方法栈合二为一。
(3)本地方法栈也会抛出StackOverFlowError和OutOfMemoryError。
4、Java堆:即堆内存(线程共享)
(1)堆是java虚拟机所管理的内存区域中最大的一块,java堆是被所有线程共享的内存区域,在java虚拟机启动时创建,堆内存的唯一目的就是存放对象实例几乎所有的对象实例都在堆内存分配。
(2)堆是GC管理的主要区域,从垃圾回收的角度看,由于现在的垃圾收集器都是采用的分代收集算法,因此java堆还可以初步细分为新生代和老年代。
(3)Java虚拟机规定,堆可以处于物理上不连续的内存空间中,只要逻辑上连续的即可。在实现上既可以是固定的,也可以是可动态扩展的。如果在堆内存没有完成实例分配,并且堆大小也无法扩展,就会抛出OutOfMemoryError异常。
5、方法区:(线程共享)
(1)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
(2)Sun HotSpot虚拟机把方法区叫做永久代(Permanent Generation),方法区中最终要的部分是运行时常量池。
6、运行时常量池:
(1)运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。
注:关于本段的详细内容,可以参考本人的另外一篇博客:Java虚拟机详解02----JVM内存结构
三、Java对象在内存中的状态:
可达的/可触及的:
Java对象被创建后,如果被一个或多个变量引用,那就是可达的。即从根节点可以触及到这个对象。
其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的。
可恢复的:
Java对象不再被任何变量引用就进入了可恢复状态。
在回收该对象之前,该对象的finalize()方法进行资源清理。如果在finalize()方法中重新让变量引用该对象,则该对象再次变为可达状态,否则该对象进入不可达状态
不可达的:
Java对象不被任何变量引用,且系统在调用对象的finalize()方法后依然没有使该对象变成可达状态(该对象依然没有被变量引用),那么该对象将变成不可达状态。
当Java对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
、Java堆内存划分:
根据对象的存活率(年龄),Java对内存划分为3种:新生代、老年代、永久代:
1、新生代:
比如我们在方法中去new一个对象,那这方法调用完毕后,对象就会被回收,这就是一个典型的新生代对象。
现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。同时,长期存活的对象将进入老年代(虚拟机给每个对象定义一个年龄计数器)。
来看下面这张图:
Minor GC和Full GC:
GC分为两种:Minor GC和Full GC
Minor GC:
Minor GC是发生在新生代中的垃圾收集动作,采用的是复制算法。
对象在Eden和From区出生后,在经过一次Minor GC后,如果对象还存活,并且能够被to区所容纳,那么在使用复制算法时这些存活对象就会被复制到to区域,然后清理掉Eden区和from区,并将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,可以通过参数 --XX:MaxTenuringThreshold设置),这些对象就会成为老年代。
但这也是不一定的,对于一些较大的对象(即需要分配一块较大的连续内存空间)则是直接进入老年代
Full GC:
Full GC是发生在老年代的垃圾收集动作,采用的是标记-清除/整理算法。
老年代里的对象几乎都是在Survivor区熬过来的,不会那么容易死掉。因此Full GC发生的次数不会有Minor GC那么频繁,并且做一次Full GC要比做一次Minor GC的时间要长。
另外,如果采用的是标记-清除算法的话会产生许多碎片,此后如果需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次GC。
2、老年代:
在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。而且大对象直接进入老年代。
3、永久代:
即方法区。
类加载机制:
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的过程:
包括加载、链接(含验证、准备、解析)、初始化
如下图所示:
1、加载:
类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,作为方法区这个类的数据访问的入口。
也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。具体包括以下三个部分:
(1)通过类的全名产生对应类的二进制数据流。(根据early load原理,如果没找到对应的类文件,只有在类实际使用时才会抛出错误)
(2)分析并将这些二进制数据流转换为方法区方法区特定的数据结构
(3)创建对应类的java.lang.Class对象,作为方法区的入口(有了对应的Class对象,并不意味着这个类已经完成了加载链接)
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
(1)从本地文件系统加载class文件,这是绝大部分程序的加载方式
(2)从jar包中加载class文件,这种方式也很常见,例如jdbc编程时用到的数据库驱动类就是放在jar包中,jvm可以从jar文件中直接加载该class文件
(3)通过网络加载class文件
(4)把一个Java源文件动态编译、并执行加载
2、链接:
链接指的是将Java类的二进制文件合并到jvm的运行状态之中的过程。在链接之前,这个类必须被成功加载。
类的链接包括验证、准备、解析这三步。具体描述如下:
2.1 验证:
验证是用来确保Java类的二进制表示在结构上是否完全正确(如文件格式、语法语义等)。如果验证过程出错的话,会抛出java.lang.VertifyError错误。
主要验证以下内容:
- 文件格式验证
- 元数据验证:语义验证
- 字节码验证
2.2 准备:
准备过程则是创建Java类中的静态域(static修饰的内容),并将这些域的值设置为默认值,同时在方法区中分配内存空间。准备过程并不会执行代码。
注意这里是做默认初始化,不是做显式初始化。例如:
public static int value = 12;
上面的代码中,在准备阶段,会给value的值设置为0(默认初始化)。在后面的初始化阶段才会给value的值设置为12(显式初始化)。
2.3 解析:
解析的过程就是确保这些被引用的类能被正确的找到(将符号引用替换为直接引用)。解析的过程可能会导致其它的Java类被加载。
3、初始化:
初始化阶段是类加载过程的最后一步。到了初始化阶段,才真正执行类中定义的Java程序代码(或者说是字节码)。
在以下几种情况中,会执行初始化过程:
(1)创建类的实例
(2)访问类或接口的静态变量(特例:如果是用static final修饰的常量,那就不会对类进行显式初始化。static final 修改的变量则会做显式初始化)
(3)调用类的静态方法
(4)反射(Class.forName(packagename.className))
(5)初始化类的子类。注:子类初始化问题:满足主动调用,即父类访问子类中的静态变量、方法,子类才会初始化;否则仅父类初始化。
(6)java虚拟机启动时被标明为启动类的类
本文参考:https://www.cnblogs.com/smyhvae/p/4810168.html