java 复习之---Java虚拟机相关知识点详解

1、Java的内存区域
1.1: 运行时数据区
(1)、程序计数器:程序计数器是一块比较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每条线程之间都有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。如果这个计数器正在执行的是一个Java 方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个本地方法,那么这个计数器值则为空。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

(2)、Java 虚拟机栈:与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同、虚拟机栈描述的是Java方法执行时的内存模型。每个方法执行时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

jdk1.5 以后每个线程堆栈默认大小为1M ,以前每个线程栈大小为256K。可以通过- Xss 参数来设置每个线程的堆栈大小。线程栈的大小是一把双刃剑,如果设置过小,可能会出现栈溢出,特别是线程内有递归、大的循环时溢出的可能性更大;如果该值设置过大,就会影响创建线程的数量,当遇到多线程的应用时可能出现内存溢出的情况。

局部变量表存放着编译器可知的基本数据类型,对象引用,其中64位的long 和 double 类型数据会占用2个局部变量的空间,其余数据类型只占一个。局部变量表所需要的内存空间在编译期间就能分配完成,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定,在运行期间栈帧不会改变局部变量表的空间大小。

当线程请求的栈深度大于虚拟机允许的深度将抛出StackOverflowError异常,或者创建线程数量较多时,会出现栈内存溢出。如果虚拟机可以动态扩展,扩展时无法申请足够的内存就会抛出OOM 异常。

(3)、本地方法栈:本地方法栈与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈是为本方法服务得出。

(4)、Java堆:对于大多数应用来说,Java堆是Java虚拟机锁管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在Java虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的度一项实例都在这里分配内存。这一点在Java虚拟机里面描述是:所有对象的实例以及数组都要在队上分配。但是随着技术的发展,所有对象在分配在堆上也逐渐变得不是那么绝对了。

Java堆是垃圾收集器管理的主要区域,因此很多时候被称为GC堆,由于现在垃圾收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代。

新生代:可以细分为新生代由Eden Space和两块相同大小的Survivor Space(通常又称为S0 和 S1或者成为from 和 to 区域)构成 ,比例大概为8::1:1。这样划分的目的是因为虚拟机采用复制算法来回收新生代,可以有效的避免垃圾回收之后产生的碎片化问题,提高虚拟机性能,充分利用内存空间,减少内存浪费。新生成的对象在Eden区分配(大对象直接进入老年代),当Eden 区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。

GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

扫描二维码关注公众号,回复: 5357308 查看本文章

可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。

老年代:用于存放经历多次新生代GC 仍然存活的对象,老板代中生命周期较长,存活率较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。老年代锁占用的内存大小为-Xmx的对应值减去-Xmn 对应的值。

(5)、方法区:方法区和堆一样是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译的后的代码等数据。默认最小值为16MB ,最大值为64MB ,可以通过 -XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。

运行时常量池:是方法区一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。

在jdk1.8 以前,方法区是用永久代实现的,在jdk1.8 开始在虚拟机中已经移除了永久代,取而代之是一个叫元空间得出东西,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用的本地内存,因此元空间的大小仅受本地内存限制,但是元空间的大小也可以通过一些参数来指定。

(6)、直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。

在jdk1.4 中新加入了NIO类,引入了基于通道与缓存的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后同过一个存储在java 堆中的DirectoryByteBuffer操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和 Native 堆中来回复制数据。

2、内存泄漏和内存溢出
内存泄漏最终导致内存溢出。内存溢出是指申请的内存空间大于剩余的内存空间,那么便会导致内存溢出。内存泄漏是因为内存中存在一些强引用的无用对象 GC收集器无法回收其内存空间,内存泄漏断堆积最终会导致内存溢出。

内存泄漏的常见场景:
(1)、使用静态的集合类
静态集合类的生命周期和应用程序的生命周期一样长,所以在程序结束前,容器中的对象不能释放,可以会造成内存泄漏。解决办法,尽量不要使用静态的集合类,如果要使用的话,在不要时,将其赋值为null;
(2)、单例模式可能会造成内存泄漏
单例模式只允许应用程序存在一个实例对象,并且在这个实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另外一个对象引用的话,这个被引用的对象就不能被及时回收。
解决办法是在单例对象中持有其他对象使用弱引用,弱引用对象在GC线程工作时,其占用的内存会被回收掉。
(3)、数据库、网络、输入、输出流,这些资源没有显示关闭。
垃圾回收值负责内存回收,如果对象正在使用资源的话,Java虚拟机不能判断这些对象是不是正在进行操作,比如输入输出操作,也就不能回收这些对象占用的内存,所以在资源使用完之后要调用close() 方法关闭。

