JVM学习02-虚拟机的基本结构

11 篇文章 0 订阅

JVM学习02-虚拟机的基本结构

1.虚拟机的基本结构

虚拟机的结构可以参考 java虚拟机规范 进行理解

  • 类加载子系统

加载类信息(class)到方法区

  • 方法区

存放类信息、运行时常量池信息(字符串和数字常量)

  • java堆

虚拟机启动时建立的内存工作区域,存放几乎所有java对象实例,所有线程共享。

  • 直接内存(NIO)

java堆外的直接向系统申请的内存,可以由java的NIO库进行操作。速度优于java堆,适用于频繁读写,不受Xmx最大内存限制,受操作系统最大内存限制

  • 垃圾回收系统

可以对方法区、java堆和直接内在进行回收。重点回收java堆。

  • java栈

每个java虚拟机线程都有一个私有的java栈,在线程被创建的时候创建。java栈中保存着栈帧信息,还保存着局部变量、方法参数,与java方法的调用 、返回密切相关。

  • 本地方法栈

java虚拟机的重要扩展,本地方法(通常C编写 )

  • Program Counter 程序计数器/PC寄存器

程序计数器也是线程私有空间。

  • 执行引擎

核心组件,执行虚拟机字节码。

2.堆空间结构

堆空间分为新生代和老年代

  • 新生代:新生对象或年龄不大的对象

    • eden区

      对象首先分配在eden区,新生代回收后,如果对象还存活,则会进入 s0或者s1区

    • s0区(from区)和s1区(to区),大小相等的两块,可以互换角色的内存空间

      每经一次新生代回收,存活的对象的年龄会加1,达到一定条件后会被认为是老年对象进入老年区。

      (问题,判定老年对象的条件是什么)

  • 老年代

堆、方法区、栈的关系

3.函数调用与java栈

线程的执行的基本行为是函数调用,每次函数调用的数据都是通过java栈传递。java栈的主要内容是栈帧,每次函数调用都会有栈帧压入java栈,函数调用结束栈帧被弹出java栈。

函数返回时,栈帧从java栈中弹出。java方法有两种返回函数的方式,一种是return,一种是抛出异常。一个栈帧中至少要包含局部变量表、操作数栈和栈帧数据区几个部分。

