一、JVM的位置

image.png

二、JVM的体系结构

image.png
image.png

三、类加载器

  1. 虚拟机自带类加载器
  2. Bootstrap加载器
  3. 拓展类加载器
  4. 应用加载器/系统加载器

image.png

public class Car {
    public int age;

    public static void main(String[] args) {
        //类是模板,对象是具体的
        Car car1 = new Car();
        car1.age = 1;
        Car car2 = new Car();
        car2.age = 2;
        System.out.println(car1.hashCode() == car2.hashCode());

        //类都是同一个
        Class class1 = car1.getClass();
        Class class2 = car2.getClass();
        System.out.println(class1 == class2);

        //类加载器
        //AppClassLoader
        ClassLoader classLoader = class1.getClassLoader();
        System.out.println(classLoader);
        //ExtClassLoader
        // 加载/jre/lib/ext/*.jar
        ClassLoader parent = classLoader.getParent();
        System.out.println(parent);
        //null,bootstrap加载器是C++写的,java获取不到。
        // 加载/jre/lib/rt.jar
        ClassLoader grandfather = parent.getParent();
        System.out.println(grandfather);
        
/* 输出
    false
    true
    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@816f27d
    null
*/
    }
}

四、双亲委派机制

自己写一个String

//双亲委派机制会把类让父加载器加载,一直往上委托,能加载就使用当前加载器,顶层的加载器不能加载时再往下通知子加载器加载。
//app->ext->bootstrap
package java.lang
public class String {
    public String toString(){
        return "Hello";
    }
    public static void main(String[] args){
        String s = new String();
        s.toString();
    }
}

运行失败,提示找不到main方法。

五、沙箱安全机制

  1. Java安全模型的核心就是Java沙箱(SandBox)。
  2. 沙箱是一个限制程序运行的环境,将Java代码限定在JVM特定的运行范围中,并且严格限制代码对本地系统资源的访问。
安全模型的演进
JDK1.0

image.png
远程代码无法访问本地资源,而本地代码默认可信任

JDK1.1

image.png
增加了安全策略来控制远程代码访问本地资源访问权限

JDK1.2

image.png
增加了代码签名,无论是本地还是远程代码,都会按照安全策略加载到JVM中权限不同的运行空间。

JDK1.6

image.png

  1. 引入了域(Domain)的概念,把所有代码加载到不同的系统域和应用域。
  2. 系统域负责与关键资源进行交互,而应用域通过系统域的部分代理来访问资源。
  3. 虚拟机中不同的受保护域对应不同的权限,存在于域中的类文件拥有当前域的全部权限。
沙箱的基本组件

image.png

  1. 字节码校验器,确保满足JVM规范
  2. 类装载器,双亲委派机制防止恶意代码干涉正常代码。
  3. 存取控制器,控制核心API对OS的存取权限,控制的策略由用户指定。
  4. 安全管理器,核心API与OS之间的主要接口,权限控制的优先级比存取控制器高。
  5. 安全软件包,java.security.*,允许用户添加新的安全特性。

六、Native

image.png

  1. 凡是带了native关键字的,说明java的作用范围达不到了,需要去调用底层C语言的库。
  2. 先进入本地方法栈
  3. 调用本地方法接口(JNI扩展了Java的使用,融合了不同的语言)。
  4. 加载本地方法库中的方法

七、PC寄存器

程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的。
PC寄存器是一个指针,指向方法区中的方法字节码(即将执行的指令代码),让执行引擎读取下一条指令,是一个非常小的内存空间,忽略不计。

八、方法区

Method Area

  1. 方法区是所有线程共享的
  2. 所有字段和方法字节码,以及一些特殊方法,如构造函数,接口方法也在此定义。即所有定义的方法的信息都保存在该区域。
  3. 静态变量(static)、常量(final)、类信息(构造函数,接口方法)、运行时的常量池都在方法区中
  4. 实例变量存在堆内存中,和方法区无关。

九、栈

  1. 也叫栈内存,主管程序的运行,生命周期同线程同步,线程结束栈内存也会释放,不存在垃圾回收问题。
  2. 栈是私有的,每个线程维护一份,存放着8大基本类型、对象引用、实例方法
  3. 栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,栈满了会抛StackOverflowError错误。
    image.png

image.png
image.png

十、堆

一个JVM只有一个堆内存。
image.png

  1. GC垃圾回收,主要是在伊甸园和养老区。
  2. 假设内存满了会报OOM错误,堆内存不够。
  3. JDK8以后,永久存储区改名为元空间。

10.1 新生区

