Java Notes: 从内存看Java,异常处理

以以下程序作为例子:


public class CustomerTest {
    public static void main(String[] args) {
        Customer[] list = new Customer[5];
        Customer c = new Customer("Jeb", "Bush");
        list[0] = c;
        list[1] = new Customer("George", "Bush");
        ...
} }

Java程序以main函数作为入口,看到关于Customer的new关键字,找Class Customer的定义,然后在字节码(前端编译器之后,后端解释器和编译器前)中:




其中,<cinit>部分是初始化静态变量的字节码。初始化一个类时,首先执行的是这部分。这就是为什么之前在文章《初始化与清理部分》提到的,Java的初始化顺序是:静态变量、类普通变量、构造函数。引用[1]的说法:<init> is the (or one of the) constructor(s) for the instance, and non-static field initialization. 所以,普通类成员变量和构造函数是交给<init>处理的。


以下面代码为例:


class X {

   static Log log = LogFactory.getLog(); // <clinit>

   private int x = 1;   // <init>

   X(){
      // <init>
   }

   static {
      // <clinit>
   }

}

接着,字节码里面,存储的就是各种方法的字节码了。


关于Description: 


接下里,讲一下description的概念。在java当中,description会实现多态(dynamic binding)的保证:


每一个类的对象创建之后,在堆内存里的存储内容如图所示:




这里假设这个Customer有三个普通类变量:ID(int), First Name (String) 和 Last Name (String)。那么,前面的那个引用是指向哪里?就是对应的Customer类的description。Description的每个格子(引用)实质是指向这个类的整个“族谱”的引用。如下图所示:




其指向的是其本身和父类的字节码。遇到一个方法调用,如果,引用是本身的类的,首先在本身的字节码内寻找对应的方法名,如果没有找到,再到父类中寻找。


那么,如果引用的父类的,就从父类的字节码开始找起。多态的实现基础便是如此。


讲讲方法的栈内容(Stack):


在Java当中,每一个线程(注意是线程,不是进程)都有自己的stack frame。每个stack frame里面存的都是方法的一些local变量。(顺便提一句,这是和Linux的shell脚本语言是相反的,在shell的函数的变量默认是全局的,这样很容易引起同名变量的篡改。需要使用local使得其中的变量变为本地变量。)


如下图所示(引用自CS:APP):




这幅图用的是x86-32的习惯,调用函数前,把函数对象的参数按顺序依次push到上一个stack frame的中。然后,call下一个函数的时候,call函数自动把返回地址(下一个指令在内存中的地址)存在栈里面,然后再跳到对应的函数的开始指令地址。


对于x86-64的机器,习惯使用%rdi, %rsi, %rdx, %rcx, r8, %r9等作为参数变量寄存器,在call下一个函数前,用mov指令给对应的寄存器赋值即可。


Saved register就是进入了下一个函数之后,进行callee save的一些寄存器的值。在子函数里调用push实现。


This 和 super的区别:


static方法和普通类方法最大的区别在于,类方法的栈区域当中存在this引用的空间。这里顺便也说一句,相比于this,另外一个特别的关键词super则不是这样的。super并不是存在于方法的引用,其只是一个和编译器的约定暗号。一旦看到super调用的方法,自动从description的父类方法字节码可以搜起。


异常处理的几个需要知道的点:


我们之前程序出错时,在控制台看到的信息其实是程序把Runtime Exceptions抛出来了,由于我们并没有写任何catch RunTime Exception的代码,所以JVM默默的接受了这个Exception。然后,JVM对于所有exception的默认行为都是e.printStackTrace()。


咦,之前不是说抛异常应该是跑checked exceptions吗?Runtime exceptions和Runtime Errors不应该是Unchecked的吗?


是的,理论上来说,声明throws和实际throw(注意两者不同,一个只是声明,没有处理;后者是直接处理)的都应该是Checked的异常。其中有两个原因:


1)客观原因:Checked异常之所以被称为被检查的,是因为它是编译器会检查你这段代码可能会产生什么异常,如果发现有被检查的异常可能存在你的代码,编译器就是强制你要不就声明,要不就在代码当中自己处理;

2)主观原因:Checked异常产生的原因是库程序员写的代码被别人调用时,本身可能存在的问题。而这些问题一般能用catch回复。而Unchecked的exception更多是调用者的代码的问题。而且就算catch到了,也没有什么处理的可能。必须重新改代码才能正常运行。


但是,runtime exceptions是可以被catch的。


插一句题外话:JVM默认std error只输出最多1024行。递归时如果stack overflow时产生了1024行不要以为只被调用了1024次。可以通过java -XX设置,远不止1024次调用。


下面是关于Exceptions的一些实用的代码技巧:


public static int factorial(int n) {
    if (n < 0) {
        throw new IllegalArgumentException(”Negative nu...
}
    if (n == 1 || n == 0) return 1;
    return n * factorial(n-1);
}


在new时,在构造函数中插入字符串,是这个exception的message。可以在catch到exception时,使用:


catch(IllegalArgumentException e){
  System.out.println(e.getMessage());
}

来查看。


另外,子类的exceptions应该在其父类之前,否则never reach。


另外,可以使用finally语句来关闭一些资源。因为它一定会被执行。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值