由于每次函数调用都会生成对应的栈帧,所以会占用栈空间,因此,如果栈空间不足,那函数调用无法继续进行下去,当请求的栈尝试大于最大可用栈深度时,系统就会抛出StackOverFlowError栈溢出错误。可以使用参数 -Xss来指定线程的最大栈空间,这个参数决定了函数的最大的调用深度。函数嵌套调用的层次,取决于栈空间的大小,栈越大,函数就可以嵌套越多次。

  • 局部变量表

    保存函数的参数及局部变量。

    ps:相同的栈容量下,局部变量少的函数,可以支持更深的函数调用,如果函数的参数和局部变量较多,会使局部变量表膨胀,导致函数嵌套次数变少。

    栈帧中的局部量变量中的槽位是可以重用的,局部变量过了作用域,后续新申请的局部变量可能会复用过作用域的局部变量的槽位,用以节约资源。

    以下代码来测试局部变量与垃圾回收关系

    /**
     *
     * 测试变量的GC结果
     * 执行时增加jvm参数  -XX:+PrintGC
     * @author domino
     * @date 2021/5/17
     */
    public class TestLocalVariable {
    
        /**
         * 有引用指向变量不会被回收
         */
        public void localVarGc1() {
            byte[] a = new byte[6 * 1024 * 1024];
            System.gc();
        }
    
        /**
         * 无引用指向变量被回收
         */
        public void localVarGc2() {
            byte[] a = new byte[6 * 1024 * 1024];
            a = null;
            System.gc();
        }
    
        /**
         * 虽然变量超过作用域,但是变量仍存在于局部变量表中,并且也指向数组,所以不会被立即回收
         */
        public void localVarGc3() {
            {
                byte[] a = new byte[6 * 1024 * 1024];
            }
            System.gc();
        }
    
        /**
         * a变量超过了作用域,a变量失效,新建变量c,使用了a变量的字,a变量被销毁,垃圾回收可以正常回收数组
         */
        public void localVarGc4() {
            {
                byte[] a = new byte[6 * 1024 * 1024];
            }
            int c = 10;
            System.gc();
        }
    
        /**
         * 调用 localVarGc1 方法以后,localVarGc1的栈帧被销毁,栈帧中的局部变量被销毁,所以可以正常垃圾回收
         */
        public void localVarGc5() {
            localVarGc1();
            System.gc();
        }
    
        public static void main(String[] args) {
            TestLocalVariable t1 = new TestLocalVariable();
    //        t1.localVarGc1();
    //        t1.localVarGc2();
    //        t1.localVarGc3();
    //        t1.localVarGc4();
            t1.localVarGc5();
        }
    }
    

    方法1到方法5的运行结果如下,可以结合方法注释来看结果原因。

    ----------------------
    [GC (System.gc())  10080K->6824K(251392K), 0.0048977 secs]
    [Full GC (System.gc())  6824K->6599K(251392K), 0.0056204 secs]
    ----------------------
    [GC (System.gc())  16676K->6831K(251392K), 0.0008493 secs]
    [Full GC (System.gc())  6831K->411K(251392K), 0.0045246 secs]
    ----------------------
    [GC (System.gc())  10487K->6619K(251392K), 0.0010462 secs]
    [Full GC (System.gc())  6619K->6555K(251392K), 0.0041690 secs]
    ----------------------
    [GC (System.gc())  14010K->6555K(251392K), 0.0003860 secs]
    [Full GC (System.gc())  6555K->411K(251392K), 0.0036868 secs]
    ----------------------
    [GC (System.gc())  9177K->6651K(251392K), 0.0011968 secs]
    [Full GC (System.gc())  6651K->6555K(251392K), 0.0050845 secs]
    [GC (System.gc())  6555K->6555K(251392K), 0.0005586 secs]
    [Full GC (System.gc())  6555K->411K(251392K), 0.0042518 secs]
    

    使用工具jclasslib可以查看函数的中变量的作用域、槽位的索引、变量名、数据类型等等,这个文档写着比较麻烦,后面补充

  • 操作数栈

    用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

  • 帧数据区

    栈帧用来支持常量池解析、正常方法返回和异常处理等的数据

    • 帧数据区中保存着常量池的指针,方便程序访问常量池。(疑问:常量池在什么区?)
    • 异常处理表是帧数据区中重要部分,在处理异常时,虚拟机必须有一个异常处理表方便发生异常时找到处理的代码。
  • 栈上分配

    java虚拟机优化技术,将线程私有对象分配到栈上,不分配到堆,函数调用后自行销毁,不需要垃圾回收提高性能。

    栈上分配的技术基础是逃逸分析,判断对象的作用域是否能逃出函数体。如果不能,可能被分配到栈上而不在堆上。

    测试代码:

    /**
     * 本类测试栈上分配,逃逸分析,局部变量范围未超出函数体可能会被分配到栈而不是堆
     *
     * 执行方法要加以下jvm参数
     * -server -Xmx10m -Xms5m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
     * 说明:
     * -server server模式下才会启用逃逸分析
     * -Xmx10m 指定堆空间最大内存为10M
     * -XX:+DoEscapeAnalysis 启用逃逸分析
     * -XX:+PrintGC 打印GC日志
     * -XX:-UseTLAB 关闭TLAB Thread Local Allocation Buffer 线程本地分配缓存
     * -XX:+EliminateAllocations 开启标量替换(默认是打开的),允许将对象打散分配到栈上,如对象拥有id和name
     *      这两人个变量将被视为两个独立的局部变量进行分配
     * 执行结果
     * [GC (Allocation Failure)  1023K->565K(5632K), 0.0012699 secs]
     * [GC (Allocation Failure)  1589K->613K(6656K), 0.0015624 secs]
     * 9
     *  GC (Allocation Failure) : 发生了一次垃圾回收,若前面有Full则表明是Full GC,没有Full的修饰表明这是一次Minor GC
     *  。注意它不表示只GC新生代,括号里的内容是gc发生的原因,这里的Allocation Failure的原因是年轻代中没有足够区域能够存放需要分配的数据而失败,。
     *  参考:https://blog.csdn.net/topdeveloperr/article/details/88874957
     *
     *
     * @author dmn
     * @date 2021/5/19
     */
    public class OnStackTest {
        
        public static class User {
            public int id = 0;
            public String name = "";
        }
    
        public static void alloc() {
            User user = new User();
            user.id = 5;
            user.name = "dmn";
        }
    
        public static void main(String[] args) throws InterruptedException {
            long a = System.currentTimeMillis();
            for (int i = 0; i < 200000000; i++) {
                alloc();
            }
            long b = System.currentTimeMillis();
            System.out.println(b - a);
        }
    }
    

    以上代码执行结果如下,我们可以看到,执行完了,中间只出现过3次GC

    [GC (Allocation Failure)  1023K->519K(5632K), 0.0006984 secs]
    [GC (Allocation Failure)  1543K->567K(5632K), 0.0011059 secs]
    [GC (Allocation Failure)  1591K->583K(5632K), 0.0010251 secs]
    8
    

    而作为对比,我们如果将上述代码中的main方法改为如下:

        public static void main(String[] args) throws InterruptedException {  
            List<User> userList = new ArrayList<User>();
            for (int i = 0; i < 200000000; i++) {
                User user = new User();
                user.id = 5;
                user.name = "dmn";
                userList.add(user);
                System.out.println(i);
            }
        }
    

    执行的结果为如下,加了输出以后,可以看到,当集合加入了第240096个user对象以后,就发生了OutOfMemoryError了,说明内存最多放下240096个对象,不是内存够用。

    [GC (Allocation Failure)  1023K->558K(5632K), 0.0007154 secs]
    [GC (Allocation Failure)  1580K->937K(5632K), 0.0015253 secs]
    [GC (Allocation Failure)  1961K->1667K(5632K), 0.0011996 secs]
    [GC (Allocation Failure)  2691K->2705K(6656K), 0.0015517 secs]
    [GC (Allocation Failure)  4753K->4378K(6656K), 0.0023053 secs]
    [Full GC (Ergonomics)  4378K->3657K(9728K), 0.0302245 secs]
    [GC (Allocation Failure)  4790K->4833K(8704K), 0.0056917 secs]
    [GC (Allocation Failure)  5857K->5907K(9216K), 0.0018717 secs]
    [GC (Allocation Failure)  6931K->6971K(9216K), 0.0022074 secs]
    [Full GC (Ergonomics)  6971K->6211K(9216K), 0.0428260 secs]
    [GC (Allocation Failure)  6981K->7139K(9216K), 0.0035979 secs]
    [Full GC (Ergonomics)  7139K->6981K(9216K), 0.0411045 secs]
    [GC (Allocation Failure)  6981K->6981K(9216K), 0.0008322 secs]
    [Full GC (Allocation Failure)  6981K->6963K(9216K), 0.0573462 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at java.util.Arrays.copyOf(Arrays.java:3210)
    	at java.util.Arrays.copyOf(Arrays.java:3181)
    	at java.util.ArrayList.grow(ArrayList.java:265)
    	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
    	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
    	at java.util.ArrayList.add(ArrayList.java:462)
    	at cn.shutdown.demo.OnStackTest.main(OnStackTest.java:57)
    
    Process finished with exit code 1
    
    

    但是很奇怪的是我去掉了-XX:+DoEscapeAnalysis-XX:+EliminateAllocations两个jvm参数以后,执行的结果也还是一样,在配置较低的Linux服务器上面执行也是一样,没有像书中介绍的出现大量的GC日志。

    去掉参数使用idea执行结果
    [GC (Allocation Failure)  1023K->533K(5632K), 0.0008148 secs]
    [GC (Allocation Failure)  1557K->597K(5632K), 0.0015174 secs]
    [GC (Allocation Failure)  1621K->597K(5632K), 0.0021861 secs]
    9
    
    在centos中执行去掉参数的结果
    [root@hdss7-188 temp]# java -server -Xmx10m -Xms10m -XX:+PrintGC -XX:-UseTLAB  OnStackTest
    [GC (Allocation Failure)  2751K->255K(9920K), 0.0006507 secs]
    [GC (Allocation Failure)  3007K->255K(9920K), 0.0003628 secs]
    [GC (Allocation Failure)  3007K->255K(9920K), 0.0001592 secs]
    16
    

4.方法区

方法区是所有线程共享的内存区域,用于保存系统的类信息,类的字段、方法、常量池等。方法区大小决定系统可以保存多少个类。如果类太多可能导致方法区溢出而抛出内在溢出错误。

jdk1.6、jdk1.7中,方法区理解为永久区Perm。使用-XX:PermSize和-XX:MaxPermSize指定,默认-XX:MaxPermSize为64MB。如果使用了动态代理 ,可能会在运行时生成 大量的类,那就需要设置一个合理的永久区大小。

测试方法区代码:

import java.util.HashMap;

/**
 * 测试方法区/永久区,
 * 用1.7的jdk运行加以下参数
 * -XX:PermSize=5m -XX:+PrintGCDetails -XX:MaxPermSize=5m
 * 1.8的jdk运行加以下参数
 * -XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails
 *
 * @author domino
 * @date 2021/5/20
 */
public class PermTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            Map<String, Object> map = new HashMap<>();
            map.put("id", Integer.class);
            map.put("name", String.class);
           //书上的数量是100000,但很快就正常运行完了,所以我把数量提升到了2000000000
            for (i = 0; i < 2000000000; i++) {
                CglibBean bean = new CglibBean("cn.shutdown.demo.Permtest" + i, map);
            }
        } catch (Throwable e) {
            System.out.println("total create count" + i);
        } finally {
            System.out.println("total create count" + i);
        }
    }
}

