JVM基础

Posted 小田mas

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM基础相关的知识,希望对你有一定的参考价值。

1、JVM位置

JVM运行在操作系统之上,虽然说是个环境,其实就相当于一个软件,jre里面包含了jvm

2、JVM体系结构



栈里面是不会有垃圾的,main方法在最底层,来一个方法就会弹出,所以不可能存在垃圾回收。


方法区就是一个特殊的堆,所谓的JVM调优99%就是在调方法区和堆这个区,大部分时间在调堆。
引用在栈里面,实例在堆里面。

3、类加载器

加载class文件

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器
  3. 扩展类加载器
  4. 应用程序(系统类)加载器
    从下往上一层一层地递进

    null说明是不存在,或者说不是用java写的。 rt.jar(runtime.jar)根加载器里面的一些东西,在jvm里面。
    PlatformClass Loader
    ex 扩展类加载器在 jre/lib/ext 下,如果jar在目录下,也会到相应的地方(向上)去找。
    应用加载器,相当于继承了ClassLoader。

打开rt.jar可以解压,相应的jar包是可以修改的。

突然想知道 什么是字节码文件?

字节码文件是经过编译器预处理过的一种文件,是JAVA的执行文件存在形式,

Java源程序(.java)要先编译成与平台无关的字节码文件(.class),然后字节码文件再解释成机器码运行。解释是通过Java虚拟机来执行的。

它本身是二进制文件,但是不可以被系统直接执行,而是需要虚拟机解释执行,由于被预处理过,所以比一般的解释代码要快,但是仍然会比系统直接执行的慢。


根加载器 bootstrapClassLoader加载器。

这里的直接去根加载器找String类(Student对应的student类),看到了ToString()方法,哎,就直接显示找不到main方法了。
没人写java.lang包,都是com.什么什么的写。

native 本地的方法,是c、c++写的,因为线程级别的东西java处理不了。
new Thread().start(); 其实最后调用了一个本地方法start0,还是建议稳一点。

双亲委派机制

下面的一段代码可以解释一下类加载机制。

    public Class<?> loadClass(String name) throws ClassNotFoundException 
        return loadClass(name, false);
    
    //              -----??-----
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    
            // 首先,检查是否已经被类加载器加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) 
                try 
                    // 存在父加载器,递归的交由父加载器
                    if (parent != null) 
                        c = parent.loadClass(name, false);
                     else 
                        // 直到最上面的Bootstrap类加载器
                        c = findBootstrapClassOrNull(name);
                    
                 catch (ClassNotFoundException e) 
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                
 
                if (c == null) 
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                
            
            return c;
    
  1. 类加载器收到类加载的请求;
  2. 将这个请求委托给父类加载器去完成,一直向上委托,知道启动类加载器;
  3. 启动加载器检查是否能够加载这个类,能就加载,结束。使用当前的加载器,否则,抛出异常,通知子加载器进行加载
  4. 重复步骤3

转载自面试官:java双亲委派机制及作用

那什么是双亲委派机制?

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

下面的源码非常适合理解

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    
        synchronized (getClassLoadingLock(name)) 
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) 
                long t0 = System.nanoTime();
                try 
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) 
                        c = parent.loadClass(name, false);
                     else 
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    
                 catch (ClassNotFoundException e) 
                if (c == null) 
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                
            
            if (resolve) 
                resolveClass(c);
            
            return c;
        
    

类加载器的类别

BootstrapClassLoader(启动类加载器)

c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

ExtClassLoader (标准扩展类加载器)

java编写,加载扩展库,如classpath中的jre ,javax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

AppClassLoader(系统类加载器)

java编写,加载程序所在的目录,如user.dir所在的位置的class

CustomClassLoader(用户自定义类加载器)

java编写,用户自定义的类加载器,可加载指定路径的class文件

委派机制的流程图

双亲委派机制的作用

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

JNI

java native interface 本地方法接口

