JVM笔记

JVM

Java是跨平台的编程语言,一次编译,到处运行。Java代码编译为字节码也就是class文件,然后再不同的操作系统上依靠不同的Java虚拟机转换成不同平台的机器码,最终的得到执行。

       第一个程序输出打印hello world需要经历如下步奏:

       首先将,java代码编译成字节码,然后通过java helloworld执行,此时java会根据系统找到jvm.cfg,再通过jvm.cfg中相关配置找到jvm.dll,jvm.dll就是jvm的主要实现,找到jvm.dll紧接着就会初始化jvm并且获取jni接口,jni接口是java本地接口,他能够找到class文件并且剪裁放jvm,之后找到main方法,最后执行。这就是java文件的编译执行流程。

       那么jvm的结构谁怎样的呢?如下:

       Class文件通过类加载子系统加载之后,将类中的属性方法数据分别分配到jvm的不同部分。

       Jvm的内存空间包含:

方法区:各个线程的共享区域,存放类信息、常量、静态常量。

Java堆:也是线程的共享区域,这里存放有类的实例,并且这里是jvm中站内存最大的区域。

Java栈:是每个线程私有的区域,线程执行完毕,属于该线程的java栈会被销毁。每执行一个方法,会向java栈中压入一个元素,这个元素叫做栈帧,栈帧中包含了方法的局部变量,用于存放中间状态的操作栈等等,当递归函数的层级太多会引起栈溢出。

本地方法栈:类似于java栈,只不过他是用来表示执行本地方法的,这里存放了调用本地方法接口的方法。主要用于实现与操作系统、硬件的交互。

PC寄存器:这里类似于pc(程序计数器),于控制代码执行的顺序。

垃圾收集器将在后面进行详细介绍。

接下来讲jvm的内存模型:

这里我们称java堆为主内存,那么在主内存区域,各线程共享这块区域。但是每个线程执行的时候都会有自己的工作内存,当线程自己对变量进行改变赋值等操作的时候不会马上更新到主内存(堆)中里面。

如果需要线程间通信,首先要先把线程1中的变量值写在内存中,然后线程2在读取,但是实际的代码运行的时候却可能在还没写入就读了主内存中的数据,这样就会出问题,也就是不能保证修改的可见性。

这里介绍一下java中volatile,这是修饰定义变量的关键词,当volatile修饰变量之后,变量每次更新都会直接写在内存上,而不只写在工作区,这样,线程间数据的可见性就能够保证了。当然,volatile关键字修饰变量a,a=a+1,这样的操作不是原子操作,所以无法保证他的原子性。但是volatile能够保证一定程度上的有序性

//x、y为非volatile变量

//flag为volatile变量

 

x = 2;        //语句1

y = 0;        //语句2

flag = true;  //语句3

x = 4;         //语句4

y = -1;       //语句5

如上,语句1、2与4、5顺序不能保证,但是语句3对1、2不可见,对4、5可见,也就是4、5一定在3之后,保证一定的有序性。

       计算机执行代码的时候会对代码进行重排序,如上有序性所说的一样,代码执行顺序只要不影响结果就可以改变顺序。但是改变顺序需要遵守java的happen before规则:

