深入理解JVM(三,字节码的分析与应用,内存空间的划分)

关键字synchronized的限制体现:作用于方法。

package com.bytecode.stu;

public class IOStudy {
protected int a = 1;

 protected String name = "zhouyi";

public int getA() {
    return a;
}

public void setA(int b) {
    this.a = b;
}

public synchronized String nameRrturn(String name) {
    return name;
}
}

反编译看看结果:由下可知, 只是加了一个该关键字的标识符ACC_SYNCHRONIZED
在这里插入图片描述

在这里插入图片描述
作用于对象时:
在这里插入图片描述

 public java.lang.String nameRrturn(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
  stack=2, locals=4, args_size=2
     0: aload_1
     1: dup
     2: astore_2
     3: monitorenter   //从这里开始同步
     4: aload_1
     5: aload_2
     6: monitorexit   //这里结束
     7: areturn
     8: astore_3
     9: aload_2
    10: monitorexit  //这个是防止异常时怎么办?保证异常退出
    11: aload_3
    12: athrow
  Exception table:
     from    to  target type
         4     7     8   any
         8    11     8   any
  LineNumberTable:
    line 17: 0
    line 18: 4
    line 19: 8
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      13     0  this   Lcom/bytecode/stu/IOStudy;
        0      13     1  name   Ljava/lang/String;
  StackMapTable: number_of_entries = 1
    frame_type = 255 /* full_frame */
      offset_delta = 8
      locals = [ class com/bytecode/stu/IOStudy, class java/lang/String, class java/lang/Object ]
      stack = [ class java/lang/Throwable ]
}

前面多次看到每一个this变量。这就很好解释了我们的常识,那就时每一个类中可以直接用this关键字。
对于Java类中的每一个实例方法(非静态方法),其编译生成的字节码当中,方法的参数的数量总会比代码中要多一个(这就是this),它位于方法参数的第一个参数位置。
这个操作是在编译期间中的,在运行期间JVM调用调用时,自动向实例方法传入this参数。所以,在实例方法的局部变量中,至少有一个当前对象的局部变量。

借助字节码分析java异常处理机制
原来是靠硬记了很多的异常,今天突然感觉自己很傻!!!!
最好的办法是:如下的逻辑是最简单的,不需要你组记忆的。????????????
在这里插入图片描述
回到正题:

 public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=5, locals=3, args_size=1 //args_size=1这个就是this, locals=3是this,is和某一个catch块的e。
     0: new           #2                  // class java/io/FileInputStream
     3: dup
     4: new           #3                  // class java/io/File
     7: dup
     8: ldc           #4                  // String zhouyi.txt
    10: invokespecial #5                  // Method java/io/File."<init>":(Ljava/lang/String;)V
    13: invokespecial #6                  // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
    16: astore_1
    17: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
    20: ldc           #8                  // String finally
    22: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    25: goto          63
    28: astore_1
    29: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
    32: ldc           #8                  // String finally
    34: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    37: goto          63
    40: astore_1
    41: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
    44: ldc           #8                  // String finally
    46: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    49: goto          63
    52: astore_2
    53: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
    56: ldc           #8                  // String finally
    58: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    61: aload_2
    62: athrow
    63: return
  Exception table:
     from    to  target type
         0    17    28   Class java/io/FileNotFoundException
         0    17    40   Class java/lang/Exception
         0    17    52   any
  LineNumberTable:
    line 11: 0
    line 18: 17
    line 19: 25
    line 13: 28
    line 18: 29
    line 19: 37
    line 15: 40
    line 18: 41
    line 19: 49
    line 18: 52
    line 19: 61
    line 20: 63
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      64     0  this   Lcom/bytecode/stu/IOStudy;
  StackMapTable: number_of_entries = 4
    frame_type = 92 /* same_locals_1_stack_item */
      stack = [ class java/io/FileNotFoundException ]
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/Exception ]
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/Throwable ]
    frame_type = 10 /* same */
}

工具查看:可以看到。
Java字节码对于异常的处理方式:
1,统一采用异常表的方式来对异常进行处理。
2,在之前的1.4以前不是。
3,存在finally时,JVM采取的方式将finally的字节码拼接到每一个catch处理中。所以有两个catch,那么就有两个finally操作,为了解决大量的跳转问题。
在这里插入图片描述
在这里插入图片描述
如果是直接在方法上抛出呢?
可以看出是和code是同级的,对的!code是体现方法里面的内容。
在这里插入图片描述
异常无非这两种处理。

栈帧
栈帧是一种用于帮助虚拟机执行方法调用和方法执行。栈帧
本身是一种数据结构,封装了方法的局部变量、动态连接信息、方法的返回地址以及操作数栈等信息。
符号引用和直接引用:有些符号引用是在类加载阶段或是第一次使用就会转为直接引用,这种转换叫做静态解析:另外一些符号引用则是在每次运行期转为直接引用,这种叫做动态链接,这体现了Java
大概有五种 :
① invokeinterface : 调用接口中的方法,实际是在运行期是决定的,决定到底调用实现该接口的哪一个对象的特定对象。
② invokestatic : 调用静态方法。
③ invokespecial : 调用自己的私有方法、构造方法(init)以及父类的方法。
④ invokevirtual : 调用虚方法,运行期动态查找过程。
⑤ invokedynamic : 动态调用方法。