3、如何判断对象无用或者说对象已死的方法
(1、引用计数法算法:引用计数法的原理是给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1 ,当引用失效时,计数器就减1,任何时刻计数器为0的对象就是不可能被再使用的,便可以判定对象已死。但是主流Java虚拟机不在使用引用计数的方式进行内存管理,因为这个算法无法解决,两个无用对象相互引用的问题。
(2、可达性分析算法:这个算法的基本思想就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象没有任何引用链,就说这个对象是不可达的。

在Java 语言中,可以作为GC Roots 的对象包括下面几种情况:
(1、虚拟机栈用引用的对象
(2、方法区中静态属性引用的对象
(3、方法区中常量引用的对象
(4、本地方法栈中,本地方法引用的对象。

4、Java虚拟机中四种引用类型
强引用:使用最普遍的引用,特点不会被GC掉。将对象的引用置为null ,可以帮助垃圾收集器收集对象。
软引用:用来描述一些还有用但是不是必须的对象,在内存不足时,JVM 会主动回收该对象。
弱引用:弱引用具有更短的生命周期,垃圾收集器一旦发现弱引用,不管内存空间是否足够,都会回收它。
虚引用:一个对象的虚引用完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的真实引用。

5、垃圾收集的种类
根据垃圾收集的区域吧不同将GC分为:Minor GC 和 Full GC 两种。
Mintor GC 发生在新生代的Eden区域,发生次数多,消耗的时间短。
Full GC 覆盖全范围的对象堆的全收集,发生次数少,消耗的时间长。

手动触发Full GC
(1、System.gc()
(2、Runtime.getRuntime().gc()
上面的指令只是告诉JVM 尽快GC 一次,但是不会立即执行。

4、垃圾收集器
(1、Serial(串行)垃圾收集器:针对新生代;采用复制算法;单线程收集;进行垃圾收集时,必须暂停所有工作线程,直到完成;
(2、ParNew收集器:Serial收集器的多线程版本。
(3、 Parallel Scavenge收集器:新生代收集器,采用复制算法,并行多线程收集器,关注吞吐量,是一个吞吐量优先收集器,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
(4、Parallel Old收集器:Parallel Scavenge收集器的老年代版本,针对老年代;采用"标记-整理"算法;多线程收集;
(5、Serial Old收集器:Serial收集器的老年代版本,针对老年代;采用"标记-整理"算法,单线程收集;
(6、CMS收集器(低延迟):多线程并发(垃圾收集器和用户线程同时工作),标记清理,针对老年代, 以获取最短回收停顿时间为目标
(7、G1收集器:能充分利用多CPU、多核环境下的硬件优势;可以并行来缩短"Stop The World"停顿时间;也可以并发让垃圾收集与用户程序同时进行; 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;结合多种垃圾收集算法

5、Java虚拟机类加载机制
5.1、类加载时机:
使用new 关键字实例化对象的时候,读取或者设置一个类的静态字段(被final 修饰、已在编译期把结果放在常量池的静态字段除外),以及调用一个类的静态方法的时候。

使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有加载就进行初始化就进行初始化

当初始化一个类的时候发现其父类没有被初始化,首先需要初始化其父类。

当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先初始化这个类。

当使用JDK1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则首先会触发其初始化。

5.2、类加载过程:
加载:

(1、通过一个类的全限定名来获取定义此类的二进制字节流。
(2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3、在方法区中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据结构的访问入口。

验证:目的在于确保Class 文件字节流中包含信息符合当期虚拟机要求,不会危害虚拟机自身安全。(主要包括文件格式验证,元数据验证,字节码验证,符号引用验证)

准备:为类变量(即staitc 修饰的字段变量)分配内存,并设置该类变量的初始值为0

解析:主要是将常量池中的符号引用替换为直接引用。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或者一个见解定位到目标的句柄(类解析,字段解析,类方法解析,接口方法解析)。

初始化:类加载的最后阶段,到了初始化阶段才这真正开始执行类中定义的Java程序代码。

5.3、类加载器:
( 1、启动(Bootstrap)类加载器: 启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xboot/classpath参数指定的路径下的jar包加载到内存中。

(2、 扩展(Extension)类加载器 负责加载ext 目录(jre\lib)内的类。

(3、 系统(System)类加载器
也称应用程序加载器,它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

5.4 双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。
在这里插入图片描述
双亲委派机制优势
(1)采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
(2)其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

猜你喜欢

转载自blog.csdn.net/weixin_43352448/article/details/88020962