1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。(单线程程序)

    2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。(获取锁必须要等别人释放锁

    3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。(volatile变量的写之后的读不会被排序到写之前)

    4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。(start()方法一定在线程内容之前)

    5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。(线程的终止检测一定在线程的动作之后)

    6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。(interrupt方法必须在中断之前)

    7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。(对象构造完成定在对象的虚构函数之前)

8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。(传递)

JVM配置参数分为三类参数:

1、跟踪参数:用于监控jvm内存使用情况

2、堆分配参数:用于设置堆的大小

3、栈分配参数:用于设置栈区的大小

这三类参数分别用于跟踪监控JVM状态,分配堆内存以及分配栈内存。

还有永久区分配参数与栈大小分配参数。

接下来讲解垃圾回收算法:

1,应用计数法,当没有引用指向的对象就会被垃圾回收器清理掉。

2,标记清除法:它是很多垃圾回收算法的基础,简单来说有两个步骤:标记、清除。

标记:遍历所有的GC Roots,并将从GC Roots可达的对象设置为存活对象;

清除:遍历堆中的所有对象,将没有被标记可达的对象清除;

3,标记压缩法:就是标记清楚之后对内存进行压缩空间,使得内存不那么碎片化。

4,复制算法:将内存分成两个区域,所有对象放在一个区域,对内存进行标记,存活的标记,之后将标记的复制到另一个区域,将原来的区域进行全部清理。

 

Jvm的垃圾回收器,主要是对堆内存区域进行垃圾回收。

堆内存结构:

       Java堆内存结构包括两个区域:新生代与老年代。其中新生代包括一个伊甸区与两个幸存区,两个幸存区大小完全对称,如上s0与s1,也可以称为from与to两个区域

垃圾回收器包括:

1,串行收集器:串行收集器就是使用单线程进行垃圾回收。对新生代的回收使用复制算法,对老年代使用标记压缩算法。

2,并行回收器:并行回收器分为两种:

1、 ParNew回收器

这个回收器只针对新生代进行并发回收,老年代依然使用串行回收。回收算法依然和串行回收一样,新生代使用复制算法,老年代使用标记压缩算法。在多核条件下,它的性能显然优于串行回收器,

2、 Parallel回收器

依然是并行回收器,但这种回收器有两种配置,一种类似于ParNEW:新生代使用并行回收、老年代使用串行回收。它与ParNew的不同在于它在设计目标上更重视吞吐量,可以认为在相同的条件下它比ParNew更优。Parallel回收器另外一种配置则不同于ParNew,对于新生代和老年代均适应并行回收。在进行回收时,应用程序暂停,GC使用多线程并发回收,回收完成后应用程序线程继续运行。

3, CMS回收器:ConcurrentMark Sweep,并发标记清除。注意这里注意两个词:并发、标记清除。

如下流程:

从上图可以看到标记过程分三步:初始标记、并发标记、重新标记,并发标记是最主要的标记过程,而这个过程是并发执行的,可以与应用程序线程同时进行,初始标记和重新标记虽然不能和应用程序并发执行,但这两个过程标记速度快,时间短,所以对应用程序不会产生太大的影响。最后并发清除的过程,也是和应用程序同时进行的,避免了应用程序的停顿。但是这个回收由于在运行的过程中进行,就可能造成回收不测底,于是就会很频繁的进行垃圾回收。

4, GI回收器。

GI回收器,将堆划分成独立的区块,然后选择垃圾最多的区块进行清理。


 

G1相对CMS回收器来说优点在于:

1、因为划分了很多区块,回收时减小了内存碎片的产生;

2、G1适用于新生代和老年代,而CMS只适用于老年代。

类加载器原理:java代码,会经过编译器编译成字节码文件(class文件),再把字节码文件装载到JVM中,映射到各个内存区域中,我们的程序就可以在内存中运行了。

如下是java类装载流程:

加载:读取class文件的2进制流,并解析,将元数据等放进方法区,在java堆中生成对应类的对象。

连接过程:分为3步:

    验证:检测class文件的合法性,是否可以装载

    准备:这个过程会给类分配内存,给类的一些字段设置初始值等,final常量设定确定值。

    解析:将符号引用替换为直接引用,就是引用地址放到直接引用中。

初始化过程:这里才开始真正执行代码,主要是类的构造,静态代码块的执行,代码值得设定等。

    主动引用:当如下情况的时候,会立即初始化该类:

new对象时

读取或设置类的静态字段(除了被final,已在编译期把结果放入常量池的 静态字段)或调用类的静态方法时;

用java.lang.reflect包的方法对类进行反射调用没初始化过的类时

初始化一个类时发现其父类没初始化,则要先初始化其父类

含main方法的那个类,jvm启动时,需要指定一个执行主类,jvm先初始化这个类

子类继承父类时的初始化顺序

1.首先初始化父类的static变量和块,按出现顺序

2.初始化子类的static变量和块,按出现顺序

3.初始化父类的普通变量,调用父类的构造函数

4.初始化子类的普通变量,调用子类的构造函数

类加载器:是一个抽象类,ClassLoader的实例负责吧java字节码读取到jvm当中,可以选择加载的class流方式文件或者网络,ClassLoder负责整个加载流程。

    ClassLoder类的就作用就是根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例.

    大部分java程序会使用以下3种类加载器:

启动类加载器(BootstrapClassLoader):

这个类加载器负责将<JAVA_HOME>\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的.是虚拟机自身的一部分.

扩展类加载器(ExtendsionClassLoader):

这个类加载器负责加载<JAVA_HOME>\lib\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器.

应用程序类加载器(Application ClassLoader):

这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.

**getParent(),返回时null的话,就默认使用启动类加载器作为父加载器

双亲委派模型:

自下向上检查类是否被加载,一般情况下,首先从AppClassLoader中调用findLoadedClass方法查看是否已经加载,如果没有加载,则会交给父类,Extension ClassLoader去查看是否加载,还没加载,则再调用其父类,BootstrapClassLoader查看是否已经加载,如果仍然没有,自顶向下尝试加载类,那么从 Bootstrap ClassLoader到 App ClassLoader依次尝试加载。

    **同一个类被不同类加载器加载得到的结果是不一样,这个不同反应在对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果。

    双亲委托模式当实现类在其他加载器中,而接口在顶层类加载器中,这样无法找到实际的加载类,那么就需要Thread.setContextClassLoader(),传入底层ClassLoader实例。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值