类诞生、成长甚至死亡的地方。

  1. 伊甸园,所有的对象都是在伊甸园new出来的。伊甸园区满就会触发轻GC。
  2. 幸存区(0,1),伊甸园内的对象经历过一次轻GC不被回收就会进入幸存区。如果幸存区也满了会触发重GC。
  3. 新生区内的对象经历过一次重GC不被回收就会进入养老区
  4. 如果N次重GC后新生区仍满,则报OOM(OutOfMemory)错误。

10.2 永久区

常驻內存,用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存来垃圾回收,关闭JVM时会释放永久区內存。

迭代
  1. jdk1.6:永久代,常量池在永久代。(永久代是方法区的实现)
  2. jdk1.7:也有永久代,常量池在堆中。
  3. jdk1.8:元空间,常量池在元空间。

10.3 堆在物理内存

image.png
逻辑上存在,物理上不一定。

public class Test {
    public static void main(String[] args) {
        //默认情况下,分配给JVM的最大内存是电脑内存的1/4,而JVM初始化的内存是电脑内存的1/64
        long maxMemory = Runtime.getRuntime().maxMemory();
        long totalMemory = Runtime.getRuntime().totalMemory();
        System.out.println("JVM试图使用的最大内存: "+maxMemory/1024/1024+" MB");
        System.out.println("JVM初始化总内存: "+totalMemory/1024/1024+" MB");

        //可以在启动java时,
        // 修改 -Xms1024m 设定JVM最大内存,
        // 修改 -Xmx1024m 设定JVM初始化内存,
        // 添加 -XX:+PrintGCDetails 打印GC的运行情况。

        //如果出现了OOM:
        //1.尝试扩大堆内存看结果
        //2.使用工具分析内存Jprofiler。
		//分析Dump内存文件,快速定位内存泄漏, -XX:+HeapDumpOnOutOfMemoryError
		//获取堆中的数据
		//获取大的对象
    }
}

image.png
在物理上只有老年代和年轻代。

Dump内存文件
image.png

十一、GC

image.png

  1. GC时,并不会对新生代、幸存区、老年代三个地方统一回收,大部分都是在使用轻GC回收新生代中的对象。
  2. 轻GC(普通GC)针对新生代和幸存区,重GC(全局GC)针对老年代
  3. 幸存区有两块区域From 和 To,位置会相互交换,谁空谁是To

11.1 GC算法

image.png

标记清除
  1. 扫描内存,标记内存中要存活的对象,
  2. 扫描内存,对没有标记的对象进行清除。
  3. 会产生内存碎片。
  4. 两次扫描,严重浪费时间
  5. 比复制算法相比减少了空间使用。
标记压缩
  1. 优化了标记清除算法,清除对象后会对内存重排序,将存活的对象移到同意端,清除了垃圾碎片。
  2. 相比标记清除算法多了一次移动成本。
复制算法

image.png

  1. 新生代主要用复制算法,幸存区中的对象从From 复制 到 To。
  2. 好处是没有内存碎片
  3. 坏处是浪费空间,多了一倍空间是空的
  4. 适合于对象存活度低的时候。
引用计数法

难处理循环引用的问题,已淘汰。

11.2 轻重GC

  1. 每次轻GC,都会将试图释放伊甸园区中的对象,并把存活下来的对象移至幸存区(To区),轻GC后伊甸园空间为空。
  2. 幸存区中的两个区域会互相交换内容,保证其中一个区是空的,空的为To区。
  3. 当一个对象经历了15次GC还没有被回收,就会进入老年代。可以通过 -XX:MaxTenuringThreshold=5调节进去老年代的时间
  4. 重GC针对老年代中的对象。

11.3 分代收集算法

年轻代存活率低,使用复制算法。
老年代区域大,存活率高,使用清除标记(内存碎片不多)+标记压缩算法(内存碎片多)混合实现。

十二、JMM

Java Memory Model,Java内存模型

12.1 概念

作用:缓存一致性协议,用于定义数据读写的规则。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主存中(Main Memory),每个线程都有一个私有的本地内存(Local Memory)。本地内存是从主存中拷贝的。

12.2 问题

可见性问题
  1. CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中
  2. 使用volatile关键字,强制每次对象修改都刷新到主存。
竞争现象
  1. 线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到自己的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,并且这两个线程都对Obj.count做了加1操作。两个加1操作是并行的,不管是线程A还是线程B先flush计算结果到主存,最终主存中的Obj.count只会增加1次。
  2. 使用synchronize,保证多线程不能同时修改该对象。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议