import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class CglibBean {

    @SuppressWarnings("unchecked")
    public CglibBean(Map propertyMap) {
        Object object = generateBean(propertyMap);
        BeanMap beanMap = BeanMap.create(object);
    }

    @SuppressWarnings("unchecked")
    private Object generateBean(Map propertyMap) {
        BeanGenerator generator = new BeanGenerator();
        Set keySet = propertyMap.keySet();
        for (Iterator i = keySet.iterator(); i.hasNext(); ) {
            String key = (String) i.next();
            generator.addProperty(key, (Class) propertyMap.get(key));
        }
        return generator.create();
    }
} 

因为我的本机的jdk是1.8版本,已经移除了永久区,所以使用-XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails 的参数去运行,但是100000的数量很快就执行完了,执行的结果如下。

[GC (Allocation Failure) [PSYoungGen: 65536K->1042K(76288K)] 65536K->1050K(251392K), 0.0025167 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 66578K->656K(76288K)] 66586K->672K(251392K), 0.0028107 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
total create count100000
Heap
 PSYoungGen      total 76288K, used 30148K [0x00000007aa900000, 0x00000007b3e00000, 0x00000007ffe00000)
  eden space 65536K, 45% used [0x00000007aa900000,0x00000007ac5cd130,0x00000007ae900000)
  from space 10752K, 6% used [0x00000007af380000,0x00000007af424010,0x00000007afe00000)
  to   space 10752K, 0% used [0x00000007ae900000,0x00000007ae900000,0x00000007af380000)
 ParOldGen       total 175104K, used 16K [0x00000006ffe00000, 0x000000070a900000, 0x00000007aa900000)
  object space 175104K, 0% used [0x00000006ffe00000,0x00000006ffe04000,0x000000070a900000)
 Metaspace       used 3822K, capacity 5028K, committed 5120K, reserved 1056768K
  class space    used 418K, capacity 458K, committed 512K, reserved 1048576K