native:凡是带了native关键字的,说明java的作用范围到达不了,会去调用底层c的库。
会进入本地方法栈;
调用本地方法本地接口;JNI。
JNI作用:扩展java的使用,融合不同的编程语言为Java所用!最初:c、c++;
Java诞生的时候必须要有调用c、c++的程序;
在内存区域中专门开辟了一块标记区域:native mothod Stack,登记native方法。
在最终执行的时候,加载本地方法库中的方法通过JNI。
但是现在native越来越少用了,除非写java想操作硬件。比如用java驱动打印机,管理系统,在企业级应用中较为少见。

调用其他接口:socket,webservice,http



字符串常量池JDK 1.7之前在方法区,1.7之后在堆上。
字符串一般在常量池里面。
类模板在方法去之中。
静态变量(static)、常量(final)、类信息(构造方法、接口定义)(class)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,与方法区无关。

一种数据结构,程序=数据结构+算法。

栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就释放。不存在垃圾回收,一旦线程结束,栈也就结束。

栈里面可以放什么:八大基本类型,引用类型,实例方法。

栈的运行原理:

额,看的网上的一个理解:

方法的代码,当然是在方法区。

栈上的那个包含有方法局部变量的,是在栈帧里面的,每一个方法调用,都会有一个对应的栈帧的创建。

但是老师讲的时候是将实例方法存放在了栈中,我看网上大部分说,方法的代码存放在方法去,方法的变量存放在栈,方法一调用就会有对应栈帧的创建。

我觉得两个的区别就在于时间上,经过探索,看看是不是执行到,没执行到之前确实是在方法区,执行到就会进栈。

等我一点一点探索,一点一点改!


java的本质是值传递。

栈中是怎么存数据的?

一个对象在内存中是怎么实例化的?
参考Java对象在内存中实例化的过程

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件之后,一般会把什么东西放到堆中?类、方法、常量、变量,保存引用类型的真实对象。

堆内存还细分为三个区域:

  • 新生区(伊甸园区)
  • 养老区
  • 永久区

特别长的字符串内存中是存不下的,

GC 垃圾回收算法

引用计数法

用的少

复制算法

谁空谁是to


每次清理完成之后伊甸园区是空的 to区是空的。

好处:没有内存的碎片
坏处:浪费了内存空间,永远多了一块空的内存to,假设对象百分之百成活,占了整个幸存区,(极端情况下)

复制算法最佳使用场景:
对象存活度较低的情况,新生区

标记压缩清除算法
先标记清除几次,再压缩。

缺点:两次扫描严重浪费时间,会产生内存碎片
优点:不需要额外的空间(复制算法需要to)

标记压缩:
防止产生内存碎片,就是再次扫描,向一端移动存活的对象。多了一个移动成本。

总结
内存利用率:复制算法(没有浪费的空间)>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法

没有最好的算法,只有最合适的。
GC:分代收集算法

年轻代:

  • 存活率低,复制算法

老年代:

  • 区域大
  • 存活率高
  • 标记清除+标记压缩混合实现
    -jvm就在调这个(几次+几次)

JMM Java memory model

1.什么是JMM
java内存模型

2.是干嘛的?
作用:缓存一致性协议,用来定义数据读写的规则。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量都在主内存(Main Memory),每个线程都有一个私有的本地内存(Local memory)。
Java有一个主内存。
线程工作内存,每个线程都有自己的工作区域,是从主内存拷贝的。

JMM和底层实现原理。
volatile:解决共享对象可见性这个问题的关键字。保证刷新到内存。

3.该如何学习?

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存(volatile)
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

以上是关于JVM基础的主要内容,如果未能解决你的问题,请参考以下文章

JVM--java内存模型

JVM--java内存模型

JVM学习笔记 05 - JMM简述

JMM内存模型JVM内存模型

JMM内存模型JVM内存模型

浅谈主存和Cache间的地址映射