那么非虚方法呢?
1,静态方法;2,父类方法;3,构造方法;4,私有方法
以上的四种方法,他们是在被加载阶段可以将符号引用转为直接引用。
② 运行结果:
在这里插入图片描述
③ 运行结果:

package com.bytecode.stu;

/**
 * 方法的静态分派。
* Father F1 = new Son();这段代码中,F2的静态类型是Father,而F1真正指向的类型是Son
* 我们可以得出这样一个结论:变量的静态是不会发生改变的,而变量的实际类型则是可以发生变化的
 * 这也是多态的一种体现,实际类型是在运行期间决定的。
 */
public class IOStudy {
  // 方法的重载是静态的行为(这和重写是不同)
 private void test(Father father){
    System.out.println("Father");
 }
public void test(Son son) {
    System.out.println("son");
}

public void test(GrandSon son) {
    System.out.println("GrandSon");
}
public static void main(String[] args) {
    Father F1 = new Son();
    Father F2 = new GrandSon();

    IOStudy ioStudy = new IOStudy();
    ioStudy.test(F1); // 运行结果是这样,是因为拿了静态的Father类型去匹配的
    ioStudy.test(F2);
}
}

class Father{ }

class Son extends Father{ }

class GrandSon extends Son{ }

运行结果:
在这里插入图片描述
在这里插入图片描述
④运行结果:(这里在强调一遍:方法重载是静态的,是编译期行为;方法重写是动态的,是运行期行为)

package com.bytecode.stu;
/**
*方法的动态分派,但是我们发现全是Father的test,这就涉及一个重要的概念:方法接收者。
*/
public class IOStudy {

public static void main(String[] args) {
    Father son = new Son();
    Father grandSon = new GrandSon();

    son.test();
    grandSon.test();

    son = new GrandSon();
    son.test();
}
}

class Father{
public void test(){
    System.out.println("Father");
}
}

class Son extends Father{

@Override
public void test() {
    System.out.println("son");
}
}

class GrandSon extends Son{

@Override
public void test() {
    System.out.println("GrandSon");
}
}

运行结果:
在这里插入图片描述
在这里插入图片描述
上面的疑惑?就得引入虚方法表的概念。
针对于方法的动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table,vtable).
针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table,vtable)

利用字节码审视Java动态代理实例。
接口:

package com.bytecode.stu;

public interface Subject {

public void request();
}

实现真正的操作:

public class RealSubject implements Subject {
@Override
public void request() {
    System.out.println("real subject");
}
}

动态代理:

package com.bytecode.stu;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DynamicSubject implements InvocationHandler {

private Object object;

public DynamicSubject(Object obj) {
    this.object = obj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("before");
    method.invoke(this.object,args);
    System.out.println("after");
    return null;
}
}

测试类:

package com.bytecode.stu;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class IOStudy {

public static void main(String[] args) {

    RealSubject realSubject = new RealSubject();
    InvocationHandler handler = new DynamicSubject(realSubject);
    Class<?> clazz = realSubject.getClass();

    Subject subject = (Subject) Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),handler);
    subject.request();
}
}

运行结果:
在这里插入图片描述
从字节码角度去分析:
上面的每一步请采用字节码的思维方式去分析。

最后JVM内存空间的划分
虚拟机栈:Stack Frame 栈帧。
程序计数器(Program Counter):
本地方法栈:主要用于处理本地方法。
堆(Heap):
在这里插入图片描述
1、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看

做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,

各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变

这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、

线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现

的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行

一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要

有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内

存区域为“线程私有”的内存。

如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节

码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此

内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

2、Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,

它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执

行的时候都会同时创建一个栈帧(Stack Frame ①)用于存储局部变量表、操作栈、动态

链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在

虚拟机栈中从入栈到出栈的过程。

经常有人把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗

糙,Java 内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序

员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后

面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变

量表部分。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、

float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟

机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或

者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。

其中64 位长度的long 和double 类型的数据会占用2 个局部变量空间(Slot),其余

的数据类型只占用1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个

方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间

不会改变局部变量表的大小。

在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大

于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展

(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的

虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。

3、本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其

区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则

是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语

言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至

有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError

异常。

4、Java 堆

对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的

一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的

唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚

拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器

的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙

的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage

Collected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在

收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;

再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配

的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local

Allocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,

存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配

内存。
4、方法区

方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存

储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽

然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-

Heap(非堆),目的应该是与Java 堆区分开来。

对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区

称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚

拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而

已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即

使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”

至Native Memory 来实现方法区的规划了。

Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内

存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾

收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一

样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸

载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件

相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出

现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导

致内存泄漏。

根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出

OutOfMemoryError 异常。
句柄访问方式
在这里插入图片描述
直接指针访问方式
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值