Process finished with exit code 0

这不行啊,没整出来活啊,我得要他溢出啊,他没溢啊,然后我就将循环的数量提升到了2000000000(多少个零自己数去)。然后用Run With VisualVM来运行,在监控的图里面去看元数据区里面的内存占用效果。结果跑了好长的时间,分配 的10M元数据区的内存也还是没溢出(这里书上要求是设置5M的,但估计是因为我的系统和jdk都是64位的,所以IDE提示5M内存太小,然后我试着的结果是最小10M)。

然后就想试试1.7是什么效果,因为电脑只装了1.8,所以就去官网去下载jdk1.7,结果,好家伙,jdk1.7不提供下载了,官网的大概意思是,jdk1.7只提供给付了支持费的用户下载,还要提单子,于是找了别的下载,安装到电脑上面。jdk1.7百度盘下载链接

Idea上面配置了jdk1.7以后,将demo项目的编译jdk配置为1.7,PermTest类的jre也指定1.7进行运行。

然后再运行项PermTest代码

public class PermTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            for (i = 0; i < 10000000; i++) {
                CglibBean bean = new CglibBean("cn.shutdown.demo.Permtest" + i, new HashMap());
            }
        } catch (Throwable e) {
            System.out.println("total create count" + i);
        } finally {
            System.out.println("total create count" + i);
        }
    }
}

