java虚拟机 JVM

1、JVM

java虚拟机栈:线程私有  
生命周期同线程。**每个方法在执行同时,创建栈帧**   用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 栈中的局部变量表主要存放一些基本类型的变量(int, short, long, byte, float,double, boolean, char)和对象句柄。 栈中有局部变量表,包含参数和局部变量。

java堆:线程共有
  **在虚拟机启动时创建**   (可能划分出多个线程私有的分配缓冲区,Thread Local Allow) ,Java虚拟机管理内存中最大的一块,此区域唯一目的就是存放对象实例,几乎所有对象实例在此区分配,线程共享内存。可细分为新生代和老年代,方便GC。主流虚拟机都是按可扩展实现(通过-Xmx 和 -Xms 控制)


栈溢出
递归,容易引起栈溢出stackoverflow;因为方法循环调用,方法调用会不断创建栈帧。
造成栈溢出的几种情况:
1)递归过深
2)数组、List、map数据过大
3 ) 创建过多线程
对于Java虚拟机栈和本地方法栈,Java虚拟机规范规定了**两种异常**状况:
① 线程请求深度>虚拟机所允许的深度,将抛出StackOverFlowError(SOF)异常;
② 如果虚拟机可动态扩展,且扩展时无法申请到足够的内存,就会抛出OutOfMemoryError(OOM)异常。
堆溢出
如果在堆中没有内存完成实例分配,且堆无法扩展时,将抛出OOM异常。
在方法区也会抛出 OOM 异常

可使用以下代码造成堆栈溢出:

package overflow;

import java.util.ArrayList;

/**
 * Created by hupo.wh on 2016/7/7.
 */


public class MyTest {

    public void testHeap(){
        for(;;){
            ArrayList list = new ArrayList (2000);
        }
    }
    int num=1;
    public void testStack(){
        num++;
        this.testStack();
    }

    public static void main(String[] args){

        MyTest t  = new MyTest();
        t.testHeap();
        //t.testStack();
    }
}

如下代码会造成OOM堆溢出:

package OOM;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by hupo.wh on 2016/7/15.
 */
public class App1 {

    static class OOMClass {
        long[] num = new long[10240];
    }

    public static void main(String[] args) {
        List<OOMClass> list = new ArrayList<>();
        while (true) {
            list.add(new OOMClass());
        }
    }


}
另外,Java虚拟机的堆大小如何设置:命令行
 java –Xms128m //JVM占用最小内存
–Xmx512m //JVM占用最大内存
–XX:PermSize=64m //最小堆大小
–XX:MaxPermSize=128m //最大堆大小
2、类加载机制

Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。

      虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

Java 中的类加载器大致可以分成两类:一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。**系统提供**的类加载器主要有下面三个:
(1)引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
BootStrapClassLoader
负责jdk_home/jre/lib目录下的核心 api或 -Xbootclasspath选项指定的jar包加载进来。
(2)扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
ExtClassLoader
负责jdk_home/jre/lib/ext目录下的jar包或 -Djava.ext.dirs指定目录下的jar包加载进来。
(3)系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
AppClassLoader
负责java -classpath/-Djava.class.path所指的目录下的类与jar包加载进来,System.getClassLoader获取到的就是这个类加载器。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
除了引导类加载器之外,所有的类加载器都有一个父类加载器。getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。


1.2.6 类加载过程
从类被加载到虚拟机内存中开始,到卸载出内存为止,类的生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。

其中加载(除了自定义加载)+链接的过程是完全由jvm负责的,什么时候要对类进行初始化工作(加载+链接在此之前已经完成了),jvm有严格的规定(四种情况):
1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,假如类还没进行初始化,则马上对其进行初始化工作。其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。
2.使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。
3.初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。
4.当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。
以上4种预处理称为对一个类进行主动的引用,其余的其他情况,称为被动引用,都不会触发类的初始化。
加载:

在加载阶段,虚拟机主要完成三件事:

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

验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:

1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。

准备:

准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。对非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:

private static int size = 12;
那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。
解析:
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
初始化:
在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成

双亲委派模型:
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。
首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。**Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。**只有两者都相同的情况,才认为两个类是相同的。
3、垃圾回收 GC

思想:

通过一系列称为 GC Roots 的对象作为起始点,从这些点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链连接(用图论的话来说,就是从GC Roots到这个对象不可达),证明此对象不可用。

Java语言中,可作为GC Roots的对象包括:

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象

(2)方法区中类静态属性引用的对象

(3)方法区中常量引用的对象
(4)本地方法栈中JNI ( 即一般说的Native方法)引用的对象

  • root搜索算法

这种算法目前定义了几个root,也就是这几个对象是jvm虚拟机不会被回收的对象,所以这些对象引用的对象都是在使用中的对象,这些对象未使用的对象就是即将要被回收的对象。简单就是说:如果对象能够达到root,就不会被回收,如果对象不能够达到root,就会被回收。

以下对象会被认为是root对象:

  • 被启动类(bootstrap加载器)加载的类和创建的对象
  • jvm运行时方法区类静态变量(static)引用的对象
  • jvm运行时方法去常量池引用的对象
  • jvm当前运行线程中的虚拟机栈变量表引用的对象
  • 本地方法栈中(jni)引用的对象
什么时候出发垃圾回收?

1、    对象没有引用

2、    作用域发生未捕获异常

3、    程序在作用域正常执行完毕

4、    程序执行了System.exit()

5、    程序发生意外终止(被杀进程等)
1.3.3 再谈引用

在JDK 1.2之后 ,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference )、软引用(Soft Reference )、弱引用(Weak Reference )、虚引用(Phantom Reference) 4种 , 引用强度依次逐渐减弱。

强引用

指在程序代码之中普遍存在的,类似“Object obj=new Object ( ) ”这类的引用 ,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用
用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行**二次回收**。如果这次回收还没有足够的内存,才会拋出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
弱引用
也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
虚引用
也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
1.3.4 对象回收过程
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的 ,这时候它们暂时处于“缓刑” 阶段 ,要真正宣告一个对象死亡 ,至少要经历**两次标记过程**
如果这个对象被判定为有必要执行finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
1.3.5 对于方法区(Hotspot虚拟机的永久代)的回收
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:
(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
(2)加载该类的ClassLoader已经被回收
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

1.3.6 垃圾收集算法

1.3.6.1 标记-清除算法

望名生意,算法分为“标记”和“清除”两个阶段:

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程如前

它的主要不足有两个:

(1)效率问题,标记和清除两个过程的效率都不高;

(2)空间问题,标记清除之后会产生**大量不连续的内存碎片**,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。


这里写图片描述

1.3.6.2 复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

适用于对象存活率低的场景(新生代)

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针 ,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

将内存分为**一块较大的Eden空间和两块较小的Survivor空间** ,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最 后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ) ,只有10% 的内存会被 “浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保( Handle Promotion ) 。

这里写图片描述

1.3.6.3 标记-整理算法

适用于对象存活率高的场景(老年代)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是 ,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记过程类似“标记-清除”算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程


这里写图片描述

总的分类如下图:



这里写图片描述

4、JVM启动过程
JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.
1.创建JVM装载环境和配置
2.装载JVM.dll
3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
4.调用JNIEnv实例装载并处理class类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值