好家伙,按 -XX:PermSize=5m -XX:+PrintGCDetails -XX:MaxPermSize=5m这个参数去执行竟然正常的执行完了。也没有出现永久区溢出的情况。也许是现在的电脑比起作者写书的时候的电脑的性能强悍的太多了?

[GC [PSYoungGen: 66048K->1003K(76800K)] 66048K->1011K(251392K), 0.0021120 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC [PSYoungGen: 67051K->576K(76800K)] 67059K->592K(251392K), 0.0020370 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC [PSYoungGen: 66624K->624K(76800K)] 66640K->640K(251392K), 0.0008780 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC [PSYoungGen: 66672K->640K(142848K)] 66688K->656K(317440K), 0.0014140 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC [PSYoungGen: 132736K->640K(142848K)] 132752K->656K(317440K), 0.0042810 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC [PSYoungGen: 132736K->528K(265216K)] 132752K->552K(439808K), 0.0020670 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC [PSYoungGen: 196752K->320K(265216K)] 196776K->816K(439808K), 0.0026410 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 320K->0K(265216K)] [ParOldGen: 496K->489K(95232K)] 816K->489K(360448K) [PSPermGen: 4095K->4094K(4096K)], 0.0140030 secs] [Times: user=0.03 sys=0.01, real=0.02 secs] 
....略过一部分日志....
Heap
 PSYoungGen      total 563200K, used 5749K [0x00000007aaa80000, 0x00000007cd780000, 0x0000000800000000)
  eden space 562176K, 1% used [0x00000007aaa80000,0x00000007ab01d460,0x00000007ccf80000)
  from space 1024K, 0% used [0x00000007ccf80000,0x00000007ccf80000,0x00000007cd080000)
  to   space 1024K, 0% used [0x00000007cd680000,0x00000007cd680000,0x00000007cd780000)
 ParOldGen       total 1104896K, used 427K [0x0000000700000000, 0x0000000743700000, 0x00000007aaa80000)
  object space 1104896K, 0% used [0x0000000700000000,0x000000070006ae98,0x0000000743700000)
 PSPermGen       total 4096K, used 4094K [0x00000006ffc00000, 0x0000000700000000, 0x0000000700000000)
  object space 4096K, 99% used [0x00000006ffc00000,0x00000006fffffa90,0x0000000700000000)

Process finished with exit code 0

然后我再调小参数,使用-XX:PermSize=3m -XX:+PrintGCDetails -XX:MaxPermSize=1m,然后内存终于溢出了,报出了PermGen space ,这也就是方法区(永久区)溢出了。整活完毕。

[GC [PSYoungGen: 2642K->384K(76800K)] 2642K->384K(251392K), 0.0015940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 384K->0K(76800K)] [ParOldGen: 0K->182K(122368K)] 384K->182K(199168K) [PSPermGen: 2047K->2046K(2048K)], 0.0079460 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC [PSYoungGen: 1321K->32K(94208K)] 1503K->214K(216576K), 0.0008100 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(94208K)] [ParOldGen: 182K->182K(232448K)] 214K->182K(326656K) [PSPermGen: 2047K->2047K(2048K)], 0.0051070 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC [PSYoungGen: 0K->0K(120832K)] 182K->182K(353280K), 0.0012080 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 0K->0K(120832K)] [ParOldGen: 182K->172K(351744K)] 182K->172K(472576K) [PSPermGen: 2047K->2047K(2048K)], 0.0058910 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC [PSYoungGen: 2201K->32K(132608K)] 2374K->204K(484352K), 0.0004310 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(132608K)] [ParOldGen: 172K->172K(537600K)] 204K->172K(670208K) 
....省略部分日志....
Error occurred during initialization of VM
java.lang.OutOfMemoryError: PermGen space
	at sun.misc.Launcher.<init>(Launcher.java:71)
	at sun.misc.Launcher.<clinit>(Launcher.java:57)
	at java.lang.ClassLoader.initSystemClassLoader(ClassLoader.java:1489)
	at java.lang.ClassLoader.getSystemClassLoader(ClassLoader.